refactor: replace direct calls to provisioning functions with OpenClawProvisioningService methods

This commit is contained in:
Abhimanyu Saharan
2026-02-10 22:30:14 +05:30
parent d9199f8d8d
commit ad75871e61
12 changed files with 703 additions and 844 deletions

View File

@@ -17,9 +17,7 @@ from app.db import crud
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import (
OpenClawGatewayError,
ensure_session,
openclaw_call,
send_message,
)
from app.models.activity_events import ActivityEvent
from app.models.agents import Agent
@@ -30,13 +28,11 @@ from app.schemas.gateways import GatewayTemplatesSyncResult
from app.services.openclaw.constants import DEFAULT_HEARTBEAT_CONFIG
from app.services.openclaw.provisioning import (
GatewayTemplateSyncOptions,
MainAgentProvisionRequest,
ProvisionOptions,
provision_main_agent,
sync_gateway_templates,
OpenClawProvisioningService,
)
from app.services.openclaw.session_service import GatewayTemplateSyncQuery
from app.services.openclaw.shared import GatewayAgentIdentity
from app.services.organizations import get_org_owner_user
if TYPE_CHECKING:
from sqlmodel.ext.asyncio.session import AsyncSession
@@ -132,30 +128,6 @@ class GatewayAdminLifecycleService:
.first(self.session)
)
@staticmethod
def extract_agent_id_from_entry(item: object) -> str | None:
if isinstance(item, str):
value = item.strip()
return value or None
if not isinstance(item, dict):
return None
for key in ("id", "agentId", "agent_id"):
raw = item.get(key)
if isinstance(raw, str) and raw.strip():
return raw.strip()
return None
@staticmethod
def extract_agents_list(payload: object) -> list[object]:
if isinstance(payload, list):
return [item for item in payload]
if not isinstance(payload, dict):
return []
agents = payload.get("agents") or []
if not isinstance(agents, list):
return []
return [item for item in agents]
async def upsert_main_agent_record(self, gateway: Gateway) -> tuple[Agent, bool]:
changed = False
session_key = GatewayAgentIdentity.session_key(gateway)
@@ -210,13 +182,13 @@ class GatewayAdminLifecycleService:
config = GatewayClientConfig(url=gateway.url, token=gateway.token)
target_id = GatewayAgentIdentity.openclaw_agent_id(gateway)
try:
payload = await openclaw_call("agents.list", config=config)
except OpenClawGatewayError:
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
for item in self.extract_agents_list(payload):
if self.extract_agent_id_from_entry(item) == target_id:
return True
return False
return True
async def provision_main_agent_record(
self,
@@ -227,7 +199,10 @@ class GatewayAdminLifecycleService:
action: str,
notify: bool,
) -> Agent:
session_key = GatewayAgentIdentity.session_key(gateway)
template_user = user or await get_org_owner_user(
self.session,
organization_id=gateway.organization_id,
)
raw_token = generate_agent_token()
agent.agent_token_hash = hash_agent_token(raw_token)
agent.provision_requested_at = utcnow()
@@ -241,33 +216,16 @@ class GatewayAdminLifecycleService:
if not gateway.url:
return agent
try:
await provision_main_agent(
agent,
MainAgentProvisionRequest(
gateway=gateway,
auth_token=raw_token,
user=user,
session_key=session_key,
options=ProvisionOptions(action=action),
),
await OpenClawProvisioningService().apply_agent_lifecycle(
agent=agent,
gateway=gateway,
board=None,
auth_token=raw_token,
user=template_user,
action=action,
wake=notify,
deliver_wakeup=True,
)
await ensure_session(
session_key,
config=GatewayClientConfig(url=gateway.url, token=gateway.token),
label=agent.name,
)
if notify:
await send_message(
(
f"Hello {agent.name}. Your gateway provisioning was updated.\n\n"
"Please re-read AGENTS.md, USER.md, HEARTBEAT.md, and TOOLS.md. "
"If BOOTSTRAP.md exists, run it once then delete it. "
"Begin heartbeats after startup."
),
session_key=session_key,
config=GatewayClientConfig(url=gateway.url, token=gateway.token),
deliver=True,
)
self.logger.info(
"gateway.main_agent.provision_success gateway_id=%s agent_id=%s action=%s",
gateway.id,
@@ -388,8 +346,7 @@ class GatewayAdminLifecycleService:
query.include_main,
)
await self.ensure_gateway_agents_exist([gateway])
result = await sync_gateway_templates(
self.session,
result = await OpenClawProvisioningService(self.session).sync_gateway_templates(
gateway,
GatewayTemplateSyncOptions(
user=auth.user,

View File

@@ -49,15 +49,7 @@ from app.services.openclaw.constants import (
OFFLINE_AFTER,
)
from app.services.openclaw.policies import OpenClawAuthorizationPolicy
from app.services.openclaw.provisioning import (
AgentProvisionRequest,
MainAgentProvisionRequest,
ProvisionOptions,
cleanup_agent,
cleanup_main_agent,
provision_agent,
provision_main_agent,
)
from app.services.openclaw.provisioning import OpenClawProvisioningService
from app.services.openclaw.shared import GatewayAgentIdentity
from app.services.organizations import (
OrganizationContext,
@@ -176,11 +168,6 @@ class AbstractProvisionExecution(ABC):
)
try:
await self._provision()
await self._service.send_wakeup_message(
self.agent,
self.request.target.client_config,
verb=self._wakeup_verb,
)
self.agent.provision_confirm_token_hash = None
self.agent.provision_requested_at = None
self.agent.provision_action = None
@@ -256,19 +243,18 @@ class BoardAgentProvisionExecution(AbstractProvisionExecution):
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="board is required for non-main agent provisioning",
)
await provision_agent(
self.agent,
AgentProvisionRequest(
board=board,
gateway=self.request.target.gateway,
auth_token=self.request.raw_token,
user=self.request.user,
options=ProvisionOptions(
action=self._action,
force_bootstrap=self.request.force_bootstrap,
reset_session=True,
),
),
await OpenClawProvisioningService().apply_agent_lifecycle(
agent=self.agent,
gateway=self.request.target.gateway,
board=board,
auth_token=self.request.raw_token,
user=self.request.user,
action=self._action,
force_bootstrap=self.request.force_bootstrap,
reset_session=True,
wake=True,
deliver_wakeup=True,
wakeup_verb=self._wakeup_verb,
)
@@ -276,19 +262,18 @@ class MainAgentProvisionExecution(AbstractProvisionExecution):
"""Provision execution for gateway-main agents."""
async def _provision(self) -> None:
await provision_main_agent(
self.agent,
MainAgentProvisionRequest(
gateway=self.request.target.gateway,
auth_token=self.request.raw_token,
user=self.request.user,
session_key=self.agent.openclaw_session_id,
options=ProvisionOptions(
action=self._action,
force_bootstrap=self.request.force_bootstrap,
reset_session=True,
),
),
await OpenClawProvisioningService().apply_agent_lifecycle(
agent=self.agent,
gateway=self.request.target.gateway,
board=None,
auth_token=self.request.raw_token,
user=self.request.user,
action=self._action,
force_bootstrap=self.request.force_bootstrap,
reset_session=True,
wake=True,
deliver_wakeup=True,
wakeup_verb=self._wakeup_verb,
)
@@ -337,8 +322,25 @@ class AgentLifecycleService:
return slug or uuid4().hex
@classmethod
def build_session_key(cls, agent_name: str) -> str:
return f"{AGENT_SESSION_PREFIX}:{cls.slugify(agent_name)}:main"
def resolve_session_key(cls, agent: Agent) -> str:
"""Resolve the gateway session key for an agent.
Notes:
- For board-scoped agents, default to a UUID-based key to avoid name collisions.
"""
existing = (agent.openclaw_session_id or "").strip()
if agent.board_id is None:
# Gateway-main agents must have an explicit deterministic key (set elsewhere).
if existing:
return existing
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway main agent session key is required",
)
if agent.is_board_lead:
return f"{AGENT_SESSION_PREFIX}:lead-{agent.board_id}:main"
return f"{AGENT_SESSION_PREFIX}:mc-{agent.id}:main"
@classmethod
def workspace_path(cls, agent_name: str, workspace_root: str | None) -> str:
@@ -439,23 +441,6 @@ class AgentLifecycleService:
return None
return await Gateway.objects.by_id(agent.gateway_id).first(self.session)
async def ensure_gateway_session(
self,
agent_name: str,
config: GatewayClientConfig,
) -> tuple[str, str | None]:
session_key = self.build_session_key(agent_name)
try:
await ensure_session(session_key, config=config, label=agent_name)
except OpenClawGatewayError as exc:
self.logger.warning(
"agent.session.ensure_failed agent_name=%s error=%s",
agent_name,
str(exc),
)
return session_key, str(exc)
return session_key, None
@classmethod
def with_computed_status(cls, agent: Agent) -> Agent:
now = utcnow()
@@ -607,30 +592,11 @@ class AgentLifecycleService:
detail="An agent with this name already exists in this gateway workspace.",
)
desired_session_key = self.build_session_key(requested_name)
existing_session_key = (
await self.session.exec(
select(Agent)
.join(Board, col(Agent.board_id) == col(Board.id))
.where(col(Board.gateway_id) == gateway.id)
.where(col(Agent.openclaw_session_id) == desired_session_key),
)
).first()
if existing_session_key:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=(
"This agent name would collide with an existing workspace "
"session key. Pick a different name."
),
)
async def persist_new_agent(
self,
*,
data: dict[str, Any],
client_config: GatewayClientConfig,
) -> tuple[Agent, str, str | None]:
) -> tuple[Agent, str]:
agent = Agent.model_validate(data)
agent.status = "provisioning"
raw_token = generate_agent_token()
@@ -639,58 +605,21 @@ class AgentLifecycleService:
agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy()
agent.provision_requested_at = utcnow()
agent.provision_action = "provision"
session_key, session_error = await self.ensure_gateway_session(
agent.name,
client_config,
)
agent.openclaw_session_id = session_key
agent.openclaw_session_id = self.resolve_session_key(agent)
self.session.add(agent)
await self.session.commit()
await self.session.refresh(agent)
return agent, raw_token, session_error
async def record_session_creation(
self,
*,
agent: Agent,
session_error: str | None,
) -> None:
if session_error:
record_activity(
self.session,
event_type="agent.session.failed",
message=f"Session sync failed for {agent.name}: {session_error}",
agent_id=agent.id,
)
else:
record_activity(
self.session,
event_type="agent.session.created",
message=f"Session created for {agent.name}.",
agent_id=agent.id,
)
await self.session.commit()
async def send_wakeup_message(
self,
agent: Agent,
config: GatewayClientConfig,
verb: str = "provisioned",
) -> None:
session_key = agent.openclaw_session_id or self.build_session_key(agent.name)
await ensure_session(session_key, config=config, label=agent.name)
message = (
f"Hello {agent.name}. Your workspace has been {verb}.\n\n"
"Start the agent, run BOOT.md, and if BOOTSTRAP.md exists run it once "
"then delete it. Begin heartbeats after startup."
)
await send_message(message, session_key=session_key, config=config, deliver=True)
return agent, raw_token
async def provision_new_agent(
self,
*,
agent: Agent,
request: AgentProvisionRequest,
board: Board,
gateway: Gateway,
auth_token: str,
user: User | None,
force_bootstrap: bool,
client_config: GatewayClientConfig,
) -> None:
execution = BoardAgentProvisionExecution(
@@ -699,13 +628,13 @@ class AgentLifecycleService:
provision_request=AgentUpdateProvisionRequest(
target=AgentUpdateProvisionTarget(
is_main_agent=False,
board=request.board,
gateway=request.gateway,
board=board,
gateway=gateway,
client_config=client_config,
),
raw_token=request.auth_token,
user=request.user,
force_bootstrap=request.options.force_bootstrap,
raw_token=auth_token,
user=user,
force_bootstrap=force_bootstrap,
),
action="provision",
wakeup_verb="provisioned",
@@ -852,24 +781,6 @@ class AgentLifecycleService:
client_config=client_config,
)
async def ensure_agent_update_session(
self,
*,
agent: Agent,
client_config: GatewayClientConfig,
) -> None:
session_key = agent.openclaw_session_id or self.build_session_key(agent.name)
try:
await ensure_session(session_key, config=client_config, label=agent.name)
if not agent.openclaw_session_id:
agent.openclaw_session_id = session_key
self.session.add(agent)
await self.session.commit()
await self.session.refresh(agent)
except OpenClawGatewayError as exc:
self.record_instruction_failure(self.session, agent, str(exc), "update")
await self.session.commit()
@staticmethod
def mark_agent_update_pending(agent: Agent) -> str:
raw_token = generate_agent_token()
@@ -937,23 +848,14 @@ class AgentLifecycleService:
"gateway_id": gateway.id,
"heartbeat_config": DEFAULT_HEARTBEAT_CONFIG.copy(),
}
agent, raw_token, session_error = await self.persist_new_agent(
data=data,
client_config=client_config,
)
await self.record_session_creation(
agent=agent,
session_error=session_error,
)
agent, raw_token = await self.persist_new_agent(data=data)
await self.provision_new_agent(
agent=agent,
request=AgentProvisionRequest(
board=board,
gateway=gateway,
auth_token=raw_token,
user=actor.user,
options=ProvisionOptions(action="provision"),
),
board=board,
gateway=gateway,
auth_token=raw_token,
user=actor.user,
force_bootstrap=False,
client_config=client_config,
)
return agent
@@ -987,13 +889,11 @@ class AgentLifecycleService:
gateway, client_config = await self.require_gateway(board)
await self.provision_new_agent(
agent=agent,
request=AgentProvisionRequest(
board=board,
gateway=gateway,
auth_token=raw_token,
user=user,
options=ProvisionOptions(action="provision"),
),
board=board,
gateway=gateway,
auth_token=raw_token,
user=user,
force_bootstrap=False,
client_config=client_config,
)
@@ -1003,24 +903,17 @@ class AgentLifecycleService:
agent: Agent,
actor: ActorContextLike,
) -> None:
if agent.openclaw_session_id:
_ = actor
if agent.board_id is None:
return
board = await self.require_board(
str(agent.board_id) if agent.board_id else None,
user=actor.user if actor.actor_type == "user" else None,
write=actor.actor_type == "user",
)
_, client_config = await self.require_gateway(board)
session_key, session_error = await self.ensure_gateway_session(
agent.name,
client_config,
)
agent.openclaw_session_id = session_key
desired = self.resolve_session_key(agent)
existing = (agent.openclaw_session_id or "").strip()
if existing == desired:
return
agent.openclaw_session_id = desired
self.session.add(agent)
await self.record_session_creation(
agent=agent,
session_error=session_error,
)
await self.session.commit()
await self.session.refresh(agent)
async def commit_heartbeat(
self,
@@ -1162,24 +1055,14 @@ class AgentLifecycleService:
gateway=gateway,
requested_name=requested_name,
)
agent, raw_token, session_error = await self.persist_new_agent(
data=data,
client_config=client_config,
)
await self.record_session_creation(
agent, raw_token = await self.persist_new_agent(data=data)
await self.provision_new_agent(
agent=agent,
session_error=session_error,
)
provision_request = AgentProvisionRequest(
board=board,
gateway=gateway,
auth_token=raw_token,
user=actor.user if actor.actor_type == "user" else None,
options=ProvisionOptions(action="provision"),
)
await self.provision_new_agent(
agent=agent,
request=provision_request,
force_bootstrap=False,
client_config=client_config,
)
self.logger.info("agent.create.success agent_id=%s board_id=%s", agent.id, board.id)
@@ -1229,10 +1112,6 @@ class AgentLifecycleService:
main_gateway=main_gateway,
gateway_for_main=gateway_for_main,
)
await self.ensure_agent_update_session(
agent=agent,
client_config=target.client_config,
)
raw_token = self.mark_agent_update_pending(agent)
self.session.add(agent)
await self.session.commit()
@@ -1345,7 +1224,10 @@ class AgentLifecycleService:
if gateway and gateway.url:
client_config = GatewayClientConfig(url=gateway.url, token=gateway.token)
try:
workspace_path = await cleanup_main_agent(agent, gateway)
workspace_path = await OpenClawProvisioningService().delete_agent_lifecycle(
agent=agent,
gateway=gateway,
)
except OpenClawGatewayError as exc:
self.record_instruction_failure(self.session, agent, str(exc), "delete")
await self.session.commit()
@@ -1364,7 +1246,10 @@ class AgentLifecycleService:
board = await self.require_board(str(agent.board_id))
gateway, client_config = await self.require_gateway(board)
try:
workspace_path = await cleanup_agent(agent, gateway)
workspace_path = await OpenClawProvisioningService().delete_agent_lifecycle(
agent=agent,
gateway=gateway,
)
except OpenClawGatewayError as exc:
self.record_instruction_failure(self.session, agent, str(exc), "delete")
await self.session.commit()

