716 lines
26 KiB
Python
716 lines
26 KiB
Python
"""Gateway admin lifecycle service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
import re
|
|
from typing import TYPE_CHECKING
|
|
from uuid import UUID
|
|
|
|
from fastapi import HTTPException, status
|
|
from sqlmodel import col
|
|
from sqlmodel import select
|
|
|
|
from app.core.auth import AuthContext
|
|
from app.core.logging import TRACE_LEVEL
|
|
from app.core.time import utcnow
|
|
from app.db import crud
|
|
from app.models.activity_events import ActivityEvent
|
|
from app.models.agents import Agent
|
|
from app.models.approvals import Approval
|
|
from app.models.boards import Board
|
|
from app.models.board_webhooks import BoardWebhook
|
|
from app.models.gateways import Gateway
|
|
from app.models.tasks import Task
|
|
from app.schemas.gateways import (
|
|
GatewayAgentImportCandidate,
|
|
GatewayAgentImportPreviewResponse,
|
|
GatewayAgentImportReconcileSummary,
|
|
GatewayAgentImportResponse,
|
|
GatewayAgentReconcileError,
|
|
GatewayTemplatesSyncResult,
|
|
)
|
|
from app.services.openclaw.constants import DEFAULT_HEARTBEAT_CONFIG
|
|
from app.services.openclaw.db_service import OpenClawDBService
|
|
from app.services.openclaw.error_messages import normalize_gateway_error_message
|
|
from app.services.openclaw.gateway_compat import check_gateway_version_compatibility
|
|
from app.services.openclaw.gateway_rpc import GatewayConfig as GatewayClientConfig
|
|
from app.services.openclaw.gateway_rpc import OpenClawGatewayError, openclaw_call
|
|
from app.services.openclaw.internal.agent_key import agent_key as resolve_agent_key
|
|
from app.services.openclaw.lifecycle_orchestrator import AgentLifecycleOrchestrator
|
|
from app.services.openclaw.provisioning_db import (
|
|
GatewayTemplateSyncOptions,
|
|
OpenClawProvisioningService,
|
|
)
|
|
from app.services.openclaw.session_service import GatewayTemplateSyncQuery
|
|
from app.services.openclaw.shared import GatewayAgentIdentity
|
|
|
|
if TYPE_CHECKING:
|
|
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
|
|
from app.models.users import User
|
|
|
|
|
|
_LEAD_SESSION_KEY_RE = re.compile(
|
|
r"^agent:lead-(?P<board_id>[0-9a-fA-F-]{36}):main$"
|
|
)
|
|
|
|
|
|
def _as_dict_list(value: object) -> list[dict[str, object]]:
|
|
if isinstance(value, dict):
|
|
for key in ("agents", "sessions", "items", "data"):
|
|
nested = value.get(key)
|
|
if isinstance(nested, list):
|
|
return [item for item in nested if isinstance(item, dict)]
|
|
return []
|
|
if isinstance(value, list):
|
|
return [item for item in value if isinstance(item, dict)]
|
|
return []
|
|
|
|
|
|
def _derive_session_key_for_runtime_agent(
|
|
runtime_agent_id: str,
|
|
session_by_agent_key: dict[str, str],
|
|
) -> str:
|
|
existing = session_by_agent_key.get(runtime_agent_id)
|
|
if existing:
|
|
return existing
|
|
return f"agent:{runtime_agent_id}:main"
|
|
|
|
|
|
def _parse_lead_board_id(session_key: str) -> UUID | None:
|
|
matched = _LEAD_SESSION_KEY_RE.match(session_key)
|
|
if matched is None:
|
|
return None
|
|
board_id_raw = matched.group("board_id")
|
|
try:
|
|
return UUID(board_id_raw)
|
|
except ValueError:
|
|
return None
|
|
|
|
|
|
class AbstractGatewayMainAgentManager(ABC):
|
|
"""Abstract manager for gateway-main agent naming/profile behavior."""
|
|
|
|
@abstractmethod
|
|
def build_main_agent_name(self, gateway: Gateway) -> str:
|
|
raise NotImplementedError
|
|
|
|
@abstractmethod
|
|
def build_identity_profile(self) -> dict[str, str]:
|
|
raise NotImplementedError
|
|
|
|
|
|
class DefaultGatewayMainAgentManager(AbstractGatewayMainAgentManager):
|
|
"""Default naming/profile strategy for gateway-main agents."""
|
|
|
|
def build_main_agent_name(self, gateway: Gateway) -> str:
|
|
return f"{gateway.name} Gateway Agent"
|
|
|
|
def build_identity_profile(self) -> dict[str, str]:
|
|
return {
|
|
"role": "Gateway Agent",
|
|
"communication_style": "direct, concise, practical",
|
|
"emoji": ":compass:",
|
|
}
|
|
|
|
|
|
class GatewayAdminLifecycleService(OpenClawDBService):
|
|
"""Write-side gateway lifecycle service (CRUD, main agent, template sync)."""
|
|
|
|
def __init__(
|
|
self,
|
|
session: AsyncSession,
|
|
*,
|
|
main_agent_manager: AbstractGatewayMainAgentManager | None = None,
|
|
) -> None:
|
|
super().__init__(session)
|
|
self._main_agent_manager = main_agent_manager or DefaultGatewayMainAgentManager()
|
|
|
|
@property
|
|
def main_agent_manager(self) -> AbstractGatewayMainAgentManager:
|
|
return self._main_agent_manager
|
|
|
|
@main_agent_manager.setter
|
|
def main_agent_manager(self, value: AbstractGatewayMainAgentManager) -> None:
|
|
self._main_agent_manager = value
|
|
|
|
async def require_gateway(
|
|
self,
|
|
*,
|
|
gateway_id: UUID,
|
|
organization_id: UUID,
|
|
) -> Gateway:
|
|
gateway = (
|
|
await Gateway.objects.by_id(gateway_id)
|
|
.filter(col(Gateway.organization_id) == organization_id)
|
|
.first(self.session)
|
|
)
|
|
if gateway is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Gateway not found",
|
|
)
|
|
return gateway
|
|
|
|
async def find_main_agent(self, gateway: Gateway) -> Agent | None:
|
|
return (
|
|
await Agent.objects.filter_by(gateway_id=gateway.id)
|
|
.filter(col(Agent.board_id).is_(None))
|
|
.first(self.session)
|
|
)
|
|
|
|
async def upsert_main_agent_record(self, gateway: Gateway) -> tuple[Agent, bool]:
|
|
changed = False
|
|
session_key = GatewayAgentIdentity.session_key(gateway)
|
|
agent = await self.find_main_agent(gateway)
|
|
main_agent_name = self.main_agent_manager.build_main_agent_name(gateway)
|
|
identity_profile = self.main_agent_manager.build_identity_profile()
|
|
if agent is None:
|
|
agent = Agent(
|
|
name=main_agent_name,
|
|
status="provisioning",
|
|
board_id=None,
|
|
gateway_id=gateway.id,
|
|
is_board_lead=False,
|
|
openclaw_session_id=session_key,
|
|
heartbeat_config=DEFAULT_HEARTBEAT_CONFIG.copy(),
|
|
identity_profile=identity_profile,
|
|
)
|
|
self.session.add(agent)
|
|
changed = True
|
|
if agent.board_id is not None:
|
|
agent.board_id = None
|
|
changed = True
|
|
if agent.gateway_id != gateway.id:
|
|
agent.gateway_id = gateway.id
|
|
changed = True
|
|
if agent.is_board_lead:
|
|
agent.is_board_lead = False
|
|
changed = True
|
|
if agent.name != main_agent_name:
|
|
agent.name = main_agent_name
|
|
changed = True
|
|
if agent.openclaw_session_id != session_key:
|
|
agent.openclaw_session_id = session_key
|
|
changed = True
|
|
if agent.heartbeat_config is None:
|
|
agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy()
|
|
changed = True
|
|
if agent.identity_profile is None:
|
|
agent.identity_profile = identity_profile
|
|
changed = True
|
|
if not agent.status:
|
|
agent.status = "provisioning"
|
|
changed = True
|
|
if changed:
|
|
agent.updated_at = utcnow()
|
|
self.session.add(agent)
|
|
return agent, changed
|
|
|
|
async def gateway_has_main_agent_entry(self, gateway: Gateway) -> bool:
|
|
if not gateway.url:
|
|
return False
|
|
config = GatewayClientConfig(
|
|
url=gateway.url,
|
|
token=gateway.token,
|
|
allow_insecure_tls=gateway.allow_insecure_tls,
|
|
disable_device_pairing=gateway.disable_device_pairing,
|
|
)
|
|
target_id = GatewayAgentIdentity.openclaw_agent_id(gateway)
|
|
try:
|
|
await openclaw_call("agents.files.list", {"agentId": target_id}, config=config)
|
|
except OpenClawGatewayError as exc:
|
|
message = str(exc).lower()
|
|
if any(marker in message for marker in ("not found", "unknown agent", "no such agent")):
|
|
return False
|
|
return True
|
|
return True
|
|
|
|
async def assert_gateway_runtime_compatible(
|
|
self,
|
|
*,
|
|
url: str,
|
|
token: str | None,
|
|
allow_insecure_tls: bool = False,
|
|
disable_device_pairing: bool = False,
|
|
) -> None:
|
|
"""Validate that a gateway runtime meets minimum supported version."""
|
|
config = GatewayClientConfig(
|
|
url=url,
|
|
token=token,
|
|
allow_insecure_tls=allow_insecure_tls,
|
|
disable_device_pairing=disable_device_pairing,
|
|
)
|
|
try:
|
|
result = await check_gateway_version_compatibility(config)
|
|
except OpenClawGatewayError as exc:
|
|
detail = normalize_gateway_error_message(str(exc))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
detail=f"Gateway compatibility check failed: {detail}",
|
|
) from exc
|
|
if not result.compatible:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
|
detail=result.message or "Gateway runtime version is not supported.",
|
|
)
|
|
|
|
async def provision_main_agent_record(
|
|
self,
|
|
gateway: Gateway,
|
|
agent: Agent,
|
|
*,
|
|
user: User | None,
|
|
action: str,
|
|
notify: bool,
|
|
) -> Agent:
|
|
orchestrator = AgentLifecycleOrchestrator(self.session)
|
|
try:
|
|
provisioned = await orchestrator.run_lifecycle(
|
|
gateway=gateway,
|
|
agent_id=agent.id,
|
|
board=None,
|
|
user=user,
|
|
action=action,
|
|
auth_token=None,
|
|
force_bootstrap=False,
|
|
reset_session=False,
|
|
wake=notify,
|
|
deliver_wakeup=True,
|
|
wakeup_verb=None,
|
|
clear_confirm_token=False,
|
|
raise_gateway_errors=True,
|
|
)
|
|
except HTTPException:
|
|
self.logger.error(
|
|
"gateway.main_agent.provision_failed gateway_id=%s agent_id=%s action=%s",
|
|
gateway.id,
|
|
agent.id,
|
|
action,
|
|
)
|
|
raise
|
|
self.logger.info(
|
|
"gateway.main_agent.provision_success gateway_id=%s agent_id=%s action=%s",
|
|
gateway.id,
|
|
provisioned.id,
|
|
action,
|
|
)
|
|
return provisioned
|
|
|
|
async def ensure_main_agent(
|
|
self,
|
|
gateway: Gateway,
|
|
auth: AuthContext,
|
|
*,
|
|
action: str = "provision",
|
|
) -> Agent:
|
|
self.logger.log(
|
|
TRACE_LEVEL,
|
|
"gateway.main_agent.ensure.start gateway_id=%s action=%s",
|
|
gateway.id,
|
|
action,
|
|
)
|
|
agent, _ = await self.upsert_main_agent_record(gateway)
|
|
return await self.provision_main_agent_record(
|
|
gateway,
|
|
agent,
|
|
user=auth.user,
|
|
action=action,
|
|
notify=True,
|
|
)
|
|
|
|
async def ensure_gateway_agents_exist(self, gateways: list[Gateway]) -> None:
|
|
for gateway in gateways:
|
|
agent, gateway_changed = await self.upsert_main_agent_record(gateway)
|
|
has_gateway_entry = await self.gateway_has_main_agent_entry(gateway)
|
|
needs_provision = (
|
|
gateway_changed or not bool(agent.agent_token_hash) or not has_gateway_entry
|
|
)
|
|
if needs_provision:
|
|
await self.provision_main_agent_record(
|
|
gateway,
|
|
agent,
|
|
user=None,
|
|
action="provision",
|
|
notify=False,
|
|
)
|
|
|
|
async def preview_gateway_agent_import(
|
|
self,
|
|
*,
|
|
gateway: Gateway,
|
|
) -> GatewayAgentImportPreviewResponse:
|
|
"""Return runtime agents that can be imported into Pipeline metadata."""
|
|
config = GatewayClientConfig(
|
|
url=gateway.url,
|
|
token=gateway.token,
|
|
allow_insecure_tls=gateway.allow_insecure_tls,
|
|
disable_device_pairing=gateway.disable_device_pairing,
|
|
)
|
|
try:
|
|
runtime_agents_raw = await openclaw_call("agents.list", config=config)
|
|
runtime_sessions_raw = await openclaw_call(
|
|
"sessions.list",
|
|
{"limit": 500},
|
|
config=config,
|
|
)
|
|
except OpenClawGatewayError as exc:
|
|
detail = normalize_gateway_error_message(str(exc))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|
detail=f"Gateway runtime query failed: {detail}",
|
|
) from exc
|
|
|
|
runtime_agents = _as_dict_list(runtime_agents_raw)
|
|
runtime_sessions = _as_dict_list(runtime_sessions_raw)
|
|
|
|
session_by_agent_key: dict[str, str] = {}
|
|
for runtime_session in runtime_sessions:
|
|
key = runtime_session.get("key")
|
|
if not isinstance(key, str):
|
|
continue
|
|
parts = key.split(":")
|
|
if len(parts) < 2 or parts[0] != "agent":
|
|
continue
|
|
session_by_agent_key[parts[1]] = key
|
|
|
|
existing_agents = await Agent.objects.filter_by(gateway_id=gateway.id).all(self.session)
|
|
existing_by_session_key = {
|
|
(agent.openclaw_session_id or "").strip(): agent
|
|
for agent in existing_agents
|
|
if (agent.openclaw_session_id or "").strip()
|
|
}
|
|
existing_by_runtime_key = {
|
|
resolve_agent_key(agent): agent
|
|
for agent in existing_agents
|
|
}
|
|
|
|
lead_board_ids: set[UUID] = set()
|
|
normalized_runtime_agents: list[tuple[str, str | None, str]] = []
|
|
for runtime_agent in runtime_agents:
|
|
runtime_id = runtime_agent.get("id")
|
|
if not isinstance(runtime_id, str) or not runtime_id.strip():
|
|
continue
|
|
runtime_key = runtime_id.strip()
|
|
runtime_name = runtime_agent.get("name")
|
|
normalized_name = runtime_name.strip() if isinstance(runtime_name, str) else None
|
|
session_key = _derive_session_key_for_runtime_agent(runtime_key, session_by_agent_key)
|
|
lead_board_id = _parse_lead_board_id(session_key)
|
|
if lead_board_id is not None:
|
|
lead_board_ids.add(lead_board_id)
|
|
normalized_runtime_agents.append((runtime_key, normalized_name, session_key))
|
|
|
|
board_by_id: dict[UUID, Board] = {}
|
|
if lead_board_ids:
|
|
board_rows = (
|
|
await self.session.exec(
|
|
select(Board).where(
|
|
col(Board.id).in_(lead_board_ids),
|
|
col(Board.organization_id) == gateway.organization_id,
|
|
col(Board.gateway_id) == gateway.id,
|
|
)
|
|
)
|
|
).all()
|
|
board_by_id = {board.id: board for board in board_rows}
|
|
|
|
candidates: list[GatewayAgentImportCandidate] = []
|
|
for runtime_id, runtime_name, session_key in normalized_runtime_agents:
|
|
tracked_agent = existing_by_session_key.get(session_key)
|
|
if tracked_agent is None:
|
|
tracked_agent = existing_by_runtime_key.get(runtime_id)
|
|
lead_board_id = _parse_lead_board_id(session_key)
|
|
inferred_board_id = (
|
|
lead_board_id if lead_board_id is not None and lead_board_id in board_by_id else None
|
|
)
|
|
inferred_is_board_lead = inferred_board_id is not None
|
|
candidates.append(
|
|
GatewayAgentImportCandidate(
|
|
gateway_agent_id=runtime_id,
|
|
gateway_agent_name=runtime_name,
|
|
session_key=session_key,
|
|
existing_agent_id=tracked_agent.id if tracked_agent is not None else None,
|
|
importable=tracked_agent is None,
|
|
inferred_board_id=inferred_board_id,
|
|
inferred_is_board_lead=inferred_is_board_lead,
|
|
)
|
|
)
|
|
|
|
candidates.sort(key=lambda item: item.gateway_agent_id.lower())
|
|
discovered_count = len(candidates)
|
|
importable_count = sum(1 for item in candidates if item.importable)
|
|
already_tracked_count = discovered_count - importable_count
|
|
return GatewayAgentImportPreviewResponse(
|
|
gateway_id=gateway.id,
|
|
discovered_count=discovered_count,
|
|
importable_count=importable_count,
|
|
already_tracked_count=already_tracked_count,
|
|
candidates=candidates,
|
|
)
|
|
|
|
async def import_gateway_agents(
|
|
self,
|
|
*,
|
|
gateway: Gateway,
|
|
gateway_agent_ids: list[str],
|
|
reconcile_after_import: bool = False,
|
|
rotate_tokens: bool = True,
|
|
reset_sessions: bool = True,
|
|
force_bootstrap: bool = True,
|
|
) -> GatewayAgentImportResponse:
|
|
"""Import selected runtime agents as Pipeline agent rows."""
|
|
preview = await self.preview_gateway_agent_import(gateway=gateway)
|
|
candidates_by_runtime_id = {
|
|
item.gateway_agent_id: item
|
|
for item in preview.candidates
|
|
}
|
|
seen: set[str] = set()
|
|
deduped_ids: list[str] = []
|
|
for runtime_id in gateway_agent_ids:
|
|
cleaned = runtime_id.strip()
|
|
if not cleaned or cleaned in seen:
|
|
continue
|
|
seen.add(cleaned)
|
|
deduped_ids.append(cleaned)
|
|
|
|
imported_agent_ids: list[UUID] = []
|
|
skipped_gateway_agent_ids: list[str] = []
|
|
for runtime_id in deduped_ids:
|
|
candidate = candidates_by_runtime_id.get(runtime_id)
|
|
if candidate is None or not candidate.importable:
|
|
skipped_gateway_agent_ids.append(runtime_id)
|
|
continue
|
|
imported = Agent(
|
|
name=candidate.gateway_agent_name or f"Imported {runtime_id}",
|
|
board_id=candidate.inferred_board_id,
|
|
gateway_id=gateway.id,
|
|
status="offline",
|
|
heartbeat_config=DEFAULT_HEARTBEAT_CONFIG.copy(),
|
|
openclaw_session_id=candidate.session_key,
|
|
is_board_lead=candidate.inferred_is_board_lead,
|
|
)
|
|
self.session.add(imported)
|
|
await self.session.flush()
|
|
imported_agent_ids.append(imported.id)
|
|
|
|
await self.session.commit()
|
|
reconcile_summary: GatewayAgentImportReconcileSummary | None = None
|
|
if reconcile_after_import and imported_agent_ids:
|
|
reconcile_summary = await self.reconcile_imported_agents(
|
|
gateway=gateway,
|
|
agent_ids=imported_agent_ids,
|
|
rotate_tokens=rotate_tokens,
|
|
reset_sessions=reset_sessions,
|
|
force_bootstrap=force_bootstrap,
|
|
)
|
|
|
|
return GatewayAgentImportResponse(
|
|
gateway_id=gateway.id,
|
|
imported_count=len(imported_agent_ids),
|
|
skipped_count=len(skipped_gateway_agent_ids),
|
|
imported_agent_ids=imported_agent_ids,
|
|
skipped_gateway_agent_ids=skipped_gateway_agent_ids,
|
|
reconcile=reconcile_summary,
|
|
)
|
|
|
|
async def reconcile_imported_agents(
|
|
self,
|
|
*,
|
|
gateway: Gateway,
|
|
agent_ids: list[UUID],
|
|
rotate_tokens: bool,
|
|
reset_sessions: bool,
|
|
force_bootstrap: bool,
|
|
) -> GatewayAgentImportReconcileSummary:
|
|
"""Run post-import lifecycle reconcile for imported agent rows."""
|
|
if not agent_ids:
|
|
return GatewayAgentImportReconcileSummary(
|
|
attempted=0,
|
|
updated=0,
|
|
skipped=0,
|
|
errors=[],
|
|
)
|
|
|
|
agents = (
|
|
await self.session.exec(
|
|
select(Agent).where(
|
|
col(Agent.id).in_(agent_ids),
|
|
col(Agent.gateway_id) == gateway.id,
|
|
)
|
|
)
|
|
).all()
|
|
boards_by_id: dict[UUID, Board] = {}
|
|
board_ids = [agent.board_id for agent in agents if agent.board_id is not None]
|
|
if board_ids:
|
|
board_rows = (
|
|
await self.session.exec(
|
|
select(Board).where(
|
|
col(Board.id).in_(board_ids),
|
|
col(Board.gateway_id) == gateway.id,
|
|
col(Board.organization_id) == gateway.organization_id,
|
|
)
|
|
)
|
|
).all()
|
|
boards_by_id = {board.id: board for board in board_rows}
|
|
|
|
updated = 0
|
|
skipped = 0
|
|
errors: list[GatewayAgentReconcileError] = []
|
|
main_session_key = GatewayAgentIdentity.session_key(gateway)
|
|
for agent in agents:
|
|
board: Board | None = None
|
|
if agent.board_id is not None:
|
|
board = boards_by_id.get(agent.board_id)
|
|
if board is None:
|
|
skipped += 1
|
|
errors.append(
|
|
GatewayAgentReconcileError(
|
|
agent_id=agent.id,
|
|
message="Skipping reconcile: board is missing or not mapped to gateway.",
|
|
)
|
|
)
|
|
continue
|
|
elif (agent.openclaw_session_id or "").strip() != main_session_key:
|
|
skipped += 1
|
|
errors.append(
|
|
GatewayAgentReconcileError(
|
|
agent_id=agent.id,
|
|
message=(
|
|
"Skipping reconcile: boardless imported agent is not the gateway-main "
|
|
"session and cannot be reprovisioned automatically."
|
|
),
|
|
)
|
|
)
|
|
continue
|
|
|
|
if not rotate_tokens:
|
|
skipped += 1
|
|
errors.append(
|
|
GatewayAgentReconcileError(
|
|
agent_id=agent.id,
|
|
message=(
|
|
"Skipping reconcile: rotate_tokens=false is unsupported for imported "
|
|
"agents without an existing backend token hash."
|
|
),
|
|
)
|
|
)
|
|
continue
|
|
|
|
try:
|
|
await AgentLifecycleOrchestrator(self.session).run_lifecycle(
|
|
gateway=gateway,
|
|
agent_id=agent.id,
|
|
board=board,
|
|
user=None,
|
|
action="update",
|
|
auth_token=None,
|
|
force_bootstrap=force_bootstrap,
|
|
reset_session=reset_sessions,
|
|
wake=False,
|
|
deliver_wakeup=False,
|
|
wakeup_verb="updated",
|
|
clear_confirm_token=False,
|
|
raise_gateway_errors=True,
|
|
)
|
|
except HTTPException as exc:
|
|
skipped += 1
|
|
message = exc.detail if isinstance(exc.detail, str) else str(exc.detail)
|
|
errors.append(
|
|
GatewayAgentReconcileError(
|
|
agent_id=agent.id,
|
|
message=f"Reconcile failed: {message}",
|
|
)
|
|
)
|
|
except Exception as exc:
|
|
skipped += 1
|
|
errors.append(
|
|
GatewayAgentReconcileError(
|
|
agent_id=agent.id,
|
|
message=f"Reconcile failed: {exc}",
|
|
)
|
|
)
|
|
else:
|
|
updated += 1
|
|
|
|
return GatewayAgentImportReconcileSummary(
|
|
attempted=len(agents),
|
|
updated=updated,
|
|
skipped=skipped,
|
|
errors=errors,
|
|
)
|
|
|
|
async def clear_agent_foreign_keys(self, *, agent_id: UUID) -> None:
|
|
now = utcnow()
|
|
await crud.update_where(
|
|
self.session,
|
|
Task,
|
|
col(Task.assigned_agent_id) == agent_id,
|
|
col(Task.status) == "in_progress",
|
|
assigned_agent_id=None,
|
|
status="inbox",
|
|
in_progress_at=None,
|
|
updated_at=now,
|
|
commit=False,
|
|
)
|
|
await crud.update_where(
|
|
self.session,
|
|
Task,
|
|
col(Task.assigned_agent_id) == agent_id,
|
|
col(Task.status) != "in_progress",
|
|
assigned_agent_id=None,
|
|
updated_at=now,
|
|
commit=False,
|
|
)
|
|
await crud.update_where(
|
|
self.session,
|
|
ActivityEvent,
|
|
col(ActivityEvent.agent_id) == agent_id,
|
|
agent_id=None,
|
|
commit=False,
|
|
)
|
|
await crud.update_where(
|
|
self.session,
|
|
Approval,
|
|
col(Approval.agent_id) == agent_id,
|
|
agent_id=None,
|
|
commit=False,
|
|
)
|
|
await crud.update_where(
|
|
self.session,
|
|
BoardWebhook,
|
|
col(BoardWebhook.agent_id) == agent_id,
|
|
agent_id=None,
|
|
updated_at=now,
|
|
commit=False,
|
|
)
|
|
|
|
async def sync_templates(
|
|
self,
|
|
gateway: Gateway,
|
|
*,
|
|
query: GatewayTemplateSyncQuery,
|
|
auth: AuthContext,
|
|
) -> GatewayTemplatesSyncResult:
|
|
self.logger.log(
|
|
TRACE_LEVEL,
|
|
"gateway.templates.sync.start gateway_id=%s include_main=%s",
|
|
gateway.id,
|
|
query.include_main,
|
|
)
|
|
await self.ensure_gateway_agents_exist([gateway])
|
|
result = await OpenClawProvisioningService(self.session).sync_gateway_templates(
|
|
gateway,
|
|
GatewayTemplateSyncOptions(
|
|
user=auth.user,
|
|
include_main=query.include_main,
|
|
lead_only=query.lead_only,
|
|
reset_sessions=query.reset_sessions,
|
|
rotate_tokens=query.rotate_tokens,
|
|
force_bootstrap=query.force_bootstrap,
|
|
overwrite=query.overwrite,
|
|
board_id=query.board_id,
|
|
),
|
|
)
|
|
self.logger.info("gateway.templates.sync.success gateway_id=%s", gateway.id)
|
|
return result
|