refactor: centralize gateway resolution logic with new helper functions
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
96
backend/app/services/openclaw/gateway_resolver.py
Normal file
96
backend/app/services/openclaw/gateway_resolver.py
Normal 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
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user