feat(agent): Refactor agent provisioning and cleanup logic for improved functionality

This commit is contained in:
Abhimanyu Saharan
2026-02-05 01:27:48 +05:30
parent 8eb74acb1e
commit 2c24d8993f
4 changed files with 319 additions and 235 deletions

View File

@@ -11,7 +11,6 @@ from sqlmodel import Session, col, select
from app.api.deps import ActorContext, require_admin_auth, require_admin_or_agent from app.api.deps import ActorContext, require_admin_auth, require_admin_or_agent
from app.core.agent_tokens import generate_agent_token, hash_agent_token, verify_agent_token from app.core.agent_tokens import generate_agent_token, hash_agent_token, verify_agent_token
from app.core.auth import AuthContext from app.core.auth import AuthContext
from app.core.config import settings
from app.db.session import get_session from app.db.session import get_session
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
@@ -31,8 +30,8 @@ from app.schemas.agents import (
from app.services.activity_log import record_activity from app.services.activity_log import record_activity
from app.services.agent_provisioning import ( from app.services.agent_provisioning import (
DEFAULT_HEARTBEAT_CONFIG, DEFAULT_HEARTBEAT_CONFIG,
send_provisioning_message, cleanup_agent_direct,
send_update_message, provision_agent,
) )
router = APIRouter(prefix="/agents", tags=["agents"]) router = APIRouter(prefix="/agents", tags=["agents"])
@@ -194,8 +193,6 @@ async def create_agent(
agent.agent_token_hash = hash_agent_token(raw_token) agent.agent_token_hash = hash_agent_token(raw_token)
if agent.heartbeat_config is None: if agent.heartbeat_config is None:
agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy() agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy()
provision_token = generate_agent_token()
agent.provision_confirm_token_hash = hash_agent_token(provision_token)
agent.provision_requested_at = datetime.utcnow() agent.provision_requested_at = datetime.utcnow()
agent.provision_action = "provision" agent.provision_action = "provision"
session_key, session_error = await _ensure_gateway_session(agent.name, client_config) session_key, session_error = await _ensure_gateway_session(agent.name, client_config)
@@ -219,15 +216,27 @@ async def create_agent(
) )
session.commit() session.commit()
try: try:
await send_provisioning_message( await provision_agent(agent, board, gateway, raw_token, auth.user, action="provision")
agent, board, gateway, raw_token, provision_token, auth.user await _send_wakeup_message(agent, client_config, verb="provisioned")
agent.provision_confirm_token_hash = None
agent.provision_requested_at = None
agent.provision_action = None
agent.updated_at = datetime.utcnow()
session.add(agent)
session.commit()
record_activity(
session,
event_type="agent.provision",
message=f"Provisioned directly for {agent.name}.",
agent_id=agent.id,
) )
record_activity( record_activity(
session, session,
event_type="agent.provision.requested", event_type="agent.wakeup.sent",
message=f"Provisioning requested for {agent.name}.", message=f"Wakeup message sent to {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.commit()
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
_record_instruction_failure(session, agent, str(exc), "provision") _record_instruction_failure(session, agent, str(exc), "provision")
session.commit() session.commit()
@@ -295,9 +304,7 @@ async def update_agent(
_record_instruction_failure(session, agent, str(exc), "update") _record_instruction_failure(session, agent, str(exc), "update")
session.commit() session.commit()
raw_token = generate_agent_token() raw_token = generate_agent_token()
provision_token = generate_agent_token()
agent.agent_token_hash = hash_agent_token(raw_token) agent.agent_token_hash = hash_agent_token(raw_token)
agent.provision_confirm_token_hash = hash_agent_token(provision_token)
agent.provision_requested_at = datetime.utcnow() agent.provision_requested_at = datetime.utcnow()
agent.provision_action = "update" agent.provision_action = "update"
agent.status = "updating" agent.status = "updating"
@@ -305,11 +312,25 @@ async def update_agent(
session.commit() session.commit()
session.refresh(agent) session.refresh(agent)
try: try:
await send_update_message(agent, board, gateway, raw_token, provision_token, auth.user) await provision_agent(agent, board, gateway, raw_token, auth.user, action="update")
await _send_wakeup_message(agent, client_config, verb="updated")
agent.provision_confirm_token_hash = None
agent.provision_requested_at = None
agent.provision_action = None
agent.status = "online"
agent.updated_at = datetime.utcnow()
session.add(agent)
session.commit()
record_activity( record_activity(
session, session,
event_type="agent.update.requested", event_type="agent.update.direct",
message=f"Update requested for {agent.name}.", message=f"Updated directly for {agent.name}.",
agent_id=agent.id,
)
record_activity(
session,
event_type="agent.wakeup.sent",
message=f"Wakeup message sent to {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.commit() session.commit()
@@ -367,8 +388,6 @@ async def heartbeat_or_create_agent(
) )
raw_token = generate_agent_token() raw_token = generate_agent_token()
agent.agent_token_hash = hash_agent_token(raw_token) agent.agent_token_hash = hash_agent_token(raw_token)
provision_token = generate_agent_token()
agent.provision_confirm_token_hash = hash_agent_token(provision_token)
agent.provision_requested_at = datetime.utcnow() agent.provision_requested_at = datetime.utcnow()
agent.provision_action = "provision" agent.provision_action = "provision"
session_key, session_error = await _ensure_gateway_session(agent.name, client_config) session_key, session_error = await _ensure_gateway_session(agent.name, client_config)
@@ -392,15 +411,27 @@ async def heartbeat_or_create_agent(
) )
session.commit() session.commit()
try: try:
await send_provisioning_message( await provision_agent(agent, board, gateway, raw_token, actor.user, action="provision")
agent, board, gateway, raw_token, provision_token, actor.user await _send_wakeup_message(agent, client_config, verb="provisioned")
agent.provision_confirm_token_hash = None
agent.provision_requested_at = None
agent.provision_action = None
agent.updated_at = datetime.utcnow()
session.add(agent)
session.commit()
record_activity(
session,
event_type="agent.provision",
message=f"Provisioned directly for {agent.name}.",
agent_id=agent.id,
) )
record_activity( record_activity(
session, session,
event_type="agent.provision.requested", event_type="agent.wakeup.sent",
message=f"Provisioning requested for {agent.name}.", message=f"Wakeup message sent to {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.commit()
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
_record_instruction_failure(session, agent, str(exc), "provision") _record_instruction_failure(session, agent, str(exc), "provision")
session.commit() session.commit()
@@ -414,8 +445,6 @@ async def heartbeat_or_create_agent(
agent.agent_token_hash = hash_agent_token(raw_token) agent.agent_token_hash = hash_agent_token(raw_token)
if agent.heartbeat_config is None: if agent.heartbeat_config is None:
agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy() agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy()
provision_token = generate_agent_token()
agent.provision_confirm_token_hash = hash_agent_token(provision_token)
agent.provision_requested_at = datetime.utcnow() agent.provision_requested_at = datetime.utcnow()
agent.provision_action = "provision" agent.provision_action = "provision"
session.add(agent) session.add(agent)
@@ -424,15 +453,27 @@ async def heartbeat_or_create_agent(
try: try:
board = _require_board(session, str(agent.board_id) if agent.board_id else None) board = _require_board(session, str(agent.board_id) if agent.board_id else None)
gateway, client_config = _require_gateway(session, board) gateway, client_config = _require_gateway(session, board)
await send_provisioning_message( await provision_agent(agent, board, gateway, raw_token, actor.user, action="provision")
agent, board, gateway, raw_token, provision_token, actor.user await _send_wakeup_message(agent, client_config, verb="provisioned")
agent.provision_confirm_token_hash = None
agent.provision_requested_at = None
agent.provision_action = None
agent.updated_at = datetime.utcnow()
session.add(agent)
session.commit()
record_activity(
session,
event_type="agent.provision",
message=f"Provisioned directly for {agent.name}.",
agent_id=agent.id,
) )
record_activity( record_activity(
session, session,
event_type="agent.provision.requested", event_type="agent.wakeup.sent",
message=f"Provisioning requested for {agent.name}.", message=f"Wakeup message sent to {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.commit()
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
_record_instruction_failure(session, agent, str(exc), "provision") _record_instruction_failure(session, agent, str(exc), "provision")
session.commit() session.commit()
@@ -485,61 +526,39 @@ def delete_agent(
return {"ok": True} return {"ok": True}
board = _require_board(session, str(agent.board_id) if agent.board_id else None) board = _require_board(session, str(agent.board_id) if agent.board_id else None)
gateway, client_config = _require_gateway(session, board) gateway, _ = _require_gateway(session, board)
raw_token = generate_agent_token()
agent.delete_confirm_token_hash = hash_agent_token(raw_token)
agent.delete_requested_at = datetime.utcnow()
agent.status = "deleting"
agent.updated_at = datetime.utcnow()
session.add(agent)
record_activity(
session,
event_type="agent.delete.requested",
message=f"Delete requested for {agent.name}.",
agent_id=agent.id,
)
session.commit()
async def _gateway_cleanup_request() -> None:
main_session = gateway.main_session_key
if not main_session:
raise OpenClawGatewayError("Gateway main_session_key is required")
workspace_path = _workspace_path(agent.name, gateway.workspace_root)
base_url = settings.base_url or "REPLACE_WITH_BASE_URL"
cleanup_message = (
"Cleanup request for deleted agent.\n\n"
f"Agent name: {agent.name}\n"
f"Agent id: {agent.id}\n"
f"Session key: {agent.openclaw_session_id or _build_session_key(agent.name)}\n"
f"Workspace path: {workspace_path}\n\n"
"Actions:\n"
"1) Remove the workspace directory.\n"
"2) Delete the agent session from the gateway.\n"
"3) Confirm deletion by calling:\n"
f" POST {base_url}/api/v1/agents/{agent.id}/delete/confirm\n"
' Body: {"token": "' + raw_token + '"}\n'
"Reply NO_REPLY."
)
await ensure_session(main_session, config=client_config, label="Main Agent")
await send_message(
cleanup_message,
session_key=main_session,
config=client_config,
deliver=False,
)
try: try:
import asyncio import asyncio
asyncio.run(_gateway_cleanup_request()) asyncio.run(cleanup_agent_direct(agent, gateway, delete_workspace=True))
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
_record_instruction_failure(session, agent, str(exc), "delete") _record_instruction_failure(session, agent, str(exc), "delete")
session.commit() session.commit()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY, status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"Gateway cleanup request failed: {exc}", detail=f"Gateway cleanup failed: {exc}",
) from exc
except Exception as exc: # pragma: no cover - unexpected cleanup errors
_record_instruction_failure(session, agent, str(exc), "delete")
session.commit()
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Workspace cleanup failed: {exc}",
) from exc ) from exc
record_activity(
session,
event_type="agent.delete.direct",
message=f"Deleted agent {agent.name}.",
agent_id=None,
)
session.execute(
update(ActivityEvent)
.where(col(ActivityEvent.agent_id) == agent.id)
.values(agent_id=None)
)
session.delete(agent)
session.commit()
return {"ok": True} return {"ok": True}

View File

@@ -31,4 +31,5 @@ class Settings(BaseSettings):
log_use_utc: bool = False log_use_utc: bool = False
settings = Settings() settings = Settings()

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import json import json
import re import re
import shutil
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from uuid import uuid4 from uuid import uuid4
@@ -10,25 +11,27 @@ from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoes
from app.core.config import settings from app.core.config import settings
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, openclaw_call
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.models.gateways import Gateway
from app.models.users import User from app.models.users import User
TEMPLATE_FILES = [
"AGENTS.md",
"BOOT.md",
"BOOTSTRAP.md",
"HEARTBEAT.md",
"IDENTITY.md",
"SOUL.md",
"TOOLS.md",
"USER.md",
]
DEFAULT_HEARTBEAT_CONFIG = {"every": "10m", "target": "none"} DEFAULT_HEARTBEAT_CONFIG = {"every": "10m", "target": "none"}
DEFAULT_GATEWAY_FILES = frozenset(
{
"AGENTS.md",
"SOUL.md",
"TOOLS.md",
"IDENTITY.md",
"USER.md",
"HEARTBEAT.md",
"BOOTSTRAP.md",
"MEMORY.md",
}
)
def _repo_root() -> Path: def _repo_root() -> Path:
return Path(__file__).resolve().parents[3] return Path(__file__).resolve().parents[3]
@@ -67,31 +70,6 @@ def _template_env() -> Environment:
) )
def _read_templates(
context: dict[str, str], overrides: dict[str, str] | None = None
) -> dict[str, str]:
env = _template_env()
templates: dict[str, str] = {}
override_map = overrides or {}
for name in TEMPLATE_FILES:
path = _templates_root() / name
override = override_map.get(name)
if override:
templates[name] = env.from_string(override).render(**context).strip()
continue
if not path.exists():
templates[name] = ""
continue
template = env.get_template(name)
templates[name] = template.render(**context).strip()
return templates
def _render_file_block(name: str, content: str) -> str:
body = content if content else f"# {name}\n\nTODO: add content\n"
return f"\n{name}\n```md\n{body}\n```\n"
def _workspace_path(agent_name: str, workspace_root: str) -> str: def _workspace_path(agent_name: str, workspace_root: str) -> str:
if not workspace_root: if not workspace_root:
raise ValueError("gateway_workspace_root is required") raise ValueError("gateway_workspace_root is required")
@@ -100,6 +78,16 @@ def _workspace_path(agent_name: str, workspace_root: str) -> str:
return f"{root}/workspace-{_slugify(agent_name)}" return f"{root}/workspace-{_slugify(agent_name)}"
def _resolve_workspace_dir(workspace_root: str, agent_name: str) -> Path:
if not workspace_root:
raise ValueError("gateway_workspace_root is required")
root = Path(workspace_root).expanduser().resolve()
workspace = Path(_workspace_path(agent_name, workspace_root)).expanduser().resolve()
if workspace == root or root not in workspace.parents:
raise ValueError("workspace path is not under workspace root")
return workspace
def _build_context( def _build_context(
agent: Agent, agent: Agent,
board: Board, board: Board,
@@ -136,152 +124,220 @@ def _build_context(
} }
def _build_file_blocks(context: dict[str, str], agent: Agent) -> str: def _session_key(agent: Agent) -> str:
if agent.openclaw_session_id:
return agent.openclaw_session_id
return f"agent:{_agent_key(agent)}:main"
async def _supported_gateway_files(config: GatewayClientConfig) -> set[str]:
try:
agents_payload = await openclaw_call("agents.list", config=config)
agents = []
default_id = None
if isinstance(agents_payload, dict):
agents = list(agents_payload.get("agents") or [])
default_id = agents_payload.get("defaultId") or agents_payload.get("default_id")
agent_id = default_id or (agents[0].get("id") if agents else None)
if not agent_id:
return set(DEFAULT_GATEWAY_FILES)
files_payload = await openclaw_call(
"agents.files.list", {"agentId": agent_id}, config=config
)
if isinstance(files_payload, dict):
files = files_payload.get("files") or []
supported = {item.get("name") for item in files if isinstance(item, dict)}
return supported or set(DEFAULT_GATEWAY_FILES)
except OpenClawGatewayError:
pass
return set(DEFAULT_GATEWAY_FILES)
async def _gateway_agent_files_index(
agent_id: str, config: GatewayClientConfig
) -> dict[str, dict[str, Any]]:
try:
payload = await openclaw_call("agents.files.list", {"agentId": agent_id}, config=config)
if isinstance(payload, dict):
files = payload.get("files") or []
return {
item.get("name"): item
for item in files
if isinstance(item, dict) and item.get("name")
}
except OpenClawGatewayError:
pass
return {}
def _render_agent_files(
context: dict[str, str],
agent: Agent,
file_names: set[str],
*,
include_bootstrap: bool,
) -> dict[str, str]:
env = _template_env()
overrides: dict[str, str] = {} overrides: dict[str, str] = {}
if agent.identity_template: if agent.identity_template:
overrides["IDENTITY.md"] = agent.identity_template overrides["IDENTITY.md"] = agent.identity_template
if agent.soul_template: if agent.soul_template:
overrides["SOUL.md"] = agent.soul_template overrides["SOUL.md"] = agent.soul_template
templates = _read_templates(context, overrides=overrides)
return "".join(_render_file_block(name, templates.get(name, "")) for name in TEMPLATE_FILES) rendered: dict[str, str] = {}
for name in sorted(file_names):
if name == "BOOTSTRAP.md" and not include_bootstrap:
continue
if name == "MEMORY.md":
rendered[name] = "# MEMORY\n\nBootstrap pending.\n"
continue
override = overrides.get(name)
if override:
rendered[name] = env.from_string(override).render(**context).strip()
continue
path = _templates_root() / name
if path.exists():
rendered[name] = env.get_template(name).render(**context).strip()
continue
rendered[name] = ""
return rendered
def build_provisioning_message( async def _patch_gateway_agent_list(
agent_id: str,
workspace_path: str,
heartbeat: dict[str, Any],
config: GatewayClientConfig,
) -> None:
cfg = await openclaw_call("config.get", config=config)
if not isinstance(cfg, dict):
raise OpenClawGatewayError("config.get returned invalid payload")
base_hash = cfg.get("hash")
data = cfg.get("config") or cfg.get("parsed") or {}
if not isinstance(data, dict):
raise OpenClawGatewayError("config.get returned invalid config")
agents = data.get("agents") or {}
lst = agents.get("list") or []
if not isinstance(lst, list):
raise OpenClawGatewayError("config agents.list is not a list")
updated = False
new_list: list[dict[str, Any]] = []
for entry in lst:
if isinstance(entry, dict) and entry.get("id") == agent_id:
new_entry = dict(entry)
new_entry["workspace"] = workspace_path
new_entry["heartbeat"] = heartbeat
new_list.append(new_entry)
updated = True
else:
new_list.append(entry)
if not updated:
new_list.append({"id": agent_id, "workspace": workspace_path, "heartbeat": heartbeat})
patch = {"agents": {"list": new_list}}
params = {"raw": json.dumps(patch)}
if base_hash:
params["baseHash"] = base_hash
await openclaw_call("config.patch", params, config=config)
async def _remove_gateway_agent_list(
agent_id: str,
config: GatewayClientConfig,
) -> None:
cfg = await openclaw_call("config.get", config=config)
if not isinstance(cfg, dict):
raise OpenClawGatewayError("config.get returned invalid payload")
base_hash = cfg.get("hash")
data = cfg.get("config") or cfg.get("parsed") or {}
if not isinstance(data, dict):
raise OpenClawGatewayError("config.get returned invalid config")
agents = data.get("agents") or {}
lst = agents.get("list") or []
if not isinstance(lst, list):
raise OpenClawGatewayError("config agents.list is not a list")
new_list = [entry for entry in lst if not (isinstance(entry, dict) and entry.get("id") == agent_id)]
if len(new_list) == len(lst):
return
patch = {"agents": {"list": new_list}}
params = {"raw": json.dumps(patch)}
if base_hash:
params["baseHash"] = base_hash
await openclaw_call("config.patch", params, config=config)
async def provision_agent(
agent: Agent, agent: Agent,
board: Board, board: Board,
gateway: Gateway, gateway: Gateway,
auth_token: str, auth_token: str,
confirm_token: str,
user: User | None,
) -> str:
context = _build_context(agent, board, gateway, auth_token, user)
file_blocks = _build_file_blocks(context, agent)
heartbeat_snippet = json.dumps(
{
"id": _agent_key(agent),
"workspace": context["workspace_path"],
"heartbeat": _heartbeat_config(agent),
},
indent=2,
sort_keys=True,
)
return (
"Provision a new OpenClaw agent workspace.\n\n"
f"Agent name: {context['agent_name']}\n"
f"Agent id: {context['agent_id']}\n"
f"Session key: {context['session_key']}\n"
f"Workspace path: {context['workspace_path']}\n\n"
f"Base URL: {context['base_url']}\n"
f"Auth token: {context['auth_token']}\n\n"
"Steps:\n"
"0) IMPORTANT: Do NOT replace or repurpose the main agent. Keep "
f"{context['main_session_key']} unchanged and its workspace intact.\n"
"1) Create the workspace directory.\n"
"2) Write the files below with the exact contents.\n"
"3) Update TOOLS.md if BASE_URL/AUTH_TOKEN must change.\n"
"4) Leave BOOTSTRAP.md in place; the agent should run it on first start and delete it.\n"
"5) Register agent id in OpenClaw so it uses this workspace path "
"(never overwrite the main agent session).\n"
" IMPORTANT: Use the configured gateway workspace root. "
"Workspace path must be <root>/workspace-<slug>.\n"
"6) Add/update the per-agent heartbeat config in the gateway config "
"for this agent (merge into agents.list entry):\n"
"```json\n"
f"{heartbeat_snippet}\n"
"```\n"
"Note: if any agents.list entry defines heartbeat, only those agents "
"run heartbeats.\n"
"7) After provisioning completes, confirm by calling:\n"
f" POST {context['base_url']}/api/v1/agents/{context['agent_id']}/provision/confirm\n"
f' Body: {{"token": "{confirm_token}", "action": "provision"}}\n\n'
"Files:" + file_blocks
)
def build_update_message(
agent: Agent,
board: Board,
gateway: Gateway,
auth_token: str,
confirm_token: str,
user: User | None,
) -> str:
context = _build_context(agent, board, gateway, auth_token, user)
file_blocks = _build_file_blocks(context, agent)
heartbeat_snippet = json.dumps(
{
"id": _agent_key(agent),
"workspace": context["workspace_path"],
"heartbeat": _heartbeat_config(agent),
},
indent=2,
sort_keys=True,
)
return (
"Update an existing OpenClaw agent workspace.\n\n"
f"Agent name: {context['agent_name']}\n"
f"Agent id: {context['agent_id']}\n"
f"Session key: {context['session_key']}\n"
f"Workspace path: {context['workspace_path']}\n\n"
f"Base URL: {context['base_url']}\n"
f"Auth token: {context['auth_token']}\n\n"
"Steps:\n"
"0) IMPORTANT: Do NOT replace or repurpose the main agent. Keep "
f"{context['main_session_key']} unchanged and its workspace intact.\n"
"1) Locate the existing workspace directory (do NOT create a new one or change its path).\n"
"2) Overwrite the files below with the exact contents.\n"
"3) Update TOOLS.md with the new BASE_URL/AUTH_TOKEN/SESSION_KEY values.\n"
"4) Do NOT create a new agent or session; update the existing one in place.\n"
"5) Keep BOOTSTRAP.md only if it already exists; do not recreate it if missing.\n\n"
" IMPORTANT: Use the configured gateway workspace root. "
"Workspace path must be <root>/workspace-<slug>.\n"
"6) Update the per-agent heartbeat config in the gateway config for this agent:\n"
"```json\n"
f"{heartbeat_snippet}\n"
"```\n"
"Note: if any agents.list entry defines heartbeat, only those agents "
"run heartbeats.\n"
"7) After the update completes (and only after files are written), confirm by calling:\n"
f" POST {context['base_url']}/api/v1/agents/{context['agent_id']}/provision/confirm\n"
f' Body: {{"token": "{confirm_token}", "action": "update"}}\n'
" Mission Control will send the hello message only after this confirmation.\n\n"
"Files:" + file_blocks
)
async def send_provisioning_message(
agent: Agent,
board: Board,
gateway: Gateway,
auth_token: str,
confirm_token: str,
user: User | None, user: User | None,
*,
action: str = "provision",
) -> None: ) -> None:
if not gateway.url: if not gateway.url:
return return
if not gateway.main_session_key: if not gateway.workspace_root:
raise ValueError("gateway_main_session_key is required") raise ValueError("gateway_workspace_root is required")
main_session = gateway.main_session_key
client_config = GatewayClientConfig(url=gateway.url, token=gateway.token) client_config = GatewayClientConfig(url=gateway.url, token=gateway.token)
await ensure_session(main_session, config=client_config, label="Main Agent") session_key = _session_key(agent)
message = build_provisioning_message(agent, board, gateway, auth_token, confirm_token, user) await ensure_session(session_key, config=client_config, label=agent.name)
await send_message(message, session_key=main_session, config=client_config, deliver=False)
agent_id = _agent_key(agent)
workspace_path = _workspace_path(agent.name, gateway.workspace_root)
heartbeat = _heartbeat_config(agent)
await _patch_gateway_agent_list(agent_id, workspace_path, heartbeat, client_config)
context = _build_context(agent, board, gateway, auth_token, user)
supported = await _supported_gateway_files(client_config)
existing_files = await _gateway_agent_files_index(agent_id, client_config)
include_bootstrap = True
if action == "update":
if not existing_files:
include_bootstrap = False
else:
entry = existing_files.get("BOOTSTRAP.md")
if entry and entry.get("missing") is True:
include_bootstrap = False
rendered = _render_agent_files(
context,
agent,
supported,
include_bootstrap=include_bootstrap,
)
for name, content in rendered.items():
if content == "":
continue
await openclaw_call(
"agents.files.set",
{"agentId": agent_id, "name": name, "content": content},
config=client_config,
)
async def send_update_message( async def cleanup_agent_direct(
agent: Agent, agent: Agent,
board: Board,
gateway: Gateway, gateway: Gateway,
auth_token: str, *,
confirm_token: str, delete_workspace: bool = True,
user: User | None,
) -> None: ) -> None:
if not gateway.url: if not gateway.url:
return return
if not gateway.main_session_key: if not gateway.workspace_root:
raise ValueError("gateway_main_session_key is required") raise ValueError("gateway_workspace_root is required")
main_session = gateway.main_session_key
client_config = GatewayClientConfig(url=gateway.url, token=gateway.token) client_config = GatewayClientConfig(url=gateway.url, token=gateway.token)
await ensure_session(main_session, config=client_config, label="Main Agent")
message = build_update_message(agent, board, gateway, auth_token, confirm_token, user) agent_id = _agent_key(agent)
await send_message(message, session_key=main_session, config=client_config, deliver=False) await _remove_gateway_agent_list(agent_id, client_config)
session_key = _session_key(agent)
await openclaw_call("sessions.delete", {"key": session_key}, config=client_config)
if delete_workspace:
workspace_dir = _resolve_workspace_dir(gateway.workspace_root, agent.name)
if workspace_dir.exists():
shutil.rmtree(workspace_dir)

View File

@@ -8,9 +8,17 @@ There is no memory yet. Create what is missing and proceed without blocking.
1) Create `memory/` and `memory.md` if missing. 1) Create `memory/` and `memory.md` if missing.
2) Read `IDENTITY.md`, `USER.md`, and `SOUL.md`. 2) Read `IDENTITY.md`, `USER.md`, and `SOUL.md`.
3) If any fields are blank, leave them blank. Do not invent values. 3) If any fields are blank, leave them blank. Do not invent values.
4) Write a short note to `memory.md` that bootstrap completed and list any 4) If `BASE_URL`, `AUTH_TOKEN`, and `BOARD_ID` are set in `TOOLS.md`, check in
to Mission Control to mark the agent online:
```bash
curl -s -X POST "$BASE_URL/api/v1/agents/heartbeat" \
-H "X-Agent-Token: $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "'$AGENT_NAME'", "board_id": "'$BOARD_ID'", "status": "online"}'
```
5) Write a short note to `memory.md` that bootstrap completed and list any
missing fields (e.g., user name, timezone). missing fields (e.g., user name, timezone).
5) Delete this file. 6) Delete this file.
## Optional: if a human is already present ## Optional: if a human is already present
You may ask a short, single message to fill missing fields. If no reply arrives You may ask a short, single message to fill missing fields. If no reply arrives