refactor: simplify code formatting and improve readability across multiple files
This commit is contained in:
@@ -22,10 +22,7 @@ from app.models.activity_events import ActivityEvent
|
||||
from app.models.agents import Agent
|
||||
from app.models.boards import Board
|
||||
from app.models.tasks import Task
|
||||
from app.schemas.activity_events import (
|
||||
ActivityEventRead,
|
||||
ActivityTaskCommentFeedItemRead,
|
||||
)
|
||||
from app.schemas.activity_events import ActivityEventRead, ActivityTaskCommentFeedItemRead
|
||||
from app.schemas.pagination import DefaultLimitOffsetPage
|
||||
from app.services.organizations import (
|
||||
OrganizationContext,
|
||||
@@ -198,10 +195,7 @@ async def list_task_comment_feed(
|
||||
|
||||
def _transform(items: Sequence[Any]) -> Sequence[Any]:
|
||||
rows = _coerce_task_comment_rows(items)
|
||||
return [
|
||||
_feed_item(event, task, board, agent)
|
||||
for event, task, board, agent in rows
|
||||
]
|
||||
return [_feed_item(event, task, board, agent) for event, task, board, agent in rows]
|
||||
|
||||
return await paginate(session, statement, transformer=_transform)
|
||||
|
||||
|
||||
@@ -53,19 +53,9 @@ from app.schemas.gateway_coordination import (
|
||||
GatewayMainAskUserResponse,
|
||||
)
|
||||
from app.schemas.pagination import DefaultLimitOffsetPage
|
||||
from app.schemas.tasks import (
|
||||
TaskCommentCreate,
|
||||
TaskCommentRead,
|
||||
TaskCreate,
|
||||
TaskRead,
|
||||
TaskUpdate,
|
||||
)
|
||||
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
|
||||
from app.services.activity_log import record_activity
|
||||
from app.services.board_leads import (
|
||||
LeadAgentOptions,
|
||||
LeadAgentRequest,
|
||||
ensure_board_lead_agent,
|
||||
)
|
||||
from app.services.board_leads import LeadAgentOptions, LeadAgentRequest, ensure_board_lead_agent
|
||||
from app.services.task_dependencies import (
|
||||
blocked_by_dependency_ids,
|
||||
dependency_status_by_id,
|
||||
@@ -212,7 +202,8 @@ async def _require_gateway_board(
|
||||
board = await Board.objects.by_id(board_id).first(session)
|
||||
if board is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Board not found",
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Board not found",
|
||||
)
|
||||
if board.gateway_id != gateway.id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
@@ -322,7 +313,8 @@ async def create_task(
|
||||
dependency_ids=normalized_deps,
|
||||
)
|
||||
blocked_by = blocked_by_dependency_ids(
|
||||
dependency_ids=normalized_deps, status_by_id=dep_status,
|
||||
dependency_ids=normalized_deps,
|
||||
status_by_id=dep_status,
|
||||
)
|
||||
|
||||
if blocked_by and (task.assigned_agent_id is not None or task.status != "inbox"):
|
||||
@@ -393,11 +385,7 @@ async def update_task(
|
||||
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
||||
) -> TaskRead:
|
||||
"""Update a task after board-level access checks."""
|
||||
if (
|
||||
agent_ctx.agent.board_id
|
||||
and task.board_id
|
||||
and agent_ctx.agent.board_id != task.board_id
|
||||
):
|
||||
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
return await tasks_api.update_task(
|
||||
payload=payload,
|
||||
@@ -417,11 +405,7 @@ async def list_task_comments(
|
||||
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
||||
) -> LimitOffsetPage[TaskCommentRead]:
|
||||
"""List comments for a task visible to the authenticated agent."""
|
||||
if (
|
||||
agent_ctx.agent.board_id
|
||||
and task.board_id
|
||||
and agent_ctx.agent.board_id != task.board_id
|
||||
):
|
||||
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
return await tasks_api.list_task_comments(
|
||||
task=task,
|
||||
@@ -430,7 +414,8 @@ async def list_task_comments(
|
||||
|
||||
|
||||
@router.post(
|
||||
"/boards/{board_id}/tasks/{task_id}/comments", response_model=TaskCommentRead,
|
||||
"/boards/{board_id}/tasks/{task_id}/comments",
|
||||
response_model=TaskCommentRead,
|
||||
)
|
||||
async def create_task_comment(
|
||||
payload: TaskCommentCreate,
|
||||
@@ -439,11 +424,7 @@ async def create_task_comment(
|
||||
agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
|
||||
) -> ActivityEvent:
|
||||
"""Create a task comment on behalf of the authenticated agent."""
|
||||
if (
|
||||
agent_ctx.agent.board_id
|
||||
and task.board_id
|
||||
and agent_ctx.agent.board_id != task.board_id
|
||||
):
|
||||
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
return await tasks_api.create_task_comment(
|
||||
payload=payload,
|
||||
@@ -454,7 +435,8 @@ async def create_task_comment(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/boards/{board_id}/memory", response_model=DefaultLimitOffsetPage[BoardMemoryRead],
|
||||
"/boards/{board_id}/memory",
|
||||
response_model=DefaultLimitOffsetPage[BoardMemoryRead],
|
||||
)
|
||||
async def list_board_memory(
|
||||
is_chat: bool | None = IS_CHAT_QUERY,
|
||||
@@ -588,7 +570,9 @@ async def nudge_agent(
|
||||
config = await _gateway_config(session, board)
|
||||
try:
|
||||
await ensure_session(
|
||||
target.openclaw_session_id, config=config, label=target.name,
|
||||
target.openclaw_session_id,
|
||||
config=config,
|
||||
label=target.name,
|
||||
)
|
||||
await send_message(
|
||||
message,
|
||||
@@ -605,7 +589,8 @@ async def nudge_agent(
|
||||
)
|
||||
await session.commit()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc),
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
record_activity(
|
||||
session,
|
||||
@@ -657,7 +642,8 @@ async def get_agent_soul(
|
||||
)
|
||||
except OpenClawGatewayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc),
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
if isinstance(payload, str):
|
||||
return payload
|
||||
@@ -671,7 +657,8 @@ async def get_agent_soul(
|
||||
if isinstance(nested, str):
|
||||
return nested
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail="Invalid gateway response",
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="Invalid gateway response",
|
||||
)
|
||||
|
||||
|
||||
@@ -712,7 +699,8 @@ async def update_agent_soul(
|
||||
)
|
||||
except OpenClawGatewayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc),
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
reason = (payload.reason or "").strip()
|
||||
source_url = (payload.source_url or "").strip()
|
||||
@@ -770,9 +758,7 @@ async def ask_user_via_gateway_main(
|
||||
correlation = payload.correlation_id.strip() if payload.correlation_id else ""
|
||||
correlation_line = f"Correlation ID: {correlation}\n" if correlation else ""
|
||||
preferred_channel = (payload.preferred_channel or "").strip()
|
||||
channel_line = (
|
||||
f"Preferred channel: {preferred_channel}\n" if preferred_channel else ""
|
||||
)
|
||||
channel_line = f"Preferred channel: {preferred_channel}\n" if preferred_channel else ""
|
||||
|
||||
tags = payload.reply_tags or ["gateway_main", "user_reply"]
|
||||
tags_json = json.dumps(tags)
|
||||
@@ -801,7 +787,10 @@ async def ask_user_via_gateway_main(
|
||||
try:
|
||||
await ensure_session(main_session_key, config=config, label="Main Agent")
|
||||
await send_message(
|
||||
message, session_key=main_session_key, config=config, deliver=True,
|
||||
message,
|
||||
session_key=main_session_key,
|
||||
config=config,
|
||||
deliver=True,
|
||||
)
|
||||
except OpenClawGatewayError as exc:
|
||||
record_activity(
|
||||
@@ -812,7 +801,8 @@ async def ask_user_via_gateway_main(
|
||||
)
|
||||
await session.commit()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc),
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
record_activity(
|
||||
@@ -867,11 +857,7 @@ async def message_gateway_board_lead(
|
||||
)
|
||||
|
||||
base_url = settings.base_url or "http://localhost:8000"
|
||||
header = (
|
||||
"GATEWAY MAIN QUESTION"
|
||||
if payload.kind == "question"
|
||||
else "GATEWAY MAIN HANDOFF"
|
||||
)
|
||||
header = "GATEWAY MAIN QUESTION" if payload.kind == "question" else "GATEWAY MAIN HANDOFF"
|
||||
correlation = payload.correlation_id.strip() if payload.correlation_id else ""
|
||||
correlation_line = f"Correlation ID: {correlation}\n" if correlation else ""
|
||||
tags = payload.reply_tags or ["gateway_main", "lead_reply"]
|
||||
@@ -903,7 +889,8 @@ async def message_gateway_board_lead(
|
||||
)
|
||||
await session.commit()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc),
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
record_activity(
|
||||
@@ -946,11 +933,7 @@ async def broadcast_gateway_lead_message(
|
||||
boards = list(await session.exec(statement))
|
||||
|
||||
base_url = settings.base_url or "http://localhost:8000"
|
||||
header = (
|
||||
"GATEWAY MAIN QUESTION"
|
||||
if payload.kind == "question"
|
||||
else "GATEWAY MAIN HANDOFF"
|
||||
)
|
||||
header = "GATEWAY MAIN QUESTION" if payload.kind == "question" else "GATEWAY MAIN HANDOFF"
|
||||
correlation = payload.correlation_id.strip() if payload.correlation_id else ""
|
||||
correlation_line = f"Correlation ID: {correlation}\n" if correlation else ""
|
||||
tags = payload.reply_tags or ["gateway_main", "lead_reply"]
|
||||
|
||||
@@ -23,11 +23,7 @@ from app.db import crud
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import async_session_maker, get_session
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import (
|
||||
OpenClawGatewayError,
|
||||
ensure_session,
|
||||
send_message,
|
||||
)
|
||||
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
||||
from app.models.activity_events import ActivityEvent
|
||||
from app.models.agents import Agent
|
||||
from app.models.boards import Board
|
||||
@@ -154,7 +150,8 @@ async def _require_board(
|
||||
board = await Board.objects.by_id(board_id).first(session)
|
||||
if board is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Board not found",
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Board not found",
|
||||
)
|
||||
if user is not None:
|
||||
await require_board_access(session, user=user, board=board, write=write)
|
||||
@@ -162,7 +159,8 @@ async def _require_board(
|
||||
|
||||
|
||||
async def _require_gateway(
|
||||
session: AsyncSession, board: Board,
|
||||
session: AsyncSession,
|
||||
board: Board,
|
||||
) -> tuple[Gateway, GatewayClientConfig]:
|
||||
if not board.gateway_id:
|
||||
raise HTTPException(
|
||||
@@ -246,7 +244,8 @@ def _coerce_agent_items(items: Sequence[Any]) -> list[Agent]:
|
||||
|
||||
|
||||
async def _find_gateway_for_main_session(
|
||||
session: AsyncSession, session_key: str | None,
|
||||
session: AsyncSession,
|
||||
session_key: str | None,
|
||||
) -> Gateway | None:
|
||||
if not session_key:
|
||||
return None
|
||||
@@ -306,7 +305,8 @@ async def _fetch_agent_events(
|
||||
|
||||
|
||||
async def _require_user_context(
|
||||
session: AsyncSession, user: User | None,
|
||||
session: AsyncSession,
|
||||
user: User | None,
|
||||
) -> OrganizationContext:
|
||||
if user is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
@@ -332,7 +332,8 @@ async def _require_agent_access(
|
||||
if not is_org_admin(ctx.member):
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
|
||||
gateway = await _find_gateway_for_main_session(
|
||||
session, agent.openclaw_session_id,
|
||||
session,
|
||||
agent.openclaw_session_id,
|
||||
)
|
||||
if gateway is None or gateway.organization_id != ctx.organization.id:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
@@ -355,7 +356,10 @@ def _record_heartbeat(session: AsyncSession, agent: Agent) -> None:
|
||||
|
||||
|
||||
def _record_instruction_failure(
|
||||
session: AsyncSession, agent: Agent, error: str, action: str,
|
||||
session: AsyncSession,
|
||||
agent: Agent,
|
||||
error: str,
|
||||
action: str,
|
||||
) -> None:
|
||||
action_label = action.replace("_", " ").capitalize()
|
||||
record_activity(
|
||||
@@ -432,10 +436,7 @@ async def _ensure_unique_agent_name(
|
||||
if existing_gateway:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail=(
|
||||
"An agent with this name already exists in this gateway "
|
||||
"workspace."
|
||||
),
|
||||
detail=("An agent with this name already exists in this gateway " "workspace."),
|
||||
)
|
||||
|
||||
desired_session_key = _build_session_key(requested_name)
|
||||
@@ -938,7 +939,9 @@ async def _commit_heartbeat(
|
||||
|
||||
|
||||
async def _send_wakeup_message(
|
||||
agent: Agent, config: GatewayClientConfig, verb: str = "provisioned",
|
||||
agent: Agent,
|
||||
config: GatewayClientConfig,
|
||||
verb: str = "provisioned",
|
||||
) -> None:
|
||||
session_key = agent.openclaw_session_id or _build_session_key(agent.name)
|
||||
await ensure_session(session_key, config=config, label=agent.name)
|
||||
@@ -971,7 +974,8 @@ async def list_agents(
|
||||
col(Gateway.organization_id) == ctx.organization.id,
|
||||
)
|
||||
base_filter = or_(
|
||||
base_filter, col(Agent.openclaw_session_id).in_(gateway_keys),
|
||||
base_filter,
|
||||
col(Agent.openclaw_session_id).in_(gateway_keys),
|
||||
)
|
||||
statement = select(Agent).where(base_filter)
|
||||
if board_id is not None:
|
||||
@@ -987,10 +991,7 @@ async def list_agents(
|
||||
|
||||
def _transform(items: Sequence[Any]) -> Sequence[Any]:
|
||||
agents = _coerce_agent_items(items)
|
||||
return [
|
||||
_to_agent_read(_with_computed_status(agent), main_session_keys)
|
||||
for agent in agents
|
||||
]
|
||||
return [_to_agent_read(_with_computed_status(agent), main_session_keys) for agent in agents]
|
||||
|
||||
return await paginate(session, statement, transformer=_transform)
|
||||
|
||||
@@ -1019,19 +1020,17 @@ async def stream_agents(
|
||||
async with async_session_maker() as stream_session:
|
||||
if board_id is not None:
|
||||
agents = await _fetch_agent_events(
|
||||
stream_session, board_id, last_seen,
|
||||
stream_session,
|
||||
board_id,
|
||||
last_seen,
|
||||
)
|
||||
elif allowed_ids:
|
||||
agents = await _fetch_agent_events(stream_session, None, last_seen)
|
||||
agents = [
|
||||
agent for agent in agents if agent.board_id in allowed_ids
|
||||
]
|
||||
agents = [agent for agent in agents if agent.board_id in allowed_ids]
|
||||
else:
|
||||
agents = []
|
||||
main_session_keys = (
|
||||
await _get_gateway_main_session_keys(stream_session)
|
||||
if agents
|
||||
else set()
|
||||
await _get_gateway_main_session_keys(stream_session) if agents else set()
|
||||
)
|
||||
for agent in agents:
|
||||
updated_at = agent.updated_at or agent.last_seen_at or utcnow()
|
||||
@@ -1252,7 +1251,8 @@ async def delete_agent(
|
||||
await _require_agent_access(session, agent=agent, ctx=ctx, write=True)
|
||||
|
||||
board = await _require_board(
|
||||
session, str(agent.board_id) if agent.board_id else None,
|
||||
session,
|
||||
str(agent.board_id) if agent.board_id else None,
|
||||
)
|
||||
gateway, client_config = await _require_gateway(session, board)
|
||||
try:
|
||||
|
||||
@@ -24,12 +24,7 @@ from app.core.time import utcnow
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import async_session_maker, get_session
|
||||
from app.models.approvals import Approval
|
||||
from app.schemas.approvals import (
|
||||
ApprovalCreate,
|
||||
ApprovalRead,
|
||||
ApprovalStatus,
|
||||
ApprovalUpdate,
|
||||
)
|
||||
from app.schemas.approvals import ApprovalCreate, ApprovalRead, ApprovalStatus, ApprovalUpdate
|
||||
from app.schemas.pagination import DefaultLimitOffsetPage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -156,9 +151,7 @@ async def stream_approvals(
|
||||
).one(),
|
||||
)
|
||||
task_ids = {
|
||||
approval.task_id
|
||||
for approval in approvals
|
||||
if approval.task_id is not None
|
||||
approval.task_id for approval in approvals if approval.task_id is not None
|
||||
}
|
||||
counts_by_task_id: dict[UUID, tuple[int, int]] = {}
|
||||
if task_ids:
|
||||
|
||||
@@ -26,21 +26,14 @@ from app.core.time import utcnow
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import async_session_maker, get_session
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import (
|
||||
OpenClawGatewayError,
|
||||
ensure_session,
|
||||
send_message,
|
||||
)
|
||||
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
||||
from app.models.agents import Agent
|
||||
from app.models.board_group_memory import BoardGroupMemory
|
||||
from app.models.board_groups import BoardGroup
|
||||
from app.models.boards import Board
|
||||
from app.models.gateways import Gateway
|
||||
from app.models.users import User
|
||||
from app.schemas.board_group_memory import (
|
||||
BoardGroupMemoryCreate,
|
||||
BoardGroupMemoryRead,
|
||||
)
|
||||
from app.schemas.board_group_memory import BoardGroupMemoryCreate, BoardGroupMemoryRead
|
||||
from app.schemas.pagination import DefaultLimitOffsetPage
|
||||
from app.services.mentions import extract_mentions, matches_agent_mention
|
||||
from app.services.organizations import (
|
||||
|
||||
@@ -10,12 +10,7 @@ from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import func
|
||||
from sqlmodel import col, select
|
||||
|
||||
from app.api.deps import (
|
||||
ActorContext,
|
||||
require_admin_or_agent,
|
||||
require_org_admin,
|
||||
require_org_member,
|
||||
)
|
||||
from app.api.deps import ActorContext, require_admin_or_agent, require_org_admin, require_org_member
|
||||
from app.core.time import utcnow
|
||||
from app.db import crud
|
||||
from app.db.pagination import paginate
|
||||
@@ -34,10 +29,7 @@ from app.schemas.board_groups import BoardGroupCreate, BoardGroupRead, BoardGrou
|
||||
from app.schemas.common import OkResponse
|
||||
from app.schemas.pagination import DefaultLimitOffsetPage
|
||||
from app.schemas.view_models import BoardGroupSnapshot
|
||||
from app.services.agent_provisioning import (
|
||||
DEFAULT_HEARTBEAT_CONFIG,
|
||||
sync_gateway_agent_heartbeats,
|
||||
)
|
||||
from app.services.agent_provisioning import DEFAULT_HEARTBEAT_CONFIG, sync_gateway_agent_heartbeats
|
||||
from app.services.board_group_snapshot import build_group_snapshot
|
||||
from app.services.organizations import (
|
||||
OrganizationContext,
|
||||
@@ -86,8 +78,7 @@ async def _require_group_access(
|
||||
return group
|
||||
|
||||
board_ids = [
|
||||
board.id
|
||||
for board in await Board.objects.filter_by(board_group_id=group_id).all(session)
|
||||
board.id for board in await Board.objects.filter_by(board_group_id=group_id).all(session)
|
||||
]
|
||||
if not board_ids:
|
||||
if is_org_admin(member):
|
||||
@@ -144,7 +135,10 @@ async def get_board_group(
|
||||
) -> BoardGroup:
|
||||
"""Get a board group by id."""
|
||||
return await _require_group_access(
|
||||
session, group_id=group_id, member=ctx.member, write=False,
|
||||
session,
|
||||
group_id=group_id,
|
||||
member=ctx.member,
|
||||
write=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -159,7 +153,10 @@ async def get_board_group_snapshot(
|
||||
) -> BoardGroupSnapshot:
|
||||
"""Get a snapshot across boards in a group."""
|
||||
group = await _require_group_access(
|
||||
session, group_id=group_id, member=ctx.member, write=False,
|
||||
session,
|
||||
group_id=group_id,
|
||||
member=ctx.member,
|
||||
write=False,
|
||||
)
|
||||
if per_board_task_limit < 0:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
@@ -174,9 +171,7 @@ async def get_board_group_snapshot(
|
||||
allowed_ids = set(
|
||||
await list_accessible_board_ids(session, member=ctx.member, write=False),
|
||||
)
|
||||
snapshot.boards = [
|
||||
item for item in snapshot.boards if item.board.id in allowed_ids
|
||||
]
|
||||
snapshot.boards = [item for item in snapshot.boards if item.board.id in allowed_ids]
|
||||
return snapshot
|
||||
|
||||
|
||||
@@ -339,14 +334,13 @@ async def update_board_group(
|
||||
) -> BoardGroup:
|
||||
"""Update a board group."""
|
||||
group = await _require_group_access(
|
||||
session, group_id=group_id, member=ctx.member, write=True,
|
||||
session,
|
||||
group_id=group_id,
|
||||
member=ctx.member,
|
||||
write=True,
|
||||
)
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
if (
|
||||
"slug" in updates
|
||||
and updates["slug"] is not None
|
||||
and not updates["slug"].strip()
|
||||
):
|
||||
if "slug" in updates and updates["slug"] is not None and not updates["slug"].strip():
|
||||
updates["slug"] = _slugify(updates.get("name") or group.name)
|
||||
updates["updated_at"] = utcnow()
|
||||
return await crud.patch(session, group, updates)
|
||||
@@ -360,7 +354,10 @@ async def delete_board_group(
|
||||
) -> OkResponse:
|
||||
"""Delete a board group."""
|
||||
await _require_group_access(
|
||||
session, group_id=group_id, member=ctx.member, write=True,
|
||||
session,
|
||||
group_id=group_id,
|
||||
member=ctx.member,
|
||||
write=True,
|
||||
)
|
||||
|
||||
# Boards reference groups, so clear the FK first to keep deletes simple.
|
||||
@@ -378,7 +375,10 @@ async def delete_board_group(
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, BoardGroup, col(BoardGroup.id) == group_id, commit=False,
|
||||
session,
|
||||
BoardGroup,
|
||||
col(BoardGroup.id) == group_id,
|
||||
commit=False,
|
||||
)
|
||||
await session.commit()
|
||||
return OkResponse()
|
||||
|
||||
@@ -24,11 +24,7 @@ from app.core.time import utcnow
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import async_session_maker, get_session
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import (
|
||||
OpenClawGatewayError,
|
||||
ensure_session,
|
||||
send_message,
|
||||
)
|
||||
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
||||
from app.models.agents import Agent
|
||||
from app.models.board_memory import BoardMemory
|
||||
from app.models.gateways import Gateway
|
||||
|
||||
@@ -21,11 +21,7 @@ from app.core.config import settings
|
||||
from app.core.time import utcnow
|
||||
from app.db.session import get_session
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import (
|
||||
OpenClawGatewayError,
|
||||
ensure_session,
|
||||
send_message,
|
||||
)
|
||||
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
||||
from app.models.board_onboarding import BoardOnboardingSession
|
||||
from app.models.gateways import Gateway
|
||||
from app.schemas.board_onboarding import (
|
||||
@@ -39,11 +35,7 @@ from app.schemas.board_onboarding import (
|
||||
BoardOnboardingUserProfile,
|
||||
)
|
||||
from app.schemas.boards import BoardRead
|
||||
from app.services.board_leads import (
|
||||
LeadAgentOptions,
|
||||
LeadAgentRequest,
|
||||
ensure_board_lead_agent,
|
||||
)
|
||||
from app.services.board_leads import LeadAgentOptions, LeadAgentRequest, ensure_board_lead_agent
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
@@ -62,7 +54,8 @@ ADMIN_AUTH_DEP = Depends(require_admin_auth)
|
||||
|
||||
|
||||
async def _gateway_config(
|
||||
session: AsyncSession, board: Board,
|
||||
session: AsyncSession,
|
||||
board: Board,
|
||||
) -> tuple[Gateway, GatewayClientConfig]:
|
||||
if not board.gateway_id:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
@@ -255,11 +248,15 @@ async def start_onboarding(
|
||||
try:
|
||||
await ensure_session(session_key, config=config, label="Main Agent")
|
||||
await send_message(
|
||||
prompt, session_key=session_key, config=config, deliver=False,
|
||||
prompt,
|
||||
session_key=session_key,
|
||||
config=config,
|
||||
deliver=False,
|
||||
)
|
||||
except OpenClawGatewayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc),
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
onboarding = BoardOnboardingSession(
|
||||
@@ -311,7 +308,8 @@ async def answer_onboarding(
|
||||
)
|
||||
except OpenClawGatewayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc),
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
|
||||
onboarding.messages = messages
|
||||
|
||||
@@ -104,7 +104,9 @@ async def _require_gateway_for_create(
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
) -> Gateway:
|
||||
return await _require_gateway(
|
||||
session, payload.gateway_id, organization_id=ctx.organization.id,
|
||||
session,
|
||||
payload.gateway_id,
|
||||
organization_id=ctx.organization.id,
|
||||
)
|
||||
|
||||
|
||||
@@ -155,7 +157,9 @@ async def _apply_board_update(
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
if "gateway_id" in updates:
|
||||
await _require_gateway(
|
||||
session, updates["gateway_id"], organization_id=board.organization_id,
|
||||
session,
|
||||
updates["gateway_id"],
|
||||
organization_id=board.organization_id,
|
||||
)
|
||||
if "board_group_id" in updates and updates["board_group_id"] is not None:
|
||||
await _require_board_group(
|
||||
@@ -164,10 +168,7 @@ async def _apply_board_update(
|
||||
organization_id=board.organization_id,
|
||||
)
|
||||
crud.apply_updates(board, updates)
|
||||
if (
|
||||
updates.get("board_type") == "goal"
|
||||
and (not board.objective or not board.success_metrics)
|
||||
):
|
||||
if updates.get("board_type") == "goal" and (not board.objective or not board.success_metrics):
|
||||
# Validate only when explicitly switching to goal boards.
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
@@ -183,7 +184,8 @@ async def _apply_board_update(
|
||||
|
||||
|
||||
async def _board_gateway(
|
||||
session: AsyncSession, board: Board,
|
||||
session: AsyncSession,
|
||||
board: Board,
|
||||
) -> tuple[Gateway | None, GatewayClientConfig | None]:
|
||||
if not board.gateway_id:
|
||||
return None, None
|
||||
@@ -255,7 +257,8 @@ async def list_boards(
|
||||
if board_group_id is not None:
|
||||
statement = statement.where(col(Board.board_group_id) == board_group_id)
|
||||
statement = statement.order_by(
|
||||
func.lower(col(Board.name)).asc(), col(Board.created_at).desc(),
|
||||
func.lower(col(Board.name)).asc(),
|
||||
col(Board.created_at).desc(),
|
||||
)
|
||||
return await paginate(session, statement)
|
||||
|
||||
@@ -350,10 +353,14 @@ async def delete_board(
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, TaskDependency, col(TaskDependency.board_id) == board.id,
|
||||
session,
|
||||
TaskDependency,
|
||||
col(TaskDependency.board_id) == board.id,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, TaskFingerprint, col(TaskFingerprint.board_id) == board.id,
|
||||
session,
|
||||
TaskFingerprint,
|
||||
col(TaskFingerprint.board_id) == board.id,
|
||||
)
|
||||
|
||||
# Approvals can reference tasks and agents, so delete before both.
|
||||
|
||||
@@ -91,7 +91,8 @@ async def _resolve_gateway(
|
||||
board = await Board.objects.by_id(params.board_id).first(session)
|
||||
if board is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Board not found",
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Board not found",
|
||||
)
|
||||
if user is not None:
|
||||
await require_board_access(session, user=user, board=board, write=False)
|
||||
@@ -119,7 +120,10 @@ async def _resolve_gateway(
|
||||
|
||||
|
||||
async def _require_gateway(
|
||||
session: AsyncSession, board_id: str | None, *, user: User | None = None,
|
||||
session: AsyncSession,
|
||||
board_id: str | None,
|
||||
*,
|
||||
user: User | None = None,
|
||||
) -> tuple[Board, GatewayClientConfig, str | None]:
|
||||
params = GatewayResolveQuery(board_id=board_id)
|
||||
board, config, main_session = await _resolve_gateway(
|
||||
@@ -161,7 +165,9 @@ async def gateways_status(
|
||||
if main_session:
|
||||
try:
|
||||
ensured = await ensure_session(
|
||||
main_session, config=config, label="Main Agent",
|
||||
main_session,
|
||||
config=config,
|
||||
label="Main Agent",
|
||||
)
|
||||
if isinstance(ensured, dict):
|
||||
main_session_entry = ensured.get("entry") or ensured
|
||||
@@ -178,7 +184,9 @@ async def gateways_status(
|
||||
)
|
||||
except OpenClawGatewayError as exc:
|
||||
return GatewaysStatusResponse(
|
||||
connected=False, gateway_url=config.url, error=str(exc),
|
||||
connected=False,
|
||||
gateway_url=config.url,
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
|
||||
@@ -202,7 +210,8 @@ async def list_gateway_sessions(
|
||||
sessions = await openclaw_call("sessions.list", config=config)
|
||||
except OpenClawGatewayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc),
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
if isinstance(sessions, dict):
|
||||
sessions_list = _as_object_list(sessions.get("sessions"))
|
||||
@@ -213,7 +222,9 @@ async def list_gateway_sessions(
|
||||
if main_session:
|
||||
try:
|
||||
ensured = await ensure_session(
|
||||
main_session, config=config, label="Main Agent",
|
||||
main_session,
|
||||
config=config,
|
||||
label="Main Agent",
|
||||
)
|
||||
if isinstance(ensured, dict):
|
||||
main_session_entry = ensured.get("entry") or ensured
|
||||
@@ -233,11 +244,7 @@ async def _list_sessions(config: GatewayClientConfig) -> list[dict[str, object]]
|
||||
raw_items = _as_object_list(sessions.get("sessions"))
|
||||
else:
|
||||
raw_items = _as_object_list(sessions)
|
||||
return [
|
||||
item
|
||||
for item in raw_items
|
||||
if isinstance(item, dict)
|
||||
]
|
||||
return [item for item in raw_items if isinstance(item, dict)]
|
||||
|
||||
|
||||
async def _with_main_session(
|
||||
@@ -246,9 +253,7 @@ async def _with_main_session(
|
||||
config: GatewayClientConfig,
|
||||
main_session: str | None,
|
||||
) -> list[dict[str, object]]:
|
||||
if not main_session or any(
|
||||
item.get("key") == main_session for item in sessions_list
|
||||
):
|
||||
if not main_session or any(item.get("key") == main_session for item in sessions_list):
|
||||
return sessions_list
|
||||
try:
|
||||
await ensure_session(main_session, config=config, label="Main Agent")
|
||||
@@ -278,7 +283,8 @@ async def get_gateway_session(
|
||||
sessions_list = await _list_sessions(config)
|
||||
except OpenClawGatewayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc),
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
sessions_list = await _with_main_session(
|
||||
sessions_list,
|
||||
@@ -286,12 +292,15 @@ async def get_gateway_session(
|
||||
main_session=main_session,
|
||||
)
|
||||
session_entry = next(
|
||||
(item for item in sessions_list if item.get("key") == session_id), None,
|
||||
(item for item in sessions_list if item.get("key") == session_id),
|
||||
None,
|
||||
)
|
||||
if session_entry is None and main_session and session_id == main_session:
|
||||
try:
|
||||
ensured = await ensure_session(
|
||||
main_session, config=config, label="Main Agent",
|
||||
main_session,
|
||||
config=config,
|
||||
label="Main Agent",
|
||||
)
|
||||
if isinstance(ensured, dict):
|
||||
session_entry = ensured.get("entry") or ensured
|
||||
@@ -299,13 +308,15 @@ async def get_gateway_session(
|
||||
session_entry = None
|
||||
if session_entry is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Session not found",
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Session not found",
|
||||
)
|
||||
return GatewaySessionResponse(session=session_entry)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/sessions/{session_id}/history", response_model=GatewaySessionHistoryResponse,
|
||||
"/sessions/{session_id}/history",
|
||||
response_model=GatewaySessionHistoryResponse,
|
||||
)
|
||||
async def get_session_history(
|
||||
session_id: str,
|
||||
@@ -322,7 +333,8 @@ async def get_session_history(
|
||||
history = await get_chat_history(session_id, config=config)
|
||||
except OpenClawGatewayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc),
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
if isinstance(history, dict) and isinstance(history.get("messages"), list):
|
||||
return GatewaySessionHistoryResponse(history=history["messages"])
|
||||
@@ -339,7 +351,9 @@ async def send_gateway_session_message(
|
||||
) -> OkResponse:
|
||||
"""Send a message into a specific gateway session."""
|
||||
board, config, main_session = await _require_gateway(
|
||||
session, board_id, user=auth.user,
|
||||
session,
|
||||
board_id,
|
||||
user=auth.user,
|
||||
)
|
||||
if auth.user is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
@@ -350,7 +364,8 @@ async def send_gateway_session_message(
|
||||
await send_message(payload.content, session_key=session_id, config=config)
|
||||
except OpenClawGatewayError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc),
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail=str(exc),
|
||||
) from exc
|
||||
return OkResponse()
|
||||
|
||||
|
||||
@@ -17,11 +17,7 @@ from app.db import crud
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import get_session
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import (
|
||||
OpenClawGatewayError,
|
||||
ensure_session,
|
||||
send_message,
|
||||
)
|
||||
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
||||
from app.models.agents import Agent
|
||||
from app.models.gateways import Gateway
|
||||
from app.schemas.common import OkResponse
|
||||
@@ -38,12 +34,8 @@ from app.services.agent_provisioning import (
|
||||
ProvisionOptions,
|
||||
provision_main_agent,
|
||||
)
|
||||
from app.services.template_sync import (
|
||||
GatewayTemplateSyncOptions,
|
||||
)
|
||||
from app.services.template_sync import (
|
||||
sync_gateway_templates as sync_gateway_templates_service,
|
||||
)
|
||||
from app.services.template_sync import GatewayTemplateSyncOptions
|
||||
from app.services.template_sync import sync_gateway_templates as sync_gateway_templates_service
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi_pagination.limit_offset import LimitOffsetPage
|
||||
@@ -109,7 +101,8 @@ async def _require_gateway(
|
||||
)
|
||||
if gateway is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Gateway not found",
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Gateway not found",
|
||||
)
|
||||
return gateway
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ from typing import Literal
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import DateTime, case, func
|
||||
from sqlalchemy import DateTime, case
|
||||
from sqlalchemy import cast as sql_cast
|
||||
from sqlalchemy import func
|
||||
from sqlmodel import col, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
|
||||
@@ -80,7 +80,8 @@ ORG_ADMIN_DEP = Depends(require_org_admin)
|
||||
|
||||
|
||||
def _member_to_read(
|
||||
member: OrganizationMember, user: User | None,
|
||||
member: OrganizationMember,
|
||||
user: User | None,
|
||||
) -> OrganizationMemberRead:
|
||||
model = OrganizationMemberRead.model_validate(member, from_attributes=True)
|
||||
if user is not None:
|
||||
@@ -167,9 +168,7 @@ async def list_my_organizations(
|
||||
|
||||
await get_active_membership(session, auth.user)
|
||||
db_user = await User.objects.by_id(auth.user.id).first(session)
|
||||
active_id = (
|
||||
db_user.active_organization_id if db_user else auth.user.active_organization_id
|
||||
)
|
||||
active_id = db_user.active_organization_id if db_user else auth.user.active_organization_id
|
||||
|
||||
statement = (
|
||||
select(Organization, OrganizationMember)
|
||||
@@ -202,7 +201,9 @@ async def set_active_org(
|
||||
if auth.user is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
member = await set_active_organization(
|
||||
session, user=auth.user, organization_id=payload.organization_id,
|
||||
session,
|
||||
user=auth.user,
|
||||
organization_id=payload.organization_id,
|
||||
)
|
||||
organization = await Organization.objects.by_id(member.organization_id).first(
|
||||
session,
|
||||
@@ -245,7 +246,10 @@ async def delete_my_org(
|
||||
group_ids = select(BoardGroup.id).where(col(BoardGroup.organization_id) == org_id)
|
||||
|
||||
await crud.delete_where(
|
||||
session, ActivityEvent, col(ActivityEvent.task_id).in_(task_ids), commit=False,
|
||||
session,
|
||||
ActivityEvent,
|
||||
col(ActivityEvent.task_id).in_(task_ids),
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session,
|
||||
@@ -266,10 +270,16 @@ async def delete_my_org(
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, Approval, col(Approval.board_id).in_(board_ids), commit=False,
|
||||
session,
|
||||
Approval,
|
||||
col(Approval.board_id).in_(board_ids),
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, BoardMemory, col(BoardMemory.board_id).in_(board_ids), commit=False,
|
||||
session,
|
||||
BoardMemory,
|
||||
col(BoardMemory.board_id).in_(board_ids),
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session,
|
||||
@@ -302,13 +312,22 @@ async def delete_my_org(
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, Task, col(Task.board_id).in_(board_ids), commit=False,
|
||||
session,
|
||||
Task,
|
||||
col(Task.board_id).in_(board_ids),
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, Agent, col(Agent.board_id).in_(board_ids), commit=False,
|
||||
session,
|
||||
Agent,
|
||||
col(Agent.board_id).in_(board_ids),
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, Board, col(Board.organization_id) == org_id, commit=False,
|
||||
session,
|
||||
Board,
|
||||
col(Board.organization_id) == org_id,
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session,
|
||||
@@ -317,10 +336,16 @@ async def delete_my_org(
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, BoardGroup, col(BoardGroup.organization_id) == org_id, commit=False,
|
||||
session,
|
||||
BoardGroup,
|
||||
col(BoardGroup.organization_id) == org_id,
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, Gateway, col(Gateway.organization_id) == org_id, commit=False,
|
||||
session,
|
||||
Gateway,
|
||||
col(Gateway.organization_id) == org_id,
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session,
|
||||
@@ -342,7 +367,10 @@ async def delete_my_org(
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, Organization, col(Organization.id) == org_id, commit=False,
|
||||
session,
|
||||
Organization,
|
||||
col(Organization.id) == org_id,
|
||||
commit=False,
|
||||
)
|
||||
await session.commit()
|
||||
return OkResponse()
|
||||
@@ -360,14 +388,14 @@ async def get_my_membership(
|
||||
).all(session)
|
||||
model = _member_to_read(ctx.member, user)
|
||||
model.board_access = [
|
||||
OrganizationBoardAccessRead.model_validate(row, from_attributes=True)
|
||||
for row in access_rows
|
||||
OrganizationBoardAccessRead.model_validate(row, from_attributes=True) for row in access_rows
|
||||
]
|
||||
return model
|
||||
|
||||
|
||||
@router.get(
|
||||
"/me/members", response_model=DefaultLimitOffsetPage[OrganizationMemberRead],
|
||||
"/me/members",
|
||||
response_model=DefaultLimitOffsetPage[OrganizationMemberRead],
|
||||
)
|
||||
async def list_org_members(
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
@@ -410,8 +438,7 @@ async def get_org_member(
|
||||
).all(session)
|
||||
model = _member_to_read(member, user)
|
||||
model.board_access = [
|
||||
OrganizationBoardAccessRead.model_validate(row, from_attributes=True)
|
||||
for row in access_rows
|
||||
OrganizationBoardAccessRead.model_validate(row, from_attributes=True) for row in access_rows
|
||||
]
|
||||
return model
|
||||
|
||||
@@ -529,9 +556,7 @@ async def remove_org_member(
|
||||
user.active_organization_id = fallback_membership
|
||||
else:
|
||||
user.active_organization_id = (
|
||||
fallback_membership.organization_id
|
||||
if fallback_membership is not None
|
||||
else None
|
||||
fallback_membership.organization_id if fallback_membership is not None else None
|
||||
)
|
||||
session.add(user)
|
||||
|
||||
@@ -540,7 +565,8 @@ async def remove_org_member(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/me/invites", response_model=DefaultLimitOffsetPage[OrganizationInviteRead],
|
||||
"/me/invites",
|
||||
response_model=DefaultLimitOffsetPage[OrganizationInviteRead],
|
||||
)
|
||||
async def list_org_invites(
|
||||
session: AsyncSession = SESSION_DEP,
|
||||
@@ -607,7 +633,9 @@ async def create_org_invite(
|
||||
if valid_board_ids != board_ids:
|
||||
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
|
||||
await apply_invite_board_access(
|
||||
session, invite=invite, entries=payload.board_access,
|
||||
session,
|
||||
invite=invite,
|
||||
entries=payload.board_access,
|
||||
)
|
||||
await session.commit()
|
||||
await session.refresh(invite)
|
||||
|
||||
@@ -29,11 +29,7 @@ from app.db import crud
|
||||
from app.db.pagination import paginate
|
||||
from app.db.session import async_session_maker, get_session
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import (
|
||||
OpenClawGatewayError,
|
||||
ensure_session,
|
||||
send_message,
|
||||
)
|
||||
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
||||
from app.models.activity_events import ActivityEvent
|
||||
from app.models.agents import Agent
|
||||
from app.models.approvals import Approval
|
||||
@@ -45,13 +41,7 @@ from app.models.tasks import Task
|
||||
from app.schemas.common import OkResponse
|
||||
from app.schemas.errors import BlockedTaskError
|
||||
from app.schemas.pagination import DefaultLimitOffsetPage
|
||||
from app.schemas.tasks import (
|
||||
TaskCommentCreate,
|
||||
TaskCommentRead,
|
||||
TaskCreate,
|
||||
TaskRead,
|
||||
TaskUpdate,
|
||||
)
|
||||
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
|
||||
from app.services.activity_log import record_activity
|
||||
from app.services.mentions import extract_mentions, matches_agent_mention
|
||||
from app.services.organizations import require_board_access
|
||||
@@ -263,8 +253,7 @@ async def _reconcile_dependents_for_dependency_toggle(
|
||||
event_type="task.status_changed",
|
||||
task_id=dependent.id,
|
||||
message=(
|
||||
"Task returned to inbox: dependency reopened "
|
||||
f"({dependency_task.title})."
|
||||
"Task returned to inbox: dependency reopened " f"({dependency_task.title})."
|
||||
),
|
||||
agent_id=actor_agent_id,
|
||||
)
|
||||
@@ -313,7 +302,8 @@ def _serialize_comment(event: ActivityEvent) -> dict[str, object]:
|
||||
|
||||
|
||||
async def _gateway_config(
|
||||
session: AsyncSession, board: Board,
|
||||
session: AsyncSession,
|
||||
board: Board,
|
||||
) -> GatewayClientConfig | None:
|
||||
if not board.gateway_id:
|
||||
return None
|
||||
@@ -368,10 +358,7 @@ async def _notify_agent_on_task_assign(
|
||||
message = (
|
||||
"TASK ASSIGNED\n"
|
||||
+ "\n".join(details)
|
||||
+ (
|
||||
"\n\nTake action: open the task and begin work. "
|
||||
"Post updates as task comments."
|
||||
)
|
||||
+ ("\n\nTake action: open the task and begin work. " "Post updates as task comments.")
|
||||
)
|
||||
try:
|
||||
await _send_agent_task_message(
|
||||
@@ -607,9 +594,7 @@ async def _stream_dependency_state(
|
||||
rows: list[tuple[ActivityEvent, Task | None]],
|
||||
) -> tuple[dict[UUID, list[UUID]], dict[UUID, str]]:
|
||||
task_ids = [
|
||||
task.id
|
||||
for event, task in rows
|
||||
if task is not None and event.event_type != "task.comment"
|
||||
task.id for event, task in rows if task is not None and event.event_type != "task.comment"
|
||||
]
|
||||
if not task_ids:
|
||||
return {}, {}
|
||||
@@ -786,7 +771,8 @@ async def create_task(
|
||||
dependency_ids=normalized_deps,
|
||||
)
|
||||
blocked_by = blocked_by_dependency_ids(
|
||||
dependency_ids=normalized_deps, status_by_id=dep_status,
|
||||
dependency_ids=normalized_deps,
|
||||
status_by_id=dep_status,
|
||||
)
|
||||
if blocked_by and (task.assigned_agent_id is not None or task.status != "inbox"):
|
||||
raise _blocked_task_error(blocked_by)
|
||||
@@ -861,9 +847,7 @@ async def update_task(
|
||||
updates = payload.model_dump(exclude_unset=True)
|
||||
comment = payload.comment if "comment" in payload.model_fields_set else None
|
||||
depends_on_task_ids = (
|
||||
payload.depends_on_task_ids
|
||||
if "depends_on_task_ids" in payload.model_fields_set
|
||||
else None
|
||||
payload.depends_on_task_ids if "depends_on_task_ids" in payload.model_fields_set else None
|
||||
)
|
||||
updates.pop("comment", None)
|
||||
updates.pop("depends_on_task_ids", None)
|
||||
@@ -906,13 +890,22 @@ async def delete_task(
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
await require_board_access(session, user=auth.user, board=board, write=True)
|
||||
await crud.delete_where(
|
||||
session, ActivityEvent, col(ActivityEvent.task_id) == task.id, commit=False,
|
||||
session,
|
||||
ActivityEvent,
|
||||
col(ActivityEvent.task_id) == task.id,
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, TaskFingerprint, col(TaskFingerprint.task_id) == task.id, commit=False,
|
||||
session,
|
||||
TaskFingerprint,
|
||||
col(TaskFingerprint.task_id) == task.id,
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session, Approval, col(Approval.task_id) == task.id, commit=False,
|
||||
session,
|
||||
Approval,
|
||||
col(Approval.task_id) == task.id,
|
||||
commit=False,
|
||||
)
|
||||
await crud.delete_where(
|
||||
session,
|
||||
@@ -929,7 +922,8 @@ async def delete_task(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{task_id}/comments", response_model=DefaultLimitOffsetPage[TaskCommentRead],
|
||||
"/{task_id}/comments",
|
||||
response_model=DefaultLimitOffsetPage[TaskCommentRead],
|
||||
)
|
||||
async def list_task_comments(
|
||||
task: Task = TASK_DEP,
|
||||
@@ -1241,11 +1235,7 @@ async def _lead_apply_assignment(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Board leads cannot assign tasks to themselves.",
|
||||
)
|
||||
if (
|
||||
agent.board_id
|
||||
and update.task.board_id
|
||||
and agent.board_id != update.task.board_id
|
||||
):
|
||||
if agent.board_id and update.task.board_id and agent.board_id != update.task.board_id:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||
update.task.assigned_agent_id = agent.id
|
||||
|
||||
@@ -1256,19 +1246,13 @@ def _lead_apply_status(update: _TaskUpdateInput) -> None:
|
||||
if update.task.status != "review":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=(
|
||||
"Board leads can only change status when a task is "
|
||||
"in review."
|
||||
),
|
||||
detail=("Board leads can only change status when a task is " "in review."),
|
||||
)
|
||||
target_status = _required_status_value(update.updates["status"])
|
||||
if target_status not in {"done", "inbox"}:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=(
|
||||
"Board leads can only move review tasks to done "
|
||||
"or inbox."
|
||||
),
|
||||
detail=("Board leads can only move review tasks to done " "or inbox."),
|
||||
)
|
||||
if target_status == "inbox":
|
||||
update.task.assigned_agent_id = None
|
||||
@@ -1397,9 +1381,7 @@ async def _apply_non_lead_agent_task_rules(
|
||||
update.task.assigned_agent_id = None
|
||||
update.task.in_progress_at = None
|
||||
else:
|
||||
update.task.assigned_agent_id = (
|
||||
update.actor.agent.id if update.actor.agent else None
|
||||
)
|
||||
update.task.assigned_agent_id = update.actor.agent.id if update.actor.agent else None
|
||||
if status_value == "in_progress":
|
||||
update.task.in_progress_at = utcnow()
|
||||
|
||||
@@ -1462,11 +1444,7 @@ async def _apply_admin_task_rules(
|
||||
agent = await Agent.objects.by_id(assigned_agent_id).first(session)
|
||||
if agent is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
if (
|
||||
agent.board_id
|
||||
and update.task.board_id
|
||||
and agent.board_id != update.task.board_id
|
||||
):
|
||||
if agent.board_id and update.task.board_id and agent.board_id != update.task.board_id:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
|
||||
|
||||
|
||||
@@ -1481,9 +1459,11 @@ async def _record_task_comment_from_update(
|
||||
event_type="task.comment",
|
||||
message=update.comment,
|
||||
task_id=update.task.id,
|
||||
agent_id=update.actor.agent.id
|
||||
if update.actor.actor_type == "agent" and update.actor.agent
|
||||
else None,
|
||||
agent_id=(
|
||||
update.actor.agent.id
|
||||
if update.actor.actor_type == "agent" and update.actor.agent
|
||||
else None
|
||||
),
|
||||
)
|
||||
session.add(event)
|
||||
await session.commit()
|
||||
@@ -1496,9 +1476,7 @@ async def _record_task_update_activity(
|
||||
) -> None:
|
||||
event_type, message = _task_event_details(update.task, update.previous_status)
|
||||
actor_agent_id = (
|
||||
update.actor.agent.id
|
||||
if update.actor.actor_type == "agent" and update.actor.agent
|
||||
else None
|
||||
update.actor.agent.id if update.actor.actor_type == "agent" and update.actor.agent else None
|
||||
)
|
||||
record_activity(
|
||||
session,
|
||||
@@ -1525,10 +1503,7 @@ async def _notify_task_update_assignment_changes(
|
||||
if (
|
||||
update.task.status == "inbox"
|
||||
and update.task.assigned_agent_id is None
|
||||
and (
|
||||
update.previous_status != "inbox"
|
||||
or update.previous_assigned is not None
|
||||
)
|
||||
and (update.previous_status != "inbox" or update.previous_assigned is not None)
|
||||
):
|
||||
board = (
|
||||
await Board.objects.by_id(update.task.board_id).first(session)
|
||||
|
||||
@@ -77,10 +77,7 @@ async def _touch_agent_presence(
|
||||
real activity even if the heartbeat loop isn't running.
|
||||
"""
|
||||
now = utcnow()
|
||||
if (
|
||||
agent.last_seen_at is not None
|
||||
and now - agent.last_seen_at < _LAST_SEEN_TOUCH_INTERVAL
|
||||
):
|
||||
if agent.last_seen_at is not None and now - agent.last_seen_at < _LAST_SEEN_TOUCH_INTERVAL:
|
||||
return
|
||||
|
||||
agent.last_seen_at = now
|
||||
|
||||
@@ -19,9 +19,7 @@ _MULTIPLIERS: dict[str, int] = {
|
||||
_MAX_SCHEDULE_SECONDS = 60 * 60 * 24 * 365 * 10
|
||||
|
||||
_ERR_SCHEDULE_REQUIRED = "schedule is required"
|
||||
_ERR_SCHEDULE_INVALID = (
|
||||
'Invalid schedule. Expected format like "10m", "1h", "2d", "1w".'
|
||||
)
|
||||
_ERR_SCHEDULE_INVALID = 'Invalid schedule. Expected format like "10m", "1h", "2d", "1w".'
|
||||
_ERR_SCHEDULE_NONPOSITIVE = "Schedule must be greater than 0."
|
||||
_ERR_SCHEDULE_TOO_LARGE = "Schedule is too large (max 10 years)."
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from fastapi.responses import JSONResponse
|
||||
from starlette.exceptions import HTTPException as StarletteHTTPException
|
||||
from starlette.responses import Response
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -44,9 +44,7 @@ class RequestIdMiddleware:
|
||||
if message["type"] == "http.response.start":
|
||||
# Starlette uses `list[tuple[bytes, bytes]]` here.
|
||||
headers: list[tuple[bytes, bytes]] = message.setdefault("headers", [])
|
||||
if not any(
|
||||
key.lower() == self._header_name_bytes for key, _ in headers
|
||||
):
|
||||
if not any(key.lower() == self._header_name_bytes for key, _ in headers):
|
||||
request_id_bytes = request_id.encode("latin-1")
|
||||
headers.append((self._header_name_bytes, request_id_bytes))
|
||||
await send(message)
|
||||
|
||||
@@ -198,8 +198,7 @@ class AppLogger:
|
||||
formatter: logging.Formatter = JsonFormatter()
|
||||
else:
|
||||
formatter = KeyValueFormatter(
|
||||
"%(asctime)s %(levelname)s %(name)s %(message)s "
|
||||
"app=%(app)s version=%(version)s",
|
||||
"%(asctime)s %(levelname)s %(name)s %(message)s " "app=%(app)s version=%(version)s",
|
||||
)
|
||||
if settings.log_use_utc:
|
||||
formatter.converter = time.gmtime
|
||||
|
||||
@@ -161,10 +161,7 @@ async def list_by(
|
||||
|
||||
async def exists(session: AsyncSession, model: type[ModelT], **lookup: object) -> bool:
|
||||
"""Return whether any object exists for lookup values."""
|
||||
return (
|
||||
(await session.exec(_lookup_statement(model, lookup).limit(1))).first()
|
||||
is not None
|
||||
)
|
||||
return (await session.exec(_lookup_statement(model, lookup).limit(1))).first() is not None
|
||||
|
||||
|
||||
def _criteria_statement(
|
||||
@@ -219,11 +216,7 @@ async def update_where(
|
||||
commit = bool(options.pop("commit", False))
|
||||
exclude_none = bool(options.pop("exclude_none", False))
|
||||
allowed_fields_raw = options.pop("allowed_fields", None)
|
||||
allowed_fields = (
|
||||
allowed_fields_raw
|
||||
if isinstance(allowed_fields_raw, set)
|
||||
else None
|
||||
)
|
||||
allowed_fields = allowed_fields_raw if isinstance(allowed_fields_raw, set) else None
|
||||
source_updates: dict[str, Any] = {}
|
||||
if updates:
|
||||
source_updates.update(dict(updates))
|
||||
@@ -276,11 +269,7 @@ async def patch(
|
||||
"""Apply partial updates and persist object."""
|
||||
exclude_none = bool(options.pop("exclude_none", False))
|
||||
allowed_fields_raw = options.pop("allowed_fields", None)
|
||||
allowed_fields = (
|
||||
allowed_fields_raw
|
||||
if isinstance(allowed_fields_raw, set)
|
||||
else None
|
||||
)
|
||||
allowed_fields = allowed_fields_raw if isinstance(allowed_fields_raw, set) else None
|
||||
commit = bool(options.pop("commit", True))
|
||||
refresh = bool(options.pop("refresh", True))
|
||||
apply_updates(
|
||||
|
||||
@@ -36,12 +36,8 @@ class BoardOnboardingConfirm(SQLModel):
|
||||
@model_validator(mode="after")
|
||||
def validate_goal_fields(self) -> Self:
|
||||
"""Require goal metadata when the board type is `goal`."""
|
||||
if self.board_type == "goal" and (
|
||||
not self.objective or not self.success_metrics
|
||||
):
|
||||
message = (
|
||||
"Confirmed goal boards require objective and success_metrics"
|
||||
)
|
||||
if self.board_type == "goal" and (not self.objective or not self.success_metrics):
|
||||
message = "Confirmed goal boards require objective and success_metrics"
|
||||
raise ValueError(message)
|
||||
return self
|
||||
|
||||
|
||||
@@ -9,9 +9,7 @@ from uuid import UUID
|
||||
from pydantic import model_validator
|
||||
from sqlmodel import SQLModel
|
||||
|
||||
_ERR_GOAL_FIELDS_REQUIRED = (
|
||||
"Confirmed goal boards require objective and success_metrics"
|
||||
)
|
||||
_ERR_GOAL_FIELDS_REQUIRED = "Confirmed goal boards require objective and success_metrics"
|
||||
_ERR_GATEWAY_REQUIRED = "gateway_id is required"
|
||||
RUNTIME_ANNOTATION_TYPES = (datetime, UUID)
|
||||
|
||||
|
||||
@@ -15,11 +15,7 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoes
|
||||
|
||||
from app.core.config import settings
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import (
|
||||
OpenClawGatewayError,
|
||||
ensure_session,
|
||||
openclaw_call,
|
||||
)
|
||||
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, openclaw_call
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.models.agents import Agent
|
||||
@@ -414,7 +410,9 @@ async def _supported_gateway_files(config: GatewayClientConfig) -> set[str]:
|
||||
if not agent_id:
|
||||
return set(DEFAULT_GATEWAY_FILES)
|
||||
files_payload = await openclaw_call(
|
||||
"agents.files.list", {"agentId": agent_id}, config=config,
|
||||
"agents.files.list",
|
||||
{"agentId": agent_id},
|
||||
config=config,
|
||||
)
|
||||
if isinstance(files_payload, dict):
|
||||
files = files_payload.get("files") or []
|
||||
@@ -438,11 +436,14 @@ async def _reset_session(session_key: str, config: GatewayClientConfig) -> None:
|
||||
|
||||
|
||||
async def _gateway_agent_files_index(
|
||||
agent_id: str, config: GatewayClientConfig,
|
||||
agent_id: str,
|
||||
config: GatewayClientConfig,
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
try:
|
||||
payload = await openclaw_call(
|
||||
"agents.files.list", {"agentId": agent_id}, config=config,
|
||||
"agents.files.list",
|
||||
{"agentId": agent_id},
|
||||
config=config,
|
||||
)
|
||||
if isinstance(payload, dict):
|
||||
files = payload.get("files") or []
|
||||
@@ -486,18 +487,14 @@ def _render_agent_files(
|
||||
)
|
||||
heartbeat_path = _templates_root() / heartbeat_template
|
||||
if heartbeat_path.exists():
|
||||
rendered[name] = (
|
||||
env.get_template(heartbeat_template).render(**context).strip()
|
||||
)
|
||||
rendered[name] = env.get_template(heartbeat_template).render(**context).strip()
|
||||
continue
|
||||
override = overrides.get(name)
|
||||
if override:
|
||||
rendered[name] = env.from_string(override).render(**context).strip()
|
||||
continue
|
||||
template_name = (
|
||||
template_overrides[name]
|
||||
if template_overrides and name in template_overrides
|
||||
else name
|
||||
template_overrides[name] if template_overrides and name in template_overrides else name
|
||||
)
|
||||
path = _templates_root() / template_name
|
||||
if path.exists():
|
||||
@@ -596,8 +593,7 @@ def _heartbeat_entry_map(
|
||||
entries: list[tuple[str, str, dict[str, Any]]],
|
||||
) -> dict[str, tuple[str, dict[str, Any]]]:
|
||||
return {
|
||||
agent_id: (workspace_path, heartbeat)
|
||||
for agent_id, workspace_path, heartbeat in entries
|
||||
agent_id: (workspace_path, heartbeat) for agent_id, workspace_path, heartbeat in entries
|
||||
}
|
||||
|
||||
|
||||
@@ -694,9 +690,7 @@ async def _remove_gateway_agent_list(
|
||||
raise OpenClawGatewayError(msg)
|
||||
|
||||
new_list = [
|
||||
entry
|
||||
for entry in lst
|
||||
if not (isinstance(entry, dict) and entry.get("id") == agent_id)
|
||||
entry for entry in lst if not (isinstance(entry, dict) and entry.get("id") == agent_id)
|
||||
]
|
||||
if len(new_list) == len(lst):
|
||||
return
|
||||
@@ -841,7 +835,9 @@ async def provision_main_agent(
|
||||
raise ValueError(msg)
|
||||
client_config = GatewayClientConfig(url=gateway.url, token=gateway.token)
|
||||
await ensure_session(
|
||||
gateway.main_session_key, config=client_config, label="Main Agent",
|
||||
gateway.main_session_key,
|
||||
config=client_config,
|
||||
label="Main Agent",
|
||||
)
|
||||
|
||||
agent_id = await _gateway_default_agent_id(
|
||||
|
||||
@@ -38,10 +38,7 @@ def _status_weight_expr() -> ColumnElement[int]:
|
||||
|
||||
def _priority_weight_expr() -> ColumnElement[int]:
|
||||
"""Return a SQL expression that sorts task priorities by configured order."""
|
||||
whens = [
|
||||
(col(Task.priority) == key, weight)
|
||||
for key, weight in _PRIORITY_ORDER.items()
|
||||
]
|
||||
whens = [(col(Task.priority) == key, weight) for key, weight in _PRIORITY_ORDER.items()]
|
||||
return case(*whens, else_=99)
|
||||
|
||||
|
||||
@@ -106,11 +103,7 @@ async def _agent_names(
|
||||
tasks: list[Task],
|
||||
) -> dict[UUID, str]:
|
||||
"""Return agent names keyed by assigned agent ids in the provided tasks."""
|
||||
assigned_ids = {
|
||||
task.assigned_agent_id
|
||||
for task in tasks
|
||||
if task.assigned_agent_id is not None
|
||||
}
|
||||
assigned_ids = {task.assigned_agent_id for task in tasks if task.assigned_agent_id is not None}
|
||||
if not assigned_ids:
|
||||
return {}
|
||||
return dict(
|
||||
|
||||
@@ -10,11 +10,7 @@ from sqlmodel import col, select
|
||||
from app.core.agent_tokens import generate_agent_token, hash_agent_token
|
||||
from app.core.time import utcnow
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import (
|
||||
OpenClawGatewayError,
|
||||
ensure_session,
|
||||
send_message,
|
||||
)
|
||||
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
||||
from app.models.agents import Agent
|
||||
from app.services.agent_provisioning import (
|
||||
DEFAULT_HEARTBEAT_CONFIG,
|
||||
|
||||
@@ -55,8 +55,7 @@ def _agent_to_read(agent: Agent, main_session_keys: set[str]) -> AgentRead:
|
||||
model = AgentRead.model_validate(agent, from_attributes=True)
|
||||
computed_status = _computed_agent_status(agent)
|
||||
is_gateway_main = bool(
|
||||
agent.openclaw_session_id
|
||||
and agent.openclaw_session_id in main_session_keys,
|
||||
agent.openclaw_session_id and agent.openclaw_session_id in main_session_keys,
|
||||
)
|
||||
return model.model_copy(
|
||||
update={
|
||||
@@ -84,11 +83,7 @@ def _task_to_card(
|
||||
) -> TaskCardRead:
|
||||
card = TaskCardRead.model_validate(task, from_attributes=True)
|
||||
approvals_count, approvals_pending_count = counts_by_task_id.get(task.id, (0, 0))
|
||||
assignee = (
|
||||
agent_name_by_id.get(task.assigned_agent_id)
|
||||
if task.assigned_agent_id
|
||||
else None
|
||||
)
|
||||
assignee = agent_name_by_id.get(task.assigned_agent_id) if task.assigned_agent_id else None
|
||||
depends_on_task_ids = deps_by_task_id.get(task.id, [])
|
||||
blocked_by_task_ids = blocked_by_dependency_ids(
|
||||
dependency_ids=depends_on_task_ids,
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from app.models.agents import Agent
|
||||
|
||||
# Mention tokens are single, space-free words (e.g. "@alex", "@lead").
|
||||
|
||||
@@ -79,7 +79,8 @@ async def get_member(
|
||||
|
||||
|
||||
async def get_first_membership(
|
||||
session: AsyncSession, user_id: UUID,
|
||||
session: AsyncSession,
|
||||
user_id: UUID,
|
||||
) -> OrganizationMember | None:
|
||||
"""Return the oldest membership for a user, if any."""
|
||||
return (
|
||||
@@ -99,7 +100,8 @@ async def set_active_organization(
|
||||
member = await get_member(session, user_id=user.id, organization_id=organization_id)
|
||||
if member is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="No org access",
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="No org access",
|
||||
)
|
||||
if user.active_organization_id != organization_id:
|
||||
user.active_organization_id = organization_id
|
||||
@@ -177,8 +179,7 @@ async def accept_invite(
|
||||
access_rows = list(
|
||||
await session.exec(
|
||||
select(OrganizationInviteBoardAccess).where(
|
||||
col(OrganizationInviteBoardAccess.organization_invite_id)
|
||||
== invite.id,
|
||||
col(OrganizationInviteBoardAccess.organization_invite_id) == invite.id,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -207,7 +208,8 @@ async def accept_invite(
|
||||
|
||||
|
||||
async def ensure_member_for_user(
|
||||
session: AsyncSession, user: User,
|
||||
session: AsyncSession,
|
||||
user: User,
|
||||
) -> OrganizationMember:
|
||||
"""Ensure a user has some membership, creating one if necessary."""
|
||||
existing = await get_active_membership(session, user)
|
||||
@@ -291,21 +293,27 @@ async def require_board_access(
|
||||
) -> OrganizationMember:
|
||||
"""Require board access for a user and return matching membership."""
|
||||
member = await get_member(
|
||||
session, user_id=user.id, organization_id=board.organization_id,
|
||||
session,
|
||||
user_id=user.id,
|
||||
organization_id=board.organization_id,
|
||||
)
|
||||
if member is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="No org access",
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="No org access",
|
||||
)
|
||||
if not await has_board_access(session, member=member, board=board, write=write):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="Board access denied",
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Board access denied",
|
||||
)
|
||||
return member
|
||||
|
||||
|
||||
def board_access_filter(
|
||||
member: OrganizationMember, *, write: bool,
|
||||
member: OrganizationMember,
|
||||
*,
|
||||
write: bool,
|
||||
) -> ColumnElement[bool]:
|
||||
"""Build a SQL filter expression for boards visible to a member."""
|
||||
if write and member_all_boards_write(member):
|
||||
|
||||
@@ -42,10 +42,7 @@ class SoulRef:
|
||||
def _parse_sitemap_soul_refs(sitemap_xml: str) -> list[SoulRef]:
|
||||
"""Parse sitemap XML and extract valid souls.directory handle/slug refs."""
|
||||
# Extract <loc> values without XML entity expansion.
|
||||
urls = [
|
||||
unescape(match.group(1)).strip()
|
||||
for match in _LOC_PATTERN.finditer(sitemap_xml)
|
||||
]
|
||||
urls = [unescape(match.group(1)).strip() for match in _LOC_PATTERN.finditer(sitemap_xml)]
|
||||
|
||||
refs: list[SoulRef] = []
|
||||
for url in urls:
|
||||
@@ -110,10 +107,7 @@ async def fetch_soul_markdown(
|
||||
normalized_slug = slug.strip().strip("/")
|
||||
if normalized_slug.endswith(".md"):
|
||||
normalized_slug = normalized_slug[: -len(".md")]
|
||||
url = (
|
||||
f"{SOULS_DIRECTORY_BASE_URL}/api/souls/"
|
||||
f"{normalized_handle}/{normalized_slug}.md"
|
||||
)
|
||||
url = f"{SOULS_DIRECTORY_BASE_URL}/api/souls/" f"{normalized_handle}/{normalized_slug}.md"
|
||||
|
||||
owns_client = client is None
|
||||
if client is None:
|
||||
|
||||
@@ -79,11 +79,7 @@ def blocked_by_dependency_ids(
|
||||
status_by_id: Mapping[UUID, str],
|
||||
) -> list[UUID]:
|
||||
"""Return dependency ids that are not yet in the done status."""
|
||||
return [
|
||||
dep_id
|
||||
for dep_id in dependency_ids
|
||||
if status_by_id.get(dep_id) != DONE_STATUS
|
||||
]
|
||||
return [dep_id for dep_id in dependency_ids if status_by_id.get(dep_id) != DONE_STATUS]
|
||||
|
||||
|
||||
async def blocked_by_for_task(
|
||||
|
||||
@@ -14,11 +14,7 @@ from sqlalchemy import func
|
||||
from sqlmodel import col, select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
from app.core.agent_tokens import (
|
||||
generate_agent_token,
|
||||
hash_agent_token,
|
||||
verify_agent_token,
|
||||
)
|
||||
from app.core.agent_tokens import generate_agent_token, hash_agent_token, verify_agent_token
|
||||
from app.core.time import utcnow
|
||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
||||
from app.integrations.openclaw_gateway import OpenClawGatewayError, openclaw_call
|
||||
@@ -108,10 +104,7 @@ def _is_transient_gateway_error(exc: Exception) -> bool:
|
||||
|
||||
|
||||
def _gateway_timeout_message(exc: OpenClawGatewayError) -> str:
|
||||
return (
|
||||
"Gateway unreachable after 10 minutes (template sync timeout). "
|
||||
f"Last error: {exc}"
|
||||
)
|
||||
return "Gateway unreachable after 10 minutes (template sync timeout). " f"Last error: {exc}"
|
||||
|
||||
|
||||
class _GatewayBackoff:
|
||||
@@ -375,6 +368,7 @@ async def _rotate_agent_token(session: AsyncSession, agent: Agent) -> str:
|
||||
|
||||
async def _ping_gateway(ctx: _SyncContext, result: GatewayTemplatesSyncResult) -> bool:
|
||||
try:
|
||||
|
||||
async def _do_ping() -> object:
|
||||
return await openclaw_call("agents.list", config=ctx.config)
|
||||
|
||||
@@ -486,6 +480,7 @@ async def _sync_one_agent(
|
||||
if not auth_token:
|
||||
return False
|
||||
try:
|
||||
|
||||
async def _do_provision() -> None:
|
||||
await provision_agent(
|
||||
agent,
|
||||
@@ -533,10 +528,7 @@ async def _sync_main_agent(
|
||||
if main_agent is None:
|
||||
_append_sync_error(
|
||||
result,
|
||||
message=(
|
||||
"Gateway main agent record not found; "
|
||||
"skipping main agent template sync."
|
||||
),
|
||||
message=("Gateway main agent record not found; " "skipping main agent template sync."),
|
||||
)
|
||||
return True
|
||||
try:
|
||||
@@ -574,6 +566,7 @@ async def _sync_main_agent(
|
||||
return True
|
||||
stop_sync = False
|
||||
try:
|
||||
|
||||
async def _do_provision_main() -> None:
|
||||
await provision_main_agent(
|
||||
main_agent,
|
||||
|
||||
@@ -32,17 +32,13 @@ def _parse_args() -> argparse.Namespace:
|
||||
parser.add_argument(
|
||||
"--reset-sessions",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Reset agent sessions after syncing files "
|
||||
"(forces agents to re-read workspace)"
|
||||
),
|
||||
help=("Reset agent sessions after syncing files " "(forces agents to re-read workspace)"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rotate-tokens",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Rotate agent tokens when TOOLS.md is missing/unreadable "
|
||||
"or token drift is detected"
|
||||
"Rotate agent tokens when TOOLS.md is missing/unreadable " "or token drift is detected"
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -56,10 +52,7 @@ def _parse_args() -> argparse.Namespace:
|
||||
async def _run() -> int:
|
||||
from app.db.session import async_session_maker
|
||||
from app.models.gateways import Gateway
|
||||
from app.services.template_sync import (
|
||||
GatewayTemplateSyncOptions,
|
||||
sync_gateway_templates,
|
||||
)
|
||||
from app.services.template_sync import GatewayTemplateSyncOptions, sync_gateway_templates
|
||||
|
||||
args = _parse_args()
|
||||
gateway_id = UUID(args.gateway_id)
|
||||
@@ -86,8 +79,7 @@ async def _run() -> int:
|
||||
|
||||
sys.stdout.write(f"gateway_id={result.gateway_id}\n")
|
||||
sys.stdout.write(
|
||||
f"include_main={result.include_main} "
|
||||
f"reset_sessions={result.reset_sessions}\n",
|
||||
f"include_main={result.include_main} " f"reset_sessions={result.reset_sessions}\n",
|
||||
)
|
||||
sys.stdout.write(
|
||||
f"agents_updated={result.agents_updated} "
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI, HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel, Field
|
||||
@@ -11,6 +12,9 @@ from app.core.error_handling import (
|
||||
REQUEST_ID_HEADER,
|
||||
_error_payload,
|
||||
_get_request_id,
|
||||
_http_exception_exception_handler,
|
||||
_request_validation_exception_handler,
|
||||
_response_validation_exception_handler,
|
||||
install_error_handling,
|
||||
)
|
||||
|
||||
@@ -123,3 +127,24 @@ def test_get_request_id_returns_none_for_missing_or_invalid_state() -> None:
|
||||
|
||||
def test_error_payload_omits_request_id_when_none() -> None:
|
||||
assert _error_payload(detail="x", request_id=None) == {"detail": "x"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_request_validation_exception_wrapper_rejects_wrong_exception() -> None:
|
||||
req = Request({"type": "http", "headers": [], "state": {}})
|
||||
with pytest.raises(TypeError, match="Expected RequestValidationError"):
|
||||
await _request_validation_exception_handler(req, Exception("x"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_response_validation_exception_wrapper_rejects_wrong_exception() -> None:
|
||||
req = Request({"type": "http", "headers": [], "state": {}})
|
||||
with pytest.raises(TypeError, match="Expected ResponseValidationError"):
|
||||
await _response_validation_exception_handler(req, Exception("x"))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_http_exception_wrapper_rejects_wrong_exception() -> None:
|
||||
req = Request({"type": "http", "headers": [], "state": {}})
|
||||
with pytest.raises(TypeError, match="Expected StarletteHTTPException"):
|
||||
await _http_exception_exception_handler(req, Exception("x"))
|
||||
|
||||
@@ -16,10 +16,7 @@ from app.models.organization_invites import OrganizationInvite
|
||||
from app.models.organization_members import OrganizationMember
|
||||
from app.models.organizations import Organization
|
||||
from app.models.users import User
|
||||
from app.schemas.organizations import (
|
||||
OrganizationBoardAccessSpec,
|
||||
OrganizationMemberAccessUpdate,
|
||||
)
|
||||
from app.schemas.organizations import OrganizationBoardAccessSpec, OrganizationMemberAccessUpdate
|
||||
from app.services import organizations
|
||||
|
||||
|
||||
@@ -87,10 +84,7 @@ class _FakeSession:
|
||||
|
||||
|
||||
def test_normalize_invited_email_strips_and_lowercases() -> None:
|
||||
assert (
|
||||
organizations.normalize_invited_email(" Foo@Example.com ")
|
||||
== "foo@example.com"
|
||||
)
|
||||
assert organizations.normalize_invited_email(" Foo@Example.com ") == "foo@example.com"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -128,7 +122,9 @@ async def test_ensure_member_for_user_returns_existing_membership(
|
||||
) -> None:
|
||||
user = User(clerk_user_id="u1")
|
||||
existing = OrganizationMember(
|
||||
organization_id=uuid4(), user_id=user.id, role="member",
|
||||
organization_id=uuid4(),
|
||||
user_id=user.id,
|
||||
role="member",
|
||||
)
|
||||
|
||||
async def _fake_get_active(_session: Any, _user: User) -> OrganizationMember:
|
||||
@@ -161,11 +157,15 @@ async def test_ensure_member_for_user_accepts_pending_invite(
|
||||
return invite
|
||||
|
||||
accepted = OrganizationMember(
|
||||
organization_id=org_id, user_id=user.id, role="member",
|
||||
organization_id=org_id,
|
||||
user_id=user.id,
|
||||
role="member",
|
||||
)
|
||||
|
||||
async def _fake_accept(
|
||||
_session: Any, _invite: OrganizationInvite, _user: User,
|
||||
_session: Any,
|
||||
_invite: OrganizationInvite,
|
||||
_user: User,
|
||||
) -> OrganizationMember:
|
||||
assert _invite is invite
|
||||
assert _user is user
|
||||
@@ -216,7 +216,10 @@ async def test_has_board_access_denies_cross_org() -> None:
|
||||
board = Board(id=uuid4(), organization_id=uuid4(), name="b", slug="b")
|
||||
assert (
|
||||
await organizations.has_board_access(
|
||||
session, member=member, board=board, write=False,
|
||||
session,
|
||||
member=member,
|
||||
board=board,
|
||||
write=False,
|
||||
)
|
||||
is False
|
||||
)
|
||||
@@ -226,7 +229,10 @@ async def test_has_board_access_denies_cross_org() -> None:
|
||||
async def test_has_board_access_uses_org_board_access_row_read_and_write() -> None:
|
||||
org_id = uuid4()
|
||||
member = OrganizationMember(
|
||||
id=uuid4(), organization_id=org_id, user_id=uuid4(), role="member",
|
||||
id=uuid4(),
|
||||
organization_id=org_id,
|
||||
user_id=uuid4(),
|
||||
role="member",
|
||||
)
|
||||
board = Board(id=uuid4(), organization_id=org_id, name="b", slug="b")
|
||||
|
||||
@@ -239,7 +245,10 @@ async def test_has_board_access_uses_org_board_access_row_read_and_write() -> No
|
||||
session = _FakeSession(exec_results=[_FakeExecResult(first_value=access)])
|
||||
assert (
|
||||
await organizations.has_board_access(
|
||||
session, member=member, board=board, write=False,
|
||||
session,
|
||||
member=member,
|
||||
board=board,
|
||||
write=False,
|
||||
)
|
||||
is True
|
||||
)
|
||||
@@ -253,7 +262,10 @@ async def test_has_board_access_uses_org_board_access_row_read_and_write() -> No
|
||||
session2 = _FakeSession(exec_results=[_FakeExecResult(first_value=access2)])
|
||||
assert (
|
||||
await organizations.has_board_access(
|
||||
session2, member=member, board=board, write=False,
|
||||
session2,
|
||||
member=member,
|
||||
board=board,
|
||||
write=False,
|
||||
)
|
||||
is True
|
||||
)
|
||||
@@ -267,7 +279,10 @@ async def test_has_board_access_uses_org_board_access_row_read_and_write() -> No
|
||||
session3 = _FakeSession(exec_results=[_FakeExecResult(first_value=access3)])
|
||||
assert (
|
||||
await organizations.has_board_access(
|
||||
session3, member=member, board=board, write=True,
|
||||
session3,
|
||||
member=member,
|
||||
board=board,
|
||||
write=True,
|
||||
)
|
||||
is False
|
||||
)
|
||||
@@ -288,7 +303,10 @@ async def test_require_board_access_raises_when_no_member(
|
||||
session = _FakeSession(exec_results=[])
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
await organizations.require_board_access(
|
||||
session, user=user, board=board, write=False,
|
||||
session,
|
||||
user=user,
|
||||
board=board,
|
||||
write=False,
|
||||
)
|
||||
assert exc.value.status_code == 403
|
||||
|
||||
@@ -298,24 +316,33 @@ async def test_apply_member_access_update_deletes_existing_and_adds_rows_when_no
|
||||
None
|
||||
):
|
||||
member = OrganizationMember(
|
||||
id=uuid4(), organization_id=uuid4(), user_id=uuid4(), role="member",
|
||||
id=uuid4(),
|
||||
organization_id=uuid4(),
|
||||
user_id=uuid4(),
|
||||
role="member",
|
||||
)
|
||||
update = OrganizationMemberAccessUpdate(
|
||||
all_boards_read=False,
|
||||
all_boards_write=False,
|
||||
board_access=[
|
||||
OrganizationBoardAccessSpec(
|
||||
board_id=uuid4(), can_read=True, can_write=False,
|
||||
board_id=uuid4(),
|
||||
can_read=True,
|
||||
can_write=False,
|
||||
),
|
||||
OrganizationBoardAccessSpec(
|
||||
board_id=uuid4(), can_read=True, can_write=True,
|
||||
board_id=uuid4(),
|
||||
can_read=True,
|
||||
can_write=True,
|
||||
),
|
||||
],
|
||||
)
|
||||
session = _FakeSession(exec_results=[])
|
||||
|
||||
await organizations.apply_member_access_update(
|
||||
session, member=member, update=update,
|
||||
session,
|
||||
member=member,
|
||||
update=update,
|
||||
)
|
||||
|
||||
# delete statement executed once
|
||||
|
||||
Reference in New Issue
Block a user