refactor: centralize gateway resolution logic with new helper functions

This commit is contained in:
Abhimanyu Saharan
2026-02-11 01:47:24 +05:30
parent 9f4b38e86f
commit 275cc6f473
7 changed files with 147 additions and 100 deletions

View File

@@ -21,7 +21,6 @@ from app.core.config import settings
from app.core.time import utcnow from app.core.time import utcnow
from app.db.session import get_session from app.db.session import get_session
from app.models.board_onboarding import BoardOnboardingSession from app.models.board_onboarding import BoardOnboardingSession
from app.models.gateways import Gateway
from app.schemas.board_onboarding import ( from app.schemas.board_onboarding import (
BoardOnboardingAgentComplete, BoardOnboardingAgentComplete,
BoardOnboardingAgentUpdate, BoardOnboardingAgentUpdate,
@@ -33,6 +32,7 @@ from app.schemas.board_onboarding import (
BoardOnboardingUserProfile, BoardOnboardingUserProfile,
) )
from app.schemas.boards import BoardRead from app.schemas.boards import BoardRead
from app.services.openclaw.gateway_resolver import get_gateway_for_board
from app.services.openclaw.gateway_dispatch import GatewayDispatchService from app.services.openclaw.gateway_dispatch import GatewayDispatchService
from app.services.openclaw.onboarding_service import BoardOnboardingMessagingService from app.services.openclaw.onboarding_service import BoardOnboardingMessagingService
from app.services.openclaw.policies import OpenClawAuthorizationPolicy from app.services.openclaw.policies import OpenClawAuthorizationPolicy
@@ -310,9 +310,8 @@ async def agent_onboarding_update(
agent = actor.agent agent = actor.agent
OpenClawAuthorizationPolicy.require_gateway_scoped_actor(actor_agent=agent) OpenClawAuthorizationPolicy.require_gateway_scoped_actor(actor_agent=agent)
if board.gateway_id: gateway = await get_gateway_for_board(session, board)
gateway = await Gateway.objects.by_id(board.gateway_id).first(session) if gateway is not None:
if gateway:
OpenClawAuthorizationPolicy.require_gateway_main_actor_binding( OpenClawAuthorizationPolicy.require_gateway_main_actor_binding(
actor_agent=agent, actor_agent=agent,
gateway=gateway, gateway=gateway,

View File

@@ -39,6 +39,7 @@ from app.schemas.pagination import DefaultLimitOffsetPage
from app.schemas.view_models import BoardGroupSnapshot, BoardSnapshot from app.schemas.view_models import BoardGroupSnapshot, BoardSnapshot
from app.services.board_group_snapshot import build_board_group_snapshot from app.services.board_group_snapshot import build_board_group_snapshot
from app.services.board_snapshot import build_board_snapshot from app.services.board_snapshot import build_board_snapshot
from app.services.openclaw.gateway_resolver import gateway_client_config, require_gateway_for_board
from app.services.openclaw.gateway_rpc import OpenClawGatewayError from app.services.openclaw.gateway_rpc import OpenClawGatewayError
from app.services.openclaw.provisioning import OpenClawGatewayProvisioner from app.services.openclaw.provisioning import OpenClawGatewayProvisioner
from app.services.organizations import OrganizationContext, board_access_filter from app.services.organizations import OrganizationContext, board_access_filter
@@ -173,23 +174,10 @@ async def _board_gateway(
) -> Gateway | None: ) -> Gateway | None:
if not board.gateway_id: if not board.gateway_id:
return None return None
config = await Gateway.objects.by_id(board.gateway_id).first(session) gateway = await require_gateway_for_board(session, board, require_workspace_root=True)
if config is None: # Validate the connection config; the caller needs a configured gateway URL.
raise HTTPException( gateway_client_config(gateway)
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, return gateway
detail="Board gateway_id is invalid",
)
if not config.url:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway url is required",
)
if not config.workspace_root:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway workspace_root is required",
)
return config
@router.get("", response_model=DefaultLimitOffsetPage[BoardRead]) @router.get("", response_model=DefaultLimitOffsetPage[BoardRead])

View File

@@ -33,6 +33,7 @@ from app.services.openclaw.exceptions import (
map_gateway_error_to_http_exception, map_gateway_error_to_http_exception,
) )
from app.services.openclaw.gateway_dispatch import GatewayDispatchService from app.services.openclaw.gateway_dispatch import GatewayDispatchService
from app.services.openclaw.gateway_resolver import gateway_client_config
from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig
from app.services.openclaw.gateway_rpc import OpenClawGatewayError, openclaw_call from app.services.openclaw.gateway_rpc import OpenClawGatewayError, openclaw_call
from app.services.openclaw.internal.agent_key import agent_key from app.services.openclaw.internal.agent_key import agent_key
@@ -119,8 +120,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService):
actor_agent=actor_agent, actor_agent=actor_agent,
gateway=gateway, gateway=gateway,
) )
OpenClawAuthorizationPolicy.require_gateway_configured(gateway) return gateway, gateway_client_config(gateway)
return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token)
async def require_gateway_board( async def require_gateway_board(
self, self,

View File

@@ -8,11 +8,15 @@ from __future__ import annotations
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, status
from app.models.boards import Board from app.models.boards import Board
from app.models.gateways import Gateway from app.models.gateways import Gateway
from app.services.openclaw.db_service import OpenClawDBService from app.services.openclaw.db_service import OpenClawDBService
from app.services.openclaw.gateway_resolver import (
get_gateway_for_board,
gateway_client_config,
optional_gateway_client_config,
require_gateway_for_board,
)
from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig
from app.services.openclaw.gateway_rpc import OpenClawGatewayError, ensure_session, send_message from app.services.openclaw.gateway_rpc import OpenClawGatewayError, ensure_session, send_message
@@ -24,29 +28,15 @@ class GatewayDispatchService(OpenClawDBService):
self, self,
board: Board, board: Board,
) -> GatewayClientConfig | None: ) -> GatewayClientConfig | None:
if board.gateway_id is None: gateway = await get_gateway_for_board(self.session, board)
return None return optional_gateway_client_config(gateway)
gateway = await Gateway.objects.by_id(board.gateway_id).first(self.session)
if gateway is None or not gateway.url:
return None
return GatewayClientConfig(url=gateway.url, token=gateway.token)
async def require_gateway_config_for_board( async def require_gateway_config_for_board(
self, self,
board: Board, board: Board,
) -> tuple[Gateway, GatewayClientConfig]: ) -> tuple[Gateway, GatewayClientConfig]:
if board.gateway_id is None: gateway = await require_gateway_for_board(self.session, board)
raise HTTPException( return gateway, gateway_client_config(gateway)
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board is not attached to a gateway",
)
gateway = await Gateway.objects.by_id(board.gateway_id).first(self.session)
if gateway is None or not gateway.url:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway is not configured for this board",
)
return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token)
async def send_agent_message( async def send_agent_message(
self, self,

View File

@@ -0,0 +1,96 @@
"""DB-backed gateway resolution helpers.
This module is the narrow boundary between Mission Control's DB models and the
DB-free OpenClaw gateway client/provisioning layers.
Goals:
- Centralize "board -> gateway row" resolution and defensive org checks.
- Centralize construction of `GatewayConfig` objects used by gateway RPC calls.
- Keep call-sites thin and avoid re-implementing the same validation rules.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from fastapi import HTTPException, status
from app.models.boards import Board
from app.models.gateways import Gateway
from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig
if TYPE_CHECKING:
from sqlmodel.ext.asyncio.session import AsyncSession
def gateway_client_config(gateway: Gateway) -> GatewayClientConfig:
"""Build a gateway RPC config from a Gateway model, requiring a URL."""
url = (gateway.url or "").strip()
if not url:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway url is required",
)
token = (gateway.token or "").strip() or None
return GatewayClientConfig(url=url, token=token)
def optional_gateway_client_config(gateway: Gateway | None) -> GatewayClientConfig | None:
"""Build a gateway RPC config when the gateway is configured; otherwise return None."""
if gateway is None:
return None
url = (gateway.url or "").strip()
if not url:
return None
token = (gateway.token or "").strip() or None
return GatewayClientConfig(url=url, token=token)
def require_gateway_workspace_root(gateway: Gateway) -> str:
"""Return a gateway workspace_root string, requiring it to be configured."""
workspace_root = (gateway.workspace_root or "").strip()
if not workspace_root:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway workspace_root is required",
)
return workspace_root
async def get_gateway_for_board(
session: AsyncSession,
board: Board,
) -> Gateway | None:
"""Return the gateway for a board when present and valid; otherwise return None."""
if board.gateway_id is None:
return None
gateway = await Gateway.objects.by_id(board.gateway_id).first(session)
if gateway is None:
return None
# Defensive guard: boards and gateways are tenant-scoped; reject cross-org mismatches.
if gateway.organization_id != board.organization_id:
return None
return gateway
async def require_gateway_for_board(
session: AsyncSession,
board: Board,
*,
require_workspace_root: bool = False,
) -> Gateway:
"""Return a board's gateway or raise a 422 with a stable error message."""
if board.gateway_id is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_id is required",
)
gateway = await get_gateway_for_board(session, board)
if gateway is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_id is invalid",
)
if require_workspace_root:
require_gateway_workspace_root(gateway)
return gateway