View File

@@ -39,7 +39,7 @@ from app.services.openclaw.policies import OpenClawAuthorizationPolicy
from app.services.openclaw.provisioning import (
LeadAgentOptions,
LeadAgentRequest,
ensure_board_lead_agent,
OpenClawProvisioningService,
)
from app.services.openclaw.shared import (
GatewayAgentIdentity,
@@ -542,8 +542,7 @@ class GatewayCoordinationService(AbstractGatewayMessagingService):
board: Board,
message: str,
) -> tuple[Agent, bool]:
lead, lead_created = await ensure_board_lead_agent(
self.session,
lead, lead_created = await OpenClawProvisioningService(self.session).ensure_board_lead_agent(
request=LeadAgentRequest(
board=board,
gateway=gateway,

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,23 @@ async def get_member(
).first(session)
async def get_org_owner_user(
session: AsyncSession,
*,
organization_id: UUID,
) -> User | None:
"""Return the org owner User, if one exists."""
owner = (
await OrganizationMember.objects.filter_by(organization_id=organization_id)
.filter(col(OrganizationMember.role) == "owner")
.order_by(col(OrganizationMember.created_at).asc())
.first(session)
)
if owner is None:
return None
return await User.objects.by_id(owner.user_id).first(session)
async def get_first_membership(
session: AsyncSession,
user_id: UUID,