feat(boards): Store gateway config per board

Move gateway configuration into board settings and wire agent\nprovisioning, heartbeat templates, and gateway status lookups\nto use board-specific gateway settings. Adds board_id on agents\nand UI updates for board-scoped selection.\n\nCo-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Abhimanyu Saharan
2026-02-04 16:04:52 +05:30
parent 12698d0781
commit 4dea771545
20 changed files with 827 additions and 196 deletions

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import re
from datetime import datetime, timedelta
from uuid import uuid4
from uuid import UUID, uuid4
from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, col, select
@@ -11,9 +11,9 @@ from sqlalchemy import update
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
from app.core.auth import AuthContext
from app.core.config import settings
from app.db.session import get_session
from app.integrations.openclaw_gateway import (
GatewayConfig,
OpenClawGatewayError,
delete_session,
ensure_session,
@@ -21,6 +21,7 @@ from app.integrations.openclaw_gateway import (
)
from app.models.agents import Agent
from app.models.activity_events import ActivityEvent
from app.models.boards import Board
from app.schemas.agents import (
AgentCreate,
AgentHeartbeat,
@@ -46,10 +47,34 @@ def _build_session_key(agent_name: str) -> str:
return f"{AGENT_SESSION_PREFIX}:{_slugify(agent_name)}:main"
async def _ensure_gateway_session(agent_name: str) -> tuple[str, str | None]:
def _require_board(session: Session, board_id: UUID | str | None) -> Board:
if not board_id:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="board_id is required",
)
board = session.get(Board, board_id)
if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Board not found")
return board
def _require_gateway_config(board: Board) -> GatewayConfig:
if not board.gateway_url:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_url is required",
)
return GatewayConfig(url=board.gateway_url, token=board.gateway_token)
async def _ensure_gateway_session(
agent_name: str,
config: GatewayConfig,
) -> tuple[str, str | None]:
session_key = _build_session_key(agent_name)
try:
await ensure_session(session_key, label=agent_name)
await ensure_session(session_key, config=config, label=agent_name)
return session_key, None
except OpenClawGatewayError as exc:
return session_key, str(exc)
@@ -97,11 +122,13 @@ async def create_agent(
session: Session = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth),
) -> Agent:
board = _require_board(session, payload.board_id)
config = _require_gateway_config(board)
agent = Agent.model_validate(payload)
agent.status = "provisioning"
raw_token = generate_agent_token()
agent.agent_token_hash = hash_agent_token(raw_token)
session_key, session_error = await _ensure_gateway_session(agent.name)
session_key, session_error = await _ensure_gateway_session(agent.name, config)
agent.openclaw_session_id = session_key
session.add(agent)
session.commit()
@@ -122,7 +149,7 @@ async def create_agent(
)
session.commit()
try:
await send_provisioning_message(agent, raw_token)
await send_provisioning_message(agent, board, raw_token)
except OpenClawGatewayError as exc:
_record_provisioning_failure(session, agent, str(exc))
session.commit()
@@ -160,6 +187,8 @@ def update_agent(
status_code=status.HTTP_403_FORBIDDEN,
detail="status is controlled by agent heartbeat",
)
if "board_id" in updates and updates["board_id"]:
_require_board(session, str(updates["board_id"]))
for key, value in updates.items():
setattr(agent, key, value)
agent.updated_at = datetime.utcnow()
@@ -204,10 +233,12 @@ async def heartbeat_or_create_agent(
if agent is None:
if actor.actor_type == "agent":
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
agent = Agent(name=payload.name, status="provisioning")
board = _require_board(session, payload.board_id)
config = _require_gateway_config(board)
agent = Agent(name=payload.name, status="provisioning", board_id=board.id)
raw_token = generate_agent_token()
agent.agent_token_hash = hash_agent_token(raw_token)
session_key, session_error = await _ensure_gateway_session(agent.name)
session_key, session_error = await _ensure_gateway_session(agent.name, config)
agent.openclaw_session_id = session_key
session.add(agent)
session.commit()
@@ -228,7 +259,7 @@ async def heartbeat_or_create_agent(
)
session.commit()
try:
await send_provisioning_message(agent, raw_token)
await send_provisioning_message(agent, board, raw_token)
except OpenClawGatewayError as exc:
_record_provisioning_failure(session, agent, str(exc))
session.commit()
@@ -244,7 +275,9 @@ async def heartbeat_or_create_agent(
session.commit()
session.refresh(agent)
try:
await send_provisioning_message(agent, raw_token)
board = _require_board(session, str(agent.board_id) if agent.board_id else None)
_require_gateway_config(board)
await send_provisioning_message(agent, board, raw_token)
except OpenClawGatewayError as exc:
_record_provisioning_failure(session, agent, str(exc))
session.commit()
@@ -252,7 +285,9 @@ async def heartbeat_or_create_agent(
_record_provisioning_failure(session, agent, str(exc))
session.commit()
elif not agent.openclaw_session_id:
session_key, session_error = await _ensure_gateway_session(agent.name)
board = _require_board(session, str(agent.board_id) if agent.board_id else None)
config = _require_gateway_config(board)
session_key, session_error = await _ensure_gateway_session(agent.name, config)
agent.openclaw_session_id = session_key
if session_error:
record_activity(
@@ -290,12 +325,16 @@ def delete_agent(
) -> dict[str, bool]:
agent = session.get(Agent, agent_id)
if agent:
board = _require_board(session, str(agent.board_id) if agent.board_id else None)
config = _require_gateway_config(board)
async def _gateway_cleanup() -> None:
if agent.openclaw_session_id:
await delete_session(agent.openclaw_session_id)
main_session = settings.openclaw_main_session_key
await delete_session(agent.openclaw_session_id, config=config)
main_session = board.gateway_main_session_key or "agent:main:main"
if main_session:
workspace_root = settings.openclaw_workspace_root or "~/.openclaw/workspaces"
workspace_root = (
board.gateway_workspace_root or "~/.openclaw/workspaces"
)
workspace_path = f"{workspace_root.rstrip('/')}/{_slugify(agent.name)}"
cleanup_message = (
"Cleanup request for deleted agent.\n\n"
@@ -308,8 +347,13 @@ def delete_agent(
"2) Delete any lingering session artifacts.\n"
"Reply NO_REPLY."
)
await ensure_session(main_session, label="Main Agent")
await send_message(cleanup_message, session_key=main_session, deliver=False)
await ensure_session(main_session, config=config, label="Main Agent")
await send_message(
cleanup_message,
session_key=main_session,
config=config,
deliver=False,
)
try:
import asyncio

View File

@@ -31,7 +31,10 @@ def create_board(
session: Session = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth),
) -> Board:
board = Board.model_validate(payload)
data = payload.model_dump()
if data.get("gateway_token") == "":
data["gateway_token"] = None
board = Board.model_validate(data)
session.add(board)
session.commit()
session.refresh(board)
@@ -54,6 +57,8 @@ def update_board(
auth: AuthContext = Depends(require_admin_auth),
) -> Board:
updates = payload.model_dump(exclude_unset=True)
if updates.get("gateway_token") == "":
updates["gateway_token"] = None
for key, value in updates.items():
setattr(board, key, value)
session.add(board)

View File

@@ -1,43 +1,69 @@
from __future__ import annotations
from fastapi import APIRouter, Body, Depends, HTTPException, status
from fastapi import APIRouter, Body, Depends, HTTPException, Query, status
from sqlmodel import Session
from app.api.deps import require_admin_auth
from app.core.auth import AuthContext
from app.core.config import settings
from app.integrations.openclaw_gateway import (
GatewayConfig,
OpenClawGatewayError,
ensure_session,
get_chat_history,
openclaw_call,
send_message,
)
from app.db.session import get_session
from app.models.boards import Board
router = APIRouter(prefix="/gateway", tags=["gateway"])
def _require_board_config(session: Session, board_id: str | None) -> tuple[Board, GatewayConfig]:
if not board_id:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="board_id is required",
)
board = session.get(Board, board_id)
if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Board not found")
if not board.gateway_url:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_url is required",
)
return board, GatewayConfig(url=board.gateway_url, token=board.gateway_token)
@router.get("/status")
async def gateway_status(auth: AuthContext = Depends(require_admin_auth)) -> dict[str, object]:
gateway_url = settings.openclaw_gateway_url or "ws://127.0.0.1:18789"
async def gateway_status(
board_id: str | None = Query(default=None),
session: Session = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, object]:
board, config = _require_board_config(session, board_id)
try:
sessions = await openclaw_call("sessions.list")
sessions = await openclaw_call("sessions.list", config=config)
if isinstance(sessions, dict):
sessions_list = list(sessions.get("sessions") or [])
else:
sessions_list = list(sessions or [])
main_session = settings.openclaw_main_session_key
main_session = board.gateway_main_session_key or "agent:main:main"
main_session_entry: object | None = None
main_session_error: str | None = None
if main_session:
try:
ensured = await ensure_session(main_session, label="Main Agent")
ensured = await ensure_session(
main_session, config=config, label="Main Agent"
)
if isinstance(ensured, dict):
main_session_entry = ensured.get("entry") or ensured
except OpenClawGatewayError as exc:
main_session_error = str(exc)
return {
"connected": True,
"gateway_url": gateway_url,
"gateway_url": board.gateway_url,
"sessions_count": len(sessions_list),
"sessions": sessions_list,
"main_session_key": main_session,
@@ -47,15 +73,20 @@ async def gateway_status(auth: AuthContext = Depends(require_admin_auth)) -> dic
except OpenClawGatewayError as exc:
return {
"connected": False,
"gateway_url": gateway_url,
"gateway_url": board.gateway_url,
"error": str(exc),
}
@router.get("/sessions")
async def list_sessions(auth: AuthContext = Depends(require_admin_auth)) -> dict[str, object]:
async def list_sessions(
board_id: str | None = Query(default=None),
session: Session = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, object]:
board, config = _require_board_config(session, board_id)
try:
sessions = await openclaw_call("sessions.list")
sessions = await openclaw_call("sessions.list", config=config)
except OpenClawGatewayError as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
if isinstance(sessions, dict):
@@ -63,63 +94,79 @@ async def list_sessions(auth: AuthContext = Depends(require_admin_auth)) -> dict
else:
sessions_list = list(sessions or [])
main_session = settings.openclaw_main_session_key
main_session = board.gateway_main_session_key or "agent:main:main"
main_session_entry: object | None = None
if main_session:
try:
ensured = await ensure_session(main_session, label="Main Agent")
ensured = await ensure_session(
main_session, config=config, label="Main Agent"
)
if isinstance(ensured, dict):
main_session_entry = ensured.get("entry") or ensured
except OpenClawGatewayError:
main_session_entry = None
return {"sessions": sessions_list, "main_session_key": main_session, "main_session": main_session_entry}
return {
"sessions": sessions_list,
"main_session_key": main_session,
"main_session": main_session_entry,
}
@router.get("/sessions/{session_id}")
async def get_session(
session_id: str, auth: AuthContext = Depends(require_admin_auth)
async def get_gateway_session(
session_id: str,
board_id: str | None = Query(default=None),
session: Session = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, object]:
board, config = _require_board_config(session, board_id)
try:
sessions = await openclaw_call("sessions.list")
sessions = await openclaw_call("sessions.list", config=config)
except OpenClawGatewayError as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
if isinstance(sessions, dict):
sessions_list = list(sessions.get("sessions") or [])
else:
sessions_list = list(sessions or [])
main_session = settings.openclaw_main_session_key
main_session = board.gateway_main_session_key or "agent:main:main"
if main_session and not any(
session.get("key") == main_session for session in sessions_list
):
try:
await ensure_session(main_session, label="Main Agent")
refreshed = await openclaw_call("sessions.list")
await ensure_session(main_session, config=config, label="Main Agent")
refreshed = await openclaw_call("sessions.list", config=config)
if isinstance(refreshed, dict):
sessions_list = list(refreshed.get("sessions") or [])
else:
sessions_list = list(refreshed or [])
except OpenClawGatewayError:
pass
session = next((item for item in sessions_list if item.get("key") == session_id), None)
if session is None and main_session and session_id == main_session:
session_entry = next(
(item for item in sessions_list if item.get("key") == session_id), None
)
if session_entry is None and main_session and session_id == main_session:
try:
ensured = await ensure_session(main_session, label="Main Agent")
ensured = await ensure_session(main_session, config=config, label="Main Agent")
if isinstance(ensured, dict):
session = ensured.get("entry") or ensured
session_entry = ensured.get("entry") or ensured
except OpenClawGatewayError:
session = None
if session is None:
session_entry = None
if session_entry is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found")
return {"session": session}
return {"session": session_entry}
@router.get("/sessions/{session_id}/history")
async def get_session_history(
session_id: str, auth: AuthContext = Depends(require_admin_auth)
session_id: str,
board_id: str | None = Query(default=None),
session: Session = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, object]:
_, config = _require_board_config(session, board_id)
try:
history = await get_chat_history(session_id)
history = await get_chat_history(session_id, config=config)
except OpenClawGatewayError as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
if isinstance(history, dict) and isinstance(history.get("messages"), list):
@@ -131,6 +178,8 @@ async def get_session_history(
async def send_session_message(
session_id: str,
payload: dict = Body(...),
board_id: str | None = Query(default=None),
session: Session = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, bool]:
content = payload.get("content")
@@ -138,11 +187,12 @@ async def send_session_message(
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="content is required"
)
board, config = _require_board_config(session, board_id)
try:
main_session = settings.openclaw_main_session_key
main_session = board.gateway_main_session_key or "agent:main:main"
if main_session and session_id == main_session:
await ensure_session(main_session, label="Main Agent")
await send_message(content, session_key=session_id)
await ensure_session(main_session, config=config, label="Main Agent")
await send_message(content, session_key=session_id, config=config)
except OpenClawGatewayError as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
return {"ok": True}

View File

@@ -19,12 +19,6 @@ class Settings(BaseSettings):
clerk_verify_iat: bool = True
clerk_leeway: float = 10.0
# OpenClaw Gateway
openclaw_gateway_url: str = ""
openclaw_gateway_token: str = ""
openclaw_main_session_key: str = "agent:main:main"
openclaw_workspace_root: str = "~/.openclaw/workspaces"
cors_origins: str = ""
base_url: str = ""

View File

@@ -9,7 +9,6 @@ from uuid import uuid4
import websockets
from app.core.config import settings
class OpenClawGatewayError(RuntimeError):
@@ -21,9 +20,17 @@ class OpenClawResponse:
payload: Any
def _build_gateway_url() -> str:
base_url = settings.openclaw_gateway_url or "ws://127.0.0.1:18789"
token = settings.openclaw_gateway_token
@dataclass(frozen=True)
class GatewayConfig:
url: str
token: str | None = None
def _build_gateway_url(config: GatewayConfig) -> str:
base_url = (config.url or "").strip()
if not base_url:
raise OpenClawGatewayError("Gateway URL is not configured for this board.")
token = config.token
if not token:
return base_url
parsed = urlparse(base_url)
@@ -58,7 +65,9 @@ async def _send_request(
async def _handle_challenge(
ws: websockets.WebSocketClientProtocol, first_message: str | bytes | None
ws: websockets.WebSocketClientProtocol,
first_message: str | bytes | None,
config: GatewayConfig,
) -> None:
if not first_message:
return
@@ -69,28 +78,35 @@ async def _handle_challenge(
return
connect_id = str(uuid4())
params: dict[str, Any] = {
"minProtocol": 3,
"maxProtocol": 3,
"client": {
"id": "gateway-client",
"version": "1.0.0",
"platform": "web",
"mode": "ui",
},
}
if config.token:
params["auth"] = {"token": config.token}
response = {
"type": "req",
"id": connect_id,
"method": "connect",
"params": {
"minProtocol": 3,
"maxProtocol": 3,
"client": {
"id": "gateway-client",
"version": "1.0.0",
"platform": "web",
"mode": "ui",
},
"auth": {"token": settings.openclaw_gateway_token},
},
"params": params,
}
await ws.send(json.dumps(response))
await _await_response(ws, connect_id)
async def openclaw_call(method: str, params: dict[str, Any] | None = None) -> Any:
gateway_url = _build_gateway_url()
async def openclaw_call(
method: str,
params: dict[str, Any] | None = None,
*,
config: GatewayConfig,
) -> Any:
gateway_url = _build_gateway_url(config)
try:
async with websockets.connect(gateway_url, ping_interval=None) as ws:
first_message = None
@@ -98,7 +114,7 @@ async def openclaw_call(method: str, params: dict[str, Any] | None = None) -> An
first_message = await asyncio.wait_for(ws.recv(), timeout=2)
except asyncio.TimeoutError:
first_message = None
await _handle_challenge(ws, first_message)
await _handle_challenge(ws, first_message, config)
return await _send_request(ws, method, params)
except OpenClawGatewayError:
raise
@@ -110,6 +126,7 @@ async def send_message(
message: str,
*,
session_key: str,
config: GatewayConfig,
deliver: bool = False,
) -> Any:
params: dict[str, Any] = {
@@ -118,23 +135,31 @@ async def send_message(
"deliver": deliver,
"idempotencyKey": str(uuid4()),
}
return await openclaw_call("chat.send", params)
return await openclaw_call("chat.send", params, config=config)
async def get_chat_history(session_key: str, limit: int | None = None) -> Any:
async def get_chat_history(
session_key: str,
config: GatewayConfig,
limit: int | None = None,
) -> Any:
params: dict[str, Any] = {"sessionKey": session_key}
if limit is not None:
params["limit"] = limit
return await openclaw_call("chat.history", params)
return await openclaw_call("chat.history", params, config=config)
async def delete_session(session_key: str) -> Any:
return await openclaw_call("sessions.delete", {"key": session_key})
async def delete_session(session_key: str, *, config: GatewayConfig) -> Any:
return await openclaw_call("sessions.delete", {"key": session_key}, config=config)
async def ensure_session(session_key: str, label: str | None = None) -> Any:
async def ensure_session(
session_key: str,
*,
config: GatewayConfig,
label: str | None = None,
) -> Any:
params: dict[str, Any] = {"key": session_key}
if label:
params["label"] = label
return await openclaw_call("sessions.patch", params)
return await openclaw_call("sessions.patch", params, config=config)

View File

@@ -10,6 +10,7 @@ class Agent(SQLModel, table=True):
__tablename__ = "agents"
id: UUID = Field(default_factory=uuid4, primary_key=True)
board_id: UUID | None = Field(default=None, foreign_key="boards.id", index=True)
name: str = Field(index=True)
status: str = Field(default="provisioning", index=True)
openclaw_session_id: str | None = Field(default=None, index=True)

View File

@@ -14,5 +14,9 @@ class Board(TenantScoped, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
name: str
slug: str = Field(index=True)
gateway_url: str | None = Field(default=None)
gateway_token: str | None = Field(default=None)
gateway_main_session_key: str | None = Field(default=None)
gateway_workspace_root: str | None = Field(default=None)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow)

View File

@@ -7,6 +7,7 @@ from sqlmodel import SQLModel
class AgentBase(SQLModel):
board_id: UUID | None = None
name: str
status: str = "provisioning"
@@ -16,6 +17,7 @@ class AgentCreate(AgentBase):
class AgentUpdate(SQLModel):
board_id: UUID | None = None
name: str | None = None
status: str | None = None
@@ -34,3 +36,4 @@ class AgentHeartbeat(SQLModel):
class AgentHeartbeatCreate(AgentHeartbeat):
name: str
board_id: UUID | None = None

View File

@@ -9,15 +9,22 @@ from sqlmodel import SQLModel
class BoardBase(SQLModel):
name: str
slug: str
gateway_url: str | None = None
gateway_main_session_key: str | None = None
gateway_workspace_root: str | None = None
class BoardCreate(BoardBase):
pass
gateway_token: str | None = None
class BoardUpdate(SQLModel):
name: str | None = None
slug: str | None = None
gateway_url: str | None = None
gateway_token: str | None = None
gateway_main_session_key: str | None = None
gateway_workspace_root: str | None = None
class BoardRead(BoardBase):

View File

@@ -7,8 +7,9 @@ from uuid import uuid4
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
from app.core.config import settings
from app.integrations.openclaw_gateway import ensure_session, send_message
from app.integrations.openclaw_gateway import GatewayConfig, ensure_session, send_message
from app.models.agents import Agent
from app.models.boards import Board
TEMPLATE_FILES = [
"AGENTS.md",
@@ -62,27 +63,30 @@ def _render_file_block(name: str, content: str) -> str:
return f"\n{name}\n```md\n{body}\n```\n"
def _workspace_path(agent_name: str) -> str:
root = settings.openclaw_workspace_root or "~/.openclaw/workspaces"
def _workspace_path(agent_name: str, workspace_root: str) -> str:
root = workspace_root or "~/.openclaw/workspaces"
root = root.rstrip("/")
return f"{root}/{_slugify(agent_name)}"
def build_provisioning_message(agent: Agent, auth_token: str) -> str:
def build_provisioning_message(agent: Agent, board: Board, auth_token: str) -> str:
agent_id = str(agent.id)
workspace_path = _workspace_path(agent.name)
workspace_root = board.gateway_workspace_root or "~/.openclaw/workspaces"
workspace_path = _workspace_path(agent.name, workspace_root)
session_key = agent.openclaw_session_id or ""
base_url = settings.base_url or "REPLACE_WITH_BASE_URL"
main_session_key = board.gateway_main_session_key or "agent:main:main"
context = {
"agent_name": agent.name,
"agent_id": agent_id,
"board_id": str(board.id),
"session_key": session_key,
"workspace_path": workspace_path,
"base_url": base_url,
"auth_token": auth_token,
"main_session_key": settings.openclaw_main_session_key or "agent:main:main",
"workspace_root": settings.openclaw_workspace_root or "~/.openclaw/workspaces",
"main_session_key": main_session_key,
"workspace_root": workspace_root,
"user_name": "Unset",
"user_preferred_name": "Unset",
"user_timezone": "Unset",
@@ -113,10 +117,15 @@ def build_provisioning_message(agent: Agent, auth_token: str) -> str:
)
async def send_provisioning_message(agent: Agent, auth_token: str) -> None:
main_session = settings.openclaw_main_session_key
if not main_session:
async def send_provisioning_message(
agent: Agent,
board: Board,
auth_token: str,
) -> None:
main_session = board.gateway_main_session_key or "agent:main:main"
if not board.gateway_url:
return
await ensure_session(main_session, label="Main Agent")
message = build_provisioning_message(agent, auth_token)
await send_message(message, session_key=main_session, deliver=False)
config = GatewayConfig(url=board.gateway_url, token=board.gateway_token)
await ensure_session(main_session, config=config, label="Main Agent")
message = build_provisioning_message(agent, board, auth_token)
await send_message(message, session_key=main_session, config=config, deliver=False)