View File

@@ -55,6 +55,11 @@ from app.services.openclaw.db_agent_state import (
mint_agent_token, mint_agent_token,
) )
from app.services.openclaw.db_service import OpenClawDBService from app.services.openclaw.db_service import OpenClawDBService
from app.services.openclaw.gateway_resolver import (
gateway_client_config,
optional_gateway_client_config,
require_gateway_for_board,
)
from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig
from app.services.openclaw.gateway_rpc import ( from app.services.openclaw.gateway_rpc import (
OpenClawGatewayError, OpenClawGatewayError,
@@ -766,42 +771,12 @@ class AgentLifecycleService(OpenClawDBService):
self, self,
board: Board, board: Board,
) -> tuple[Gateway, GatewayClientConfig]: ) -> tuple[Gateway, GatewayClientConfig]:
if not board.gateway_id: gateway = await require_gateway_for_board(
raise HTTPException( self.session,
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, board,
detail="Board gateway_id is required", require_workspace_root=True,
) )
gateway = await Gateway.objects.by_id(board.gateway_id).first(self.session) return gateway, gateway_client_config(gateway)
if gateway is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_id is invalid",
)
if gateway.organization_id != board.organization_id:
raise HTTPException(
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",
)
if not gateway.workspace_root:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway workspace_root is required",
)
return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token)
@staticmethod
def gateway_client_config(gateway: Gateway) -> GatewayClientConfig:
if not gateway.url:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway url is required",
)
return GatewayClientConfig(url=gateway.url, token=gateway.token)
@staticmethod @staticmethod
def is_gateway_main(agent: Agent) -> bool: def is_gateway_main(agent: Agent) -> bool:
@@ -1679,8 +1654,8 @@ class AgentLifecycleService(OpenClawDBService):
if agent.board_id is None: if agent.board_id is None:
# Gateway-main agents are not tied to a board; resolve via agent.gateway_id. # Gateway-main agents are not tied to a board; resolve via agent.gateway_id.
gateway = await Gateway.objects.by_id(agent.gateway_id).first(self.session) gateway = await Gateway.objects.by_id(agent.gateway_id).first(self.session)
if gateway and gateway.url: client_config = optional_gateway_client_config(gateway)
client_config = GatewayClientConfig(url=gateway.url, token=gateway.token) if gateway is not None and client_config is not None:
try: try:
workspace_path = await OpenClawGatewayProvisioner().delete_agent_lifecycle( workspace_path = await OpenClawGatewayProvisioner().delete_agent_lifecycle(
agent=agent, agent=agent,

View File

@@ -12,7 +12,6 @@ from sqlmodel import col
from app.models.agents import Agent from app.models.agents import Agent
from app.models.boards import Board from app.models.boards import Board
from app.models.gateways import Gateway
from app.schemas.gateway_api import ( from app.schemas.gateway_api import (
GatewayResolveQuery, GatewayResolveQuery,
GatewaySessionHistoryResponse, GatewaySessionHistoryResponse,
@@ -22,6 +21,7 @@ from app.schemas.gateway_api import (
GatewaysStatusResponse, GatewaysStatusResponse,
) )
from app.services.openclaw.db_service import OpenClawDBService from app.services.openclaw.db_service import OpenClawDBService
from app.services.openclaw.gateway_resolver import gateway_client_config, require_gateway_for_board
from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig
from app.services.openclaw.gateway_rpc import ( from app.services.openclaw.gateway_rpc import (
OpenClawGatewayError, OpenClawGatewayError,
@@ -95,9 +95,18 @@ class GatewaySessionService(OpenClawDBService):
params.gateway_url, params.gateway_url,
) )
if params.gateway_url: if params.gateway_url:
raw_url = params.gateway_url.strip()
if not raw_url:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="board_id or gateway_url is required",
)
return ( return (
None, None,
GatewayClientConfig(url=params.gateway_url, token=params.gateway_token), GatewayClientConfig(
url=raw_url,
token=(params.gateway_token or "").strip() or None,
),
None, None,
) )
if not params.board_id: if not params.board_id:
@@ -113,18 +122,8 @@ class GatewaySessionService(OpenClawDBService):
) )
if user is not None: if user is not None:
await require_board_access(self.session, user=user, board=board, write=False) await require_board_access(self.session, user=user, board=board, write=False)
if not board.gateway_id: gateway = await require_gateway_for_board(self.session, board)
raise HTTPException( config = gateway_client_config(gateway)
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_id is required",
)
gateway = await Gateway.objects.by_id(board.gateway_id).first(self.session)
if gateway is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_id is invalid",
)
OpenClawAuthorizationPolicy.require_gateway_configured(gateway)
main_agent = ( main_agent = (
await Agent.objects.filter_by(gateway_id=gateway.id) await Agent.objects.filter_by(gateway_id=gateway.id)
.filter(col(Agent.board_id).is_(None)) .filter(col(Agent.board_id).is_(None))
@@ -133,7 +132,7 @@ class GatewaySessionService(OpenClawDBService):
main_session = main_agent.openclaw_session_id if main_agent else None main_session = main_agent.openclaw_session_id if main_agent else None
return ( return (
board, board,
GatewayClientConfig(url=gateway.url, token=gateway.token), config,
main_session, main_session,
) )