diff --git a/backend/app/api/agent.py b/backend/app/api/agent.py index 87e4eb16..929863a8 100644 --- a/backend/app/api/agent.py +++ b/backend/app/api/agent.py @@ -46,6 +46,7 @@ from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, Ta from app.services.activity_log import record_activity from app.services.openclaw.agent_service import AgentLifecycleService from app.services.openclaw.coordination_service import GatewayCoordinationService +from app.services.openclaw.policies import OpenClawAuthorizationPolicy from app.services.task_dependencies import ( blocked_by_dependency_ids, dependency_status_by_id, @@ -120,8 +121,22 @@ def _actor(agent_ctx: AgentAuthContext) -> ActorContext: def _guard_board_access(agent_ctx: AgentAuthContext, board: Board) -> None: - if agent_ctx.agent.board_id and agent_ctx.agent.board_id != board.id: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + allowed = not (agent_ctx.agent.board_id and agent_ctx.agent.board_id != board.id) + OpenClawAuthorizationPolicy.require_board_write_access(allowed=allowed) + + +def _require_board_lead(agent_ctx: AgentAuthContext) -> Agent: + return OpenClawAuthorizationPolicy.require_board_lead_actor( + actor_agent=agent_ctx.agent, + detail="Only board leads can perform this action", + ) + + +def _guard_task_access(agent_ctx: AgentAuthContext, task: Task) -> None: + allowed = not ( + agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id + ) + OpenClawAuthorizationPolicy.require_board_write_access(allowed=allowed) @router.get("/boards", response_model=DefaultLimitOffsetPage[BoardRead]) @@ -156,8 +171,10 @@ async def list_agents( """List agents, optionally filtered to a board.""" statement = select(Agent) if agent_ctx.agent.board_id: - if board_id and board_id != agent_ctx.agent.board_id: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + if board_id: + OpenClawAuthorizationPolicy.require_board_write_access( + allowed=board_id == agent_ctx.agent.board_id, + ) statement = statement.where(Agent.board_id == agent_ctx.agent.board_id) elif board_id: statement = statement.where(Agent.board_id == board_id) @@ -203,8 +220,7 @@ async def create_task( ) -> TaskRead: """Create a task on the board as the lead agent.""" _guard_board_access(agent_ctx, board) - if not agent_ctx.agent.is_board_lead: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + _require_board_lead(agent_ctx) data = payload.model_dump(exclude={"depends_on_task_ids"}) depends_on_task_ids = list(payload.depends_on_task_ids) @@ -297,8 +313,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: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + _guard_task_access(agent_ctx, task) return await tasks_api.update_task( payload=payload, task=task, @@ -317,8 +332,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: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + _guard_task_access(agent_ctx, task) return await tasks_api.list_task_comments( task=task, session=session, @@ -336,8 +350,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: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + _guard_task_access(agent_ctx, task) return await tasks_api.create_task_comment( payload=payload, task=task, @@ -444,12 +457,9 @@ async def create_agent( agent_ctx: AgentAuthContext = AGENT_CTX_DEP, ) -> AgentRead: """Create an agent on the caller's board.""" - if not agent_ctx.agent.is_board_lead: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) - if not agent_ctx.agent.board_id: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + lead = _require_board_lead(agent_ctx) payload = AgentCreate( - **{**payload.model_dump(), "board_id": agent_ctx.agent.board_id}, + **{**payload.model_dump(), "board_id": lead.board_id}, ) return await agents_api.create_agent( payload=payload, @@ -468,8 +478,7 @@ async def nudge_agent( ) -> OkResponse: """Send a direct nudge message to a board agent.""" _guard_board_access(agent_ctx, board) - if not agent_ctx.agent.is_board_lead: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + _require_board_lead(agent_ctx) coordination = GatewayCoordinationService(session) await coordination.nudge_board_agent( board=board, @@ -506,8 +515,10 @@ async def get_agent_soul( ) -> str: """Fetch the target agent's SOUL.md content from the gateway.""" _guard_board_access(agent_ctx, board) - if not agent_ctx.agent.is_board_lead and str(agent_ctx.agent.id) != agent_id: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + OpenClawAuthorizationPolicy.require_board_lead_or_same_actor( + actor_agent=agent_ctx.agent, + target_agent_id=agent_id, + ) coordination = GatewayCoordinationService(session) return await coordination.get_agent_soul( board=board, @@ -526,8 +537,7 @@ async def update_agent_soul( ) -> OkResponse: """Update an agent's SOUL.md content in DB and gateway.""" _guard_board_access(agent_ctx, board) - if not agent_ctx.agent.is_board_lead: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + _require_board_lead(agent_ctx) coordination = GatewayCoordinationService(session) await coordination.update_agent_soul( board=board, @@ -553,8 +563,7 @@ async def ask_user_via_gateway_main( ) -> GatewayMainAskUserResponse: """Route a lead's ask-user request through the dedicated gateway agent.""" _guard_board_access(agent_ctx, board) - if not agent_ctx.agent.is_board_lead: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + _require_board_lead(agent_ctx) coordination = GatewayCoordinationService(session) return await coordination.ask_user_via_gateway_main( board=board, diff --git a/backend/app/api/board_onboarding.py b/backend/app/api/board_onboarding.py index d1fa632c..201d51fc 100644 --- a/backend/app/api/board_onboarding.py +++ b/backend/app/api/board_onboarding.py @@ -34,6 +34,7 @@ from app.schemas.board_onboarding import ( ) from app.schemas.boards import BoardRead from app.services.openclaw.onboarding_service import BoardOnboardingMessagingService +from app.services.openclaw.policies import OpenClawAuthorizationPolicy from app.services.openclaw.provisioning import ( LeadAgentOptions, LeadAgentRequest, @@ -307,13 +308,15 @@ async def agent_onboarding_update( if actor.actor_type != "agent" or actor.agent is None: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) agent = actor.agent - if agent.board_id is not None: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + OpenClawAuthorizationPolicy.require_gateway_scoped_actor(actor_agent=agent) if board.gateway_id: gateway = await Gateway.objects.by_id(board.gateway_id).first(session) - if gateway and (agent.gateway_id != gateway.id or agent.board_id is not None): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + if gateway: + OpenClawAuthorizationPolicy.require_gateway_main_actor_binding( + actor_agent=agent, + gateway=gateway, + ) onboarding = ( await BoardOnboardingSession.objects.filter_by(board_id=board.id) diff --git a/backend/app/services/openclaw/agent_service.py b/backend/app/services/openclaw/agent_service.py index b5521690..2b7510f0 100644 --- a/backend/app/services/openclaw/agent_service.py +++ b/backend/app/services/openclaw/agent_service.py @@ -44,6 +44,7 @@ from app.services.openclaw.constants import ( DEFAULT_HEARTBEAT_CONFIG, OFFLINE_AFTER, ) +from app.services.openclaw.policies import OpenClawAuthorizationPolicy from app.services.openclaw.provisioning import ( AgentProvisionRequest, MainAgentProvisionRequest, @@ -500,18 +501,26 @@ class AgentLifecycleService: write: bool, ) -> None: if agent.board_id is None: - if not is_org_admin(ctx.member): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + OpenClawAuthorizationPolicy.require_org_admin(is_admin=is_org_admin(ctx.member)) gateway = await self.get_main_agent_gateway(agent) - if gateway is None or gateway.organization_id != ctx.organization.id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + OpenClawAuthorizationPolicy.require_gateway_in_org( + gateway=gateway, + organization_id=ctx.organization.id, + ) return board = await Board.objects.by_id(agent.board_id).first(self.session) - if board is None or board.organization_id != ctx.organization.id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - if not await has_board_access(self.session, member=ctx.member, board=board, write=write): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + board = OpenClawAuthorizationPolicy.require_board_in_org( + board=board, + organization_id=ctx.organization.id, + ) + allowed = await has_board_access( + self.session, + member=ctx.member, + board=board, + write=write, + ) + OpenClawAuthorizationPolicy.require_board_write_access(allowed=allowed) @staticmethod def record_heartbeat(session: AsyncSession, agent: Agent) -> None: @@ -544,27 +553,15 @@ class AgentLifecycleService: ) -> AgentCreate: if actor.actor_type == "user": ctx = await self.require_user_context(actor.user) - if not is_org_admin(ctx.member): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + OpenClawAuthorizationPolicy.require_org_admin(is_admin=is_org_admin(ctx.member)) return payload if actor.actor_type == "agent": - if not actor.agent or not actor.agent.is_board_lead: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Only board leads can create agents", - ) - if not actor.agent.board_id: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Board lead must be assigned to a board", - ) - if payload.board_id and payload.board_id != actor.agent.board_id: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Board leads can only create agents in their own board", - ) - return AgentCreate(**{**payload.model_dump(), "board_id": actor.agent.board_id}) + board_id = OpenClawAuthorizationPolicy.resolve_board_lead_create_board_id( + actor_agent=actor.agent, + requested_board_id=payload.board_id, + ) + return AgentCreate(**{**payload.model_dump(), "board_id": board_id}) return payload @@ -718,8 +715,8 @@ class AgentLifecycleService: updates: dict[str, Any], make_main: bool | None, ) -> None: - if make_main and not is_org_admin(ctx.member): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + if make_main: + OpenClawAuthorizationPolicy.require_org_admin(is_admin=is_org_admin(ctx.member)) if "status" in updates: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -727,15 +724,17 @@ class AgentLifecycleService: ) if "board_id" in updates and updates["board_id"] is not None: new_board = await self.require_board(updates["board_id"]) - if new_board.organization_id != ctx.organization.id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - if not await has_board_access( + OpenClawAuthorizationPolicy.require_board_in_org( + board=new_board, + organization_id=ctx.organization.id, + ) + allowed = await has_board_access( self.session, member=ctx.member, board=new_board, write=True, - ): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + ) + OpenClawAuthorizationPolicy.require_board_write_access(allowed=allowed) async def apply_agent_update_mutations( self, @@ -919,8 +918,7 @@ class AgentLifecycleService: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) if actor.actor_type == "user": ctx = await self.require_user_context(actor.user) - if not is_org_admin(ctx.member): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + OpenClawAuthorizationPolicy.require_org_admin(is_admin=is_org_admin(ctx.member)) board = await self.require_board( payload.board_id, @@ -1045,8 +1043,10 @@ class AgentLifecycleService: ctx: OrganizationContext, ) -> LimitOffsetPage[AgentRead]: board_ids = await list_accessible_board_ids(self.session, member=ctx.member, write=False) - if board_id is not None and board_id not in set(board_ids): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + if board_id is not None: + OpenClawAuthorizationPolicy.require_board_write_access( + allowed=board_id in set(board_ids), + ) base_filters: list[ColumnElement[bool]] = [] if board_ids: base_filters.append(col(Agent.board_id).in_(board_ids)) @@ -1099,8 +1099,8 @@ class AgentLifecycleService: last_seen = since_dt board_ids = await list_accessible_board_ids(self.session, member=ctx.member, write=False) allowed_ids = set(board_ids) - if board_id is not None and board_id not in allowed_ids: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + if board_id is not None: + OpenClawAuthorizationPolicy.require_board_write_access(allowed=board_id in allowed_ids) async def event_generator() -> AsyncIterator[dict[str, str]]: nonlocal last_seen @@ -1258,12 +1258,14 @@ class AgentLifecycleService: agent = await Agent.objects.by_id(agent_id).first(self.session) if agent is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - if actor.actor_type == "agent" and actor.agent and actor.agent.id != agent.id: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + if actor.actor_type == "agent": + OpenClawAuthorizationPolicy.require_same_agent_actor( + actor_agent_id=actor.agent.id if actor.agent else None, + target_agent_id=agent.id, + ) if actor.actor_type == "user": ctx = await self.require_user_context(actor.user) - if not is_org_admin(ctx.member): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + OpenClawAuthorizationPolicy.require_org_admin(is_admin=is_org_admin(ctx.member)) await self.require_agent_access(agent=agent, ctx=ctx, write=True) return await self.commit_heartbeat( agent=agent, @@ -1301,8 +1303,11 @@ class AgentLifecycleService: agent=agent, user=actor.user, ) - elif actor.actor_type == "agent" and actor.agent and actor.agent.id != agent.id: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + elif actor.actor_type == "agent": + OpenClawAuthorizationPolicy.require_same_agent_actor( + actor_agent_id=actor.agent.id if actor.agent else None, + target_agent_id=agent.id, + ) await self.ensure_heartbeat_session_key( agent=agent, diff --git a/backend/app/services/openclaw/coordination_service.py b/backend/app/services/openclaw/coordination_service.py index 3c4b6241..99f970bc 100644 --- a/backend/app/services/openclaw/coordination_service.py +++ b/backend/app/services/openclaw/coordination_service.py @@ -35,6 +35,7 @@ from app.services.openclaw.exceptions import ( map_gateway_error_to_http_exception, ) from app.services.openclaw.internal import agent_key, with_coordination_gateway_retry +from app.services.openclaw.policies import OpenClawAuthorizationPolicy from app.services.openclaw.provisioning import ( LeadAgentOptions, LeadAgentRequest, @@ -140,19 +141,12 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): self, actor_agent: Agent, ) -> tuple[Gateway, GatewayClientConfig]: - detail = "Only the dedicated gateway agent may call this endpoint." - if actor_agent.board_id is not None: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=detail) gateway = await Gateway.objects.by_id(actor_agent.gateway_id).first(self.session) - if gateway is None: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=detail) - if actor_agent.openclaw_session_id != GatewayAgentIdentity.session_key(gateway): - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=detail) - if not gateway.url: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Gateway url is required", - ) + gateway = OpenClawAuthorizationPolicy.require_gateway_main_actor_binding( + actor_agent=actor_agent, + gateway=gateway, + ) + OpenClawAuthorizationPolicy.require_gateway_configured(gateway) return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token) async def require_gateway_board( @@ -162,11 +156,10 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): board_id: UUID | str, ) -> Board: board = await Board.objects.by_id(board_id).first(self.session) - if board is None: - raise HTTPException(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) - return board + return OpenClawAuthorizationPolicy.require_board_in_gateway( + board=board, + gateway=gateway, + ) async def _board_agent_or_404( self, @@ -175,9 +168,10 @@ class GatewayCoordinationService(AbstractGatewayMessagingService): agent_id: str, ) -> Agent: target = await Agent.objects.by_id(agent_id).first(self.session) - if target is None or (target.board_id and target.board_id != board.id): - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) - return target + return OpenClawAuthorizationPolicy.require_board_agent_target( + target=target, + board=board, + ) @staticmethod def _gateway_file_content(payload: object) -> str | None: diff --git a/backend/app/services/openclaw/policies.py b/backend/app/services/openclaw/policies.py new file mode 100644 index 00000000..556abf4c --- /dev/null +++ b/backend/app/services/openclaw/policies.py @@ -0,0 +1,168 @@ +"""OpenClaw authorization policy primitives.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING +from uuid import UUID + +from fastapi import HTTPException, status + +from app.services.openclaw.shared import GatewayAgentIdentity + +if TYPE_CHECKING: + from app.models.agents import Agent + from app.models.boards import Board + from app.models.gateways import Gateway + + +class OpenClawAuthorizationPolicy: + """Centralized authz checks for OpenClaw lifecycle and coordination actions.""" + + _GATEWAY_MAIN_ONLY_DETAIL = "Only the dedicated gateway agent may call this endpoint." + + @staticmethod + def require_org_admin(*, is_admin: bool) -> None: + if not is_admin: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + @staticmethod + def require_same_agent_actor( + *, + actor_agent_id: UUID | None, + target_agent_id: UUID, + ) -> None: + if actor_agent_id is not None and actor_agent_id != target_agent_id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + @staticmethod + def require_gateway_scoped_actor(*, actor_agent: Agent) -> None: + if actor_agent.board_id is not None: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + @classmethod + def require_gateway_main_actor_binding( + cls, + *, + actor_agent: Agent, + gateway: Gateway | None, + ) -> Gateway: + cls.require_gateway_scoped_actor(actor_agent=actor_agent) + if gateway is None: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=cls._GATEWAY_MAIN_ONLY_DETAIL, + ) + if actor_agent.openclaw_session_id != GatewayAgentIdentity.session_key(gateway): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=cls._GATEWAY_MAIN_ONLY_DETAIL, + ) + return gateway + + @staticmethod + def require_gateway_configured(gateway: Gateway) -> None: + if not gateway.url: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail="Gateway url is required", + ) + + @staticmethod + def require_gateway_in_org( + *, + gateway: Gateway | None, + organization_id: UUID, + ) -> Gateway: + if gateway is None or gateway.organization_id != organization_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + return gateway + + @staticmethod + def require_board_in_org( + *, + board: Board | None, + organization_id: UUID, + ) -> Board: + if board is None or board.organization_id != organization_id: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + return board + + @staticmethod + def require_board_in_gateway( + *, + board: Board | None, + gateway: Gateway, + ) -> Board: + if board is None: + raise HTTPException( + 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) + return board + + @staticmethod + def require_board_agent_target( + *, + target: Agent | None, + board: Board, + ) -> Agent: + if target is None or (target.board_id and target.board_id != board.id): + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + return target + + @staticmethod + def require_board_write_access(*, allowed: bool) -> None: + if not allowed: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + @staticmethod + def require_board_lead_actor( + *, + actor_agent: Agent | None, + detail: str = "Only board leads can perform this action", + ) -> Agent: + if actor_agent is None or not actor_agent.is_board_lead: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=detail, + ) + if not actor_agent.board_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Board lead must be assigned to a board", + ) + return actor_agent + + @staticmethod + def require_board_lead_or_same_actor( + *, + actor_agent: Agent, + target_agent_id: str, + ) -> None: + allowed = actor_agent.is_board_lead or str(actor_agent.id) == target_agent_id + if not allowed: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + + @classmethod + def resolve_board_lead_create_board_id( + cls, + *, + actor_agent: Agent | None, + requested_board_id: UUID | None, + ) -> UUID: + lead = cls.require_board_lead_actor( + actor_agent=actor_agent, + detail="Only board leads can create agents", + ) + lead_board_id = lead.board_id + if lead_board_id is None: + msg = "Board lead must be assigned to a board" + raise RuntimeError(msg) + if requested_board_id and requested_board_id != lead_board_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Board leads can only create agents in their own board", + ) + return lead_board_id diff --git a/backend/app/services/openclaw/session_service.py b/backend/app/services/openclaw/session_service.py index d45ae70e..e0f39f5b 100644 --- a/backend/app/services/openclaw/session_service.py +++ b/backend/app/services/openclaw/session_service.py @@ -30,6 +30,7 @@ from app.schemas.gateway_api import ( GatewaySessionsResponse, GatewaysStatusResponse, ) +from app.services.openclaw.policies import OpenClawAuthorizationPolicy from app.services.organizations import require_board_access if TYPE_CHECKING: @@ -140,11 +141,7 @@ class GatewaySessionService: status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="Board gateway_id is invalid", ) - if not gateway.url: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail="Gateway url is required", - ) + OpenClawAuthorizationPolicy.require_gateway_configured(gateway) main_agent = ( await Agent.objects.filter_by(gateway_id=gateway.id) .filter(col(Agent.board_id).is_(None)) @@ -197,8 +194,11 @@ class GatewaySessionService: @staticmethod def _require_same_org(board: Board | None, organization_id: UUID) -> None: - if board is not None and board.organization_id != organization_id: - raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + if board is None: + return + OpenClawAuthorizationPolicy.require_board_write_access( + allowed=board.organization_id == organization_id, + ) async def get_status( self,