refactor: enforce architectural boundaries by updating OpenClaw service imports
This commit is contained in:
@@ -25,7 +25,6 @@ from app.core.config import settings
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.db.pagination import paginate
|
from app.db.pagination import paginate
|
||||||
from app.db.session import async_session_maker, get_session
|
from app.db.session import async_session_maker, get_session
|
||||||
from app.integrations.openclaw_gateway import OpenClawGatewayError
|
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
from app.models.board_group_memory import BoardGroupMemory
|
from app.models.board_group_memory import BoardGroupMemory
|
||||||
from app.models.board_groups import BoardGroup
|
from app.models.board_groups import BoardGroup
|
||||||
@@ -35,6 +34,7 @@ from app.schemas.board_group_memory import BoardGroupMemoryCreate, BoardGroupMem
|
|||||||
from app.schemas.pagination import DefaultLimitOffsetPage
|
from app.schemas.pagination import DefaultLimitOffsetPage
|
||||||
from app.services.mentions import extract_mentions, matches_agent_mention
|
from app.services.mentions import extract_mentions, matches_agent_mention
|
||||||
from app.services.openclaw.shared import (
|
from app.services.openclaw.shared import (
|
||||||
|
GatewayTransportError,
|
||||||
optional_gateway_config_for_board,
|
optional_gateway_config_for_board,
|
||||||
send_gateway_agent_message,
|
send_gateway_agent_message,
|
||||||
)
|
)
|
||||||
@@ -250,7 +250,7 @@ async def _notify_group_target(
|
|||||||
agent_name=agent.name,
|
agent_name=agent.name,
|
||||||
message=message,
|
message=message,
|
||||||
)
|
)
|
||||||
except OpenClawGatewayError:
|
except GatewayTransportError:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from app.core.time import utcnow
|
|||||||
from app.db import crud
|
from app.db import crud
|
||||||
from app.db.pagination import paginate
|
from app.db.pagination import paginate
|
||||||
from app.db.session import get_session
|
from app.db.session import get_session
|
||||||
from app.integrations.openclaw_gateway import OpenClawGatewayError
|
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
from app.models.board_group_memory import BoardGroupMemory
|
from app.models.board_group_memory import BoardGroupMemory
|
||||||
from app.models.board_groups import BoardGroup
|
from app.models.board_groups import BoardGroup
|
||||||
@@ -32,6 +31,7 @@ from app.schemas.view_models import BoardGroupSnapshot
|
|||||||
from app.services.board_group_snapshot import build_group_snapshot
|
from app.services.board_group_snapshot import build_group_snapshot
|
||||||
from app.services.openclaw.constants import DEFAULT_HEARTBEAT_CONFIG
|
from app.services.openclaw.constants import DEFAULT_HEARTBEAT_CONFIG
|
||||||
from app.services.openclaw.provisioning import sync_gateway_agent_heartbeats
|
from app.services.openclaw.provisioning import sync_gateway_agent_heartbeats
|
||||||
|
from app.services.openclaw.shared import GatewayTransportError
|
||||||
from app.services.organizations import (
|
from app.services.organizations import (
|
||||||
OrganizationContext,
|
OrganizationContext,
|
||||||
board_access_filter,
|
board_access_filter,
|
||||||
@@ -270,7 +270,7 @@ async def _sync_gateway_heartbeats(
|
|||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
await sync_gateway_agent_heartbeats(gateway, gateway_agents)
|
await sync_gateway_agent_heartbeats(gateway, gateway_agents)
|
||||||
except OpenClawGatewayError:
|
except GatewayTransportError:
|
||||||
failed_agent_ids.extend([agent.id for agent in gateway_agents])
|
failed_agent_ids.extend([agent.id for agent in gateway_agents])
|
||||||
return failed_agent_ids
|
return failed_agent_ids
|
||||||
|
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ from app.core.config import settings
|
|||||||
from app.core.time import utcnow
|
from app.core.time import utcnow
|
||||||
from app.db.pagination import paginate
|
from app.db.pagination import paginate
|
||||||
from app.db.session import async_session_maker, get_session
|
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
|
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
from app.models.board_memory import BoardMemory
|
from app.models.board_memory import BoardMemory
|
||||||
from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead
|
from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead
|
||||||
from app.schemas.pagination import DefaultLimitOffsetPage
|
from app.schemas.pagination import DefaultLimitOffsetPage
|
||||||
from app.services.mentions import extract_mentions, matches_agent_mention
|
from app.services.mentions import extract_mentions, matches_agent_mention
|
||||||
from app.services.openclaw.shared import (
|
from app.services.openclaw.shared import (
|
||||||
|
GatewayClientConfig,
|
||||||
|
GatewayTransportError,
|
||||||
optional_gateway_config_for_board,
|
optional_gateway_config_for_board,
|
||||||
send_gateway_agent_message,
|
send_gateway_agent_message,
|
||||||
)
|
)
|
||||||
@@ -124,7 +124,7 @@ async def _send_control_command(
|
|||||||
message=command,
|
message=command,
|
||||||
deliver=True,
|
deliver=True,
|
||||||
)
|
)
|
||||||
except OpenClawGatewayError:
|
except GatewayTransportError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ async def _notify_chat_targets(
|
|||||||
agent_name=agent.name,
|
agent_name=agent.name,
|
||||||
message=message,
|
message=message,
|
||||||
)
|
)
|
||||||
except OpenClawGatewayError:
|
except GatewayTransportError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ from app.core.time import utcnow
|
|||||||
from app.db import crud
|
from app.db import crud
|
||||||
from app.db.pagination import paginate
|
from app.db.pagination import paginate
|
||||||
from app.db.session import get_session
|
from app.db.session import get_session
|
||||||
from app.integrations.openclaw_gateway import OpenClawGatewayError
|
|
||||||
from app.models.activity_events import ActivityEvent
|
from app.models.activity_events import ActivityEvent
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
from app.models.approvals import Approval
|
from app.models.approvals import Approval
|
||||||
@@ -41,6 +40,7 @@ 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.provisioning import cleanup_agent
|
from app.services.openclaw.provisioning import cleanup_agent
|
||||||
|
from app.services.openclaw.shared import GatewayTransportError
|
||||||
from app.services.organizations import OrganizationContext, board_access_filter
|
from app.services.organizations import OrganizationContext, board_access_filter
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -288,7 +288,7 @@ async def delete_board(
|
|||||||
try:
|
try:
|
||||||
for agent in agents:
|
for agent in agents:
|
||||||
await cleanup_agent(agent, config)
|
await cleanup_agent(agent, config)
|
||||||
except OpenClawGatewayError as exc:
|
except GatewayTransportError as exc:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||||
detail=f"Gateway cleanup failed: {exc}",
|
detail=f"Gateway cleanup failed: {exc}",
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ from app.core.time import utcnow
|
|||||||
from app.db import crud
|
from app.db import crud
|
||||||
from app.db.pagination import paginate
|
from app.db.pagination import paginate
|
||||||
from app.db.session import async_session_maker, get_session
|
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
|
|
||||||
from app.models.activity_events import ActivityEvent
|
from app.models.activity_events import ActivityEvent
|
||||||
from app.models.agents import Agent
|
from app.models.agents import Agent
|
||||||
from app.models.approvals import Approval
|
from app.models.approvals import Approval
|
||||||
@@ -44,6 +42,8 @@ from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, Ta
|
|||||||
from app.services.activity_log import record_activity
|
from app.services.activity_log import record_activity
|
||||||
from app.services.mentions import extract_mentions, matches_agent_mention
|
from app.services.mentions import extract_mentions, matches_agent_mention
|
||||||
from app.services.openclaw.shared import (
|
from app.services.openclaw.shared import (
|
||||||
|
GatewayClientConfig,
|
||||||
|
GatewayTransportError,
|
||||||
optional_gateway_config_for_board,
|
optional_gateway_config_for_board,
|
||||||
send_gateway_agent_message,
|
send_gateway_agent_message,
|
||||||
)
|
)
|
||||||
@@ -376,7 +376,7 @@ async def _notify_agent_on_task_assign(
|
|||||||
task_id=task.id,
|
task_id=task.id,
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
except OpenClawGatewayError as exc:
|
except GatewayTransportError as exc:
|
||||||
record_activity(
|
record_activity(
|
||||||
session,
|
session,
|
||||||
event_type="task.assignee_notify_failed",
|
event_type="task.assignee_notify_failed",
|
||||||
@@ -447,7 +447,7 @@ async def _notify_lead_on_task_create(
|
|||||||
task_id=task.id,
|
task_id=task.id,
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
except OpenClawGatewayError as exc:
|
except GatewayTransportError as exc:
|
||||||
record_activity(
|
record_activity(
|
||||||
session,
|
session,
|
||||||
event_type="task.lead_notify_failed",
|
event_type="task.lead_notify_failed",
|
||||||
@@ -502,7 +502,7 @@ async def _notify_lead_on_task_unassigned(
|
|||||||
task_id=task.id,
|
task_id=task.id,
|
||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
except OpenClawGatewayError as exc:
|
except GatewayTransportError as exc:
|
||||||
record_activity(
|
record_activity(
|
||||||
session,
|
session,
|
||||||
event_type="task.lead_unassigned_notify_failed",
|
event_type="task.lead_unassigned_notify_failed",
|
||||||
@@ -1057,7 +1057,7 @@ async def _notify_task_comment_targets(
|
|||||||
"If you are mentioned but not assigned, reply in the task "
|
"If you are mentioned but not assigned, reply in the task "
|
||||||
"thread but do not change task status."
|
"thread but do not change task status."
|
||||||
)
|
)
|
||||||
with suppress(OpenClawGatewayError):
|
with suppress(GatewayTransportError):
|
||||||
await _send_agent_task_message(
|
await _send_agent_task_message(
|
||||||
session_key=agent.openclaw_session_id,
|
session_key=agent.openclaw_session_id,
|
||||||
config=config,
|
config=config,
|
||||||
|
|||||||
@@ -1,88 +1,7 @@
|
|||||||
"""OpenClaw lifecycle services package."""
|
"""OpenClaw lifecycle package.
|
||||||
|
|
||||||
from .admin_service import (
|
Import concrete modules directly (for example: ``app.services.openclaw.agent_service``)
|
||||||
AbstractGatewayMainAgentManager,
|
to keep architectural boundaries explicit.
|
||||||
DefaultGatewayMainAgentManager,
|
"""
|
||||||
GatewayAdminLifecycleService,
|
|
||||||
)
|
|
||||||
from .agent_service import (
|
|
||||||
AbstractProvisionExecution,
|
|
||||||
ActorContextLike,
|
|
||||||
AgentLifecycleService,
|
|
||||||
AgentUpdateOptions,
|
|
||||||
AgentUpdateProvisionRequest,
|
|
||||||
AgentUpdateProvisionTarget,
|
|
||||||
BoardAgentProvisionExecution,
|
|
||||||
MainAgentProvisionExecution,
|
|
||||||
)
|
|
||||||
from .constants import DEFAULT_CHANNEL_HEARTBEAT_VISIBILITY, DEFAULT_HEARTBEAT_CONFIG
|
|
||||||
from .coordination_service import AbstractGatewayMessagingService, GatewayCoordinationService
|
|
||||||
from .exceptions import (
|
|
||||||
GatewayErrorPolicy,
|
|
||||||
GatewayOperation,
|
|
||||||
map_gateway_error_message,
|
|
||||||
map_gateway_error_to_http_exception,
|
|
||||||
)
|
|
||||||
from .onboarding_service import BoardOnboardingMessagingService
|
|
||||||
from .provisioning import (
|
|
||||||
AgentProvisionRequest,
|
|
||||||
LeadAgentOptions,
|
|
||||||
LeadAgentRequest,
|
|
||||||
MainAgentProvisionRequest,
|
|
||||||
ProvisionOptions,
|
|
||||||
cleanup_agent,
|
|
||||||
ensure_board_lead_agent,
|
|
||||||
patch_gateway_agent_heartbeats,
|
|
||||||
provision_agent,
|
|
||||||
provision_main_agent,
|
|
||||||
sync_gateway_agent_heartbeats,
|
|
||||||
)
|
|
||||||
from .session_service import GatewaySessionService, GatewayTemplateSyncQuery
|
|
||||||
from .shared import (
|
|
||||||
GatewayAgentIdentity,
|
|
||||||
optional_gateway_config_for_board,
|
|
||||||
require_gateway_config_for_board,
|
|
||||||
resolve_trace_id,
|
|
||||||
send_gateway_agent_message,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
__all__: list[str] = []
|
||||||
"AbstractGatewayMainAgentManager",
|
|
||||||
"DefaultGatewayMainAgentManager",
|
|
||||||
"GatewayAdminLifecycleService",
|
|
||||||
"AbstractProvisionExecution",
|
|
||||||
"ActorContextLike",
|
|
||||||
"AgentLifecycleService",
|
|
||||||
"AgentUpdateOptions",
|
|
||||||
"AgentUpdateProvisionRequest",
|
|
||||||
"AgentUpdateProvisionTarget",
|
|
||||||
"BoardAgentProvisionExecution",
|
|
||||||
"MainAgentProvisionExecution",
|
|
||||||
"DEFAULT_CHANNEL_HEARTBEAT_VISIBILITY",
|
|
||||||
"DEFAULT_HEARTBEAT_CONFIG",
|
|
||||||
"AbstractGatewayMessagingService",
|
|
||||||
"GatewayCoordinationService",
|
|
||||||
"GatewayErrorPolicy",
|
|
||||||
"GatewayOperation",
|
|
||||||
"map_gateway_error_message",
|
|
||||||
"map_gateway_error_to_http_exception",
|
|
||||||
"BoardOnboardingMessagingService",
|
|
||||||
"AgentProvisionRequest",
|
|
||||||
"LeadAgentOptions",
|
|
||||||
"LeadAgentRequest",
|
|
||||||
"MainAgentProvisionRequest",
|
|
||||||
"ProvisionOptions",
|
|
||||||
"cleanup_agent",
|
|
||||||
"ensure_board_lead_agent",
|
|
||||||
"patch_gateway_agent_heartbeats",
|
|
||||||
"provision_agent",
|
|
||||||
"provision_main_agent",
|
|
||||||
"sync_gateway_agent_heartbeats",
|
|
||||||
"GatewaySessionService",
|
|
||||||
"GatewayTemplateSyncQuery",
|
|
||||||
"GatewayAgentIdentity",
|
|
||||||
"optional_gateway_config_for_board",
|
|
||||||
"require_gateway_config_for_board",
|
|
||||||
"resolve_trace_id",
|
|
||||||
"send_gateway_agent_message",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ from uuid import UUID, uuid4
|
|||||||
|
|
||||||
from fastapi import HTTPException, status
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
|
from app.integrations.openclaw_gateway import GatewayConfig as _GatewayClientConfig
|
||||||
from app.integrations.openclaw_gateway import ensure_session, send_message
|
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
|
||||||
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.constants import (
|
from app.services.openclaw.constants import (
|
||||||
@@ -22,6 +22,9 @@ if TYPE_CHECKING:
|
|||||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
|
GatewayClientConfig = _GatewayClientConfig
|
||||||
|
|
||||||
|
|
||||||
class GatewayAgentIdentity:
|
class GatewayAgentIdentity:
|
||||||
"""Naming and identity rules for Mission Control gateway-main agents."""
|
"""Naming and identity rules for Mission Control gateway-main agents."""
|
||||||
|
|
||||||
@@ -87,6 +90,28 @@ async def send_gateway_agent_message(
|
|||||||
await send_message(message, session_key=session_key, config=config, deliver=deliver)
|
await send_message(message, session_key=session_key, config=config, deliver=deliver)
|
||||||
|
|
||||||
|
|
||||||
|
async def send_gateway_agent_message_safe(
|
||||||
|
*,
|
||||||
|
session_key: str,
|
||||||
|
config: GatewayClientConfig,
|
||||||
|
agent_name: str,
|
||||||
|
message: str,
|
||||||
|
deliver: bool = False,
|
||||||
|
) -> GatewayTransportError | None:
|
||||||
|
"""Best-effort gateway dispatch returning transport error when one occurs."""
|
||||||
|
try:
|
||||||
|
await send_gateway_agent_message(
|
||||||
|
session_key=session_key,
|
||||||
|
config=config,
|
||||||
|
agent_name=agent_name,
|
||||||
|
message=message,
|
||||||
|
deliver=deliver,
|
||||||
|
)
|
||||||
|
except GatewayTransportError as exc:
|
||||||
|
return exc
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def resolve_trace_id(correlation_id: str | None, *, prefix: str) -> str:
|
def resolve_trace_id(correlation_id: str | None, *, prefix: str) -> str:
|
||||||
"""Resolve a stable trace id from correlation id or generate a scoped fallback."""
|
"""Resolve a stable trace id from correlation id or generate a scoped fallback."""
|
||||||
normalized = (correlation_id or "").strip()
|
normalized = (correlation_id or "").strip()
|
||||||
@@ -96,3 +121,6 @@ def resolve_trace_id(correlation_id: str | None, *, prefix: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Keep integration exceptions behind the OpenClaw service boundary.
|
||||||
|
GatewayTransportError = OpenClawGatewayError
|
||||||
|
|||||||
28
backend/tests/test_api_openclaw_integration_boundary.py
Normal file
28
backend/tests/test_api_openclaw_integration_boundary.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# ruff: noqa: S101
|
||||||
|
"""Architectural boundary tests for API/OpenClaw integration usage."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_does_not_import_openclaw_gateway_client_directly() -> None:
|
||||||
|
"""API modules should use OpenClaw services, not integration client imports."""
|
||||||
|
repo_root = Path(__file__).resolve().parents[2]
|
||||||
|
api_root = repo_root / "backend" / "app" / "api"
|
||||||
|
|
||||||
|
violations: list[str] = []
|
||||||
|
for path in api_root.rglob("*.py"):
|
||||||
|
rel = path.relative_to(repo_root)
|
||||||
|
for lineno, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||||
|
line = raw_line.strip()
|
||||||
|
if line.startswith("from app.integrations.openclaw_gateway import "):
|
||||||
|
violations.append(f"{rel}:{lineno}")
|
||||||
|
elif line.startswith("import app.integrations.openclaw_gateway"):
|
||||||
|
violations.append(f"{rel}:{lineno}")
|
||||||
|
|
||||||
|
assert not violations, (
|
||||||
|
"Import OpenClaw integration details via service modules (for example "
|
||||||
|
"`app.services.openclaw.shared`) instead of directly from `app.api`. "
|
||||||
|
f"Violations: {', '.join(violations)}"
|
||||||
|
)
|
||||||
32
backend/tests/test_openclaw_import_boundaries.py
Normal file
32
backend/tests/test_openclaw_import_boundaries.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# ruff: noqa: S101
|
||||||
|
"""Architectural boundary tests for OpenClaw service imports."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_openclaw_package_barrel_imports() -> None:
|
||||||
|
"""Disallow `from app.services.openclaw import ...` in backend code."""
|
||||||
|
repo_root = Path(__file__).resolve().parents[2]
|
||||||
|
backend_root = repo_root / "backend"
|
||||||
|
scan_roots = (backend_root / "app", backend_root / "tests")
|
||||||
|
|
||||||
|
violations: list[str] = []
|
||||||
|
for root in scan_roots:
|
||||||
|
for path in root.rglob("*.py"):
|
||||||
|
if path.name == "__init__.py":
|
||||||
|
continue
|
||||||
|
rel = path.relative_to(repo_root)
|
||||||
|
for lineno, raw_line in enumerate(
|
||||||
|
path.read_text(encoding="utf-8").splitlines(), start=1
|
||||||
|
):
|
||||||
|
line = raw_line.strip()
|
||||||
|
if line.startswith("from app.services.openclaw import "):
|
||||||
|
violations.append(f"{rel}:{lineno}")
|
||||||
|
|
||||||
|
assert not violations, (
|
||||||
|
"Use concrete OpenClaw modules (for example "
|
||||||
|
"`from app.services.openclaw.agent_service import ...`) instead of package imports. "
|
||||||
|
f"Violations: {', '.join(violations)}"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user