feat(agents): Require gateway workspace root and main session key for agent provisioning

This commit is contained in:
Abhimanyu Saharan
2026-02-04 17:14:47 +05:30
parent 8aa96ca876
commit d3642a5efd
6 changed files with 92 additions and 24 deletions

View File

@@ -52,6 +52,16 @@ def _build_session_key(agent_name: str) -> str:
return f"{AGENT_SESSION_PREFIX}:{_slugify(agent_name)}:main"
def _workspace_path(agent_name: str, workspace_root: str | None) -> str:
if not workspace_root:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_workspace_root is required",
)
root = workspace_root.rstrip("/")
return f"{root}/workspace-{_slugify(agent_name)}"
def _require_board(session: Session, board_id: UUID | str | None) -> Board:
if not board_id:
raise HTTPException(
@@ -70,6 +80,16 @@ def _require_gateway_config(board: Board) -> GatewayConfig:
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_url is required",
)
if not board.gateway_main_session_key:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_main_session_key is required",
)
if not board.gateway_workspace_root:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_workspace_root is required",
)
return GatewayConfig(url=board.gateway_url, token=board.gateway_token)
@@ -459,11 +479,8 @@ def delete_agent(
session.commit()
async def _gateway_cleanup_request() -> None:
main_session = board.gateway_main_session_key or "agent:main:main"
if not main_session:
return
workspace_root = board.gateway_workspace_root or "~/.openclaw/workspaces"
workspace_path = f"{workspace_root.rstrip('/')}/{_slugify(agent.name)}"
main_session = board.gateway_main_session_key
workspace_path = _workspace_path(agent.name, board.gateway_workspace_root)
base_url = settings.base_url or "REPLACE_WITH_BASE_URL"
cleanup_message = (
"Cleanup request for deleted agent.\n\n"

View File

@@ -46,15 +46,29 @@ def _build_session_key(agent_name: str) -> str:
def _board_gateway_config(board: Board) -> GatewayConfig | None:
if not board.gateway_url:
return None
if not board.gateway_main_session_key:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_main_session_key is required",
)
if not board.gateway_workspace_root:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_workspace_root is required",
)
return GatewayConfig(url=board.gateway_url, token=board.gateway_token)
async def _cleanup_agent_on_gateway(agent: Agent, board: Board, config: GatewayConfig) -> None:
if agent.openclaw_session_id:
await delete_session(agent.openclaw_session_id, config=config)
main_session = board.gateway_main_session_key or "agent:main:main"
workspace_root = board.gateway_workspace_root or "~/.openclaw/workspaces"
workspace_path = f"{workspace_root.rstrip('/')}/{_slugify(agent.name)}"
if not board.gateway_main_session_key:
raise OpenClawGatewayError("Board gateway_main_session_key is required")
if not board.gateway_workspace_root:
raise OpenClawGatewayError("Board gateway_workspace_root is required")
main_session = board.gateway_main_session_key
workspace_root = board.gateway_workspace_root
workspace_path = f"{workspace_root.rstrip('/')}/workspace-{_slugify(agent.name)}"
cleanup_message = (
"Cleanup request for deleted agent.\n\n"
f"Agent name: {agent.name}\n"
@@ -87,6 +101,17 @@ def create_board(
data = payload.model_dump()
if data.get("gateway_token") == "":
data["gateway_token"] = None
if data.get("gateway_url"):
if not data.get("gateway_main_session_key"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_main_session_key is required when gateway_url is set",
)
if not data.get("gateway_workspace_root"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_workspace_root is required when gateway_url is set",
)
board = Board.model_validate(data)
session.add(board)
session.commit()
@@ -114,6 +139,17 @@ def update_board(
updates["gateway_token"] = None
for key, value in updates.items():
setattr(board, key, value)
if board.gateway_url:
if not board.gateway_main_session_key:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_main_session_key is required when gateway_url is set",
)
if not board.gateway_workspace_root:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_workspace_root is required when gateway_url is set",
)
session.add(board)
session.commit()
session.refresh(board)

View File

@@ -38,6 +38,11 @@ def _require_board_config(session: Session, board_id: str | None) -> tuple[Board
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_url is required",
)
if not board.gateway_main_session_key:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_main_session_key is required",
)
return board, GatewayConfig(url=board.gateway_url, token=board.gateway_token)
@@ -54,7 +59,7 @@ async def gateway_status(
sessions_list = list(sessions.get("sessions") or [])
else:
sessions_list = list(sessions or [])
main_session = board.gateway_main_session_key or "agent:main:main"
main_session = board.gateway_main_session_key
main_session_entry: object | None = None
main_session_error: str | None = None
if main_session:
@@ -99,7 +104,7 @@ async def list_sessions(
else:
sessions_list = list(sessions or [])
main_session = board.gateway_main_session_key or "agent:main:main"
main_session = board.gateway_main_session_key
main_session_entry: object | None = None
if main_session:
try:
@@ -134,7 +139,7 @@ async def get_gateway_session(
sessions_list = list(sessions.get("sessions") or [])
else:
sessions_list = list(sessions or [])
main_session = board.gateway_main_session_key or "agent:main:main"
main_session = board.gateway_main_session_key
if main_session and not any(
session.get("key") == main_session for session in sessions_list
):
@@ -194,7 +199,7 @@ async def send_session_message(
)
board, config = _require_board_config(session, board_id)
try:
main_session = board.gateway_main_session_key or "agent:main:main"
main_session = board.gateway_main_session_key
if main_session and session_id == main_session:
await ensure_session(main_session, config=config, label="Main Agent")
await send_message(content, session_key=session_id, config=config)

View File

@@ -83,18 +83,24 @@ def _render_file_block(name: str, content: str) -> str:
def _workspace_path(agent_name: str, workspace_root: str) -> str:
root = workspace_root or "~/.openclaw/workspaces"
if not workspace_root:
raise ValueError("gateway_workspace_root is required")
root = workspace_root
root = root.rstrip("/")
return f"{root}/{_slugify(agent_name)}"
return f"{root}/workspace-{_slugify(agent_name)}"
def _build_context(agent: Agent, board: Board, auth_token: str) -> dict[str, str]:
if not board.gateway_workspace_root:
raise ValueError("gateway_workspace_root is required")
if not board.gateway_main_session_key:
raise ValueError("gateway_main_session_key is required")
agent_id = str(agent.id)
workspace_root = board.gateway_workspace_root or "~/.openclaw/workspaces"
workspace_root = board.gateway_workspace_root
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"
main_session_key = board.gateway_main_session_key
return {
"agent_name": agent.name,
"agent_id": agent_id,
@@ -148,8 +154,8 @@ def build_provisioning_message(agent: Agent, board: Board, auth_token: str) -> s
"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: Do NOT use ~/.openclaw/workspace-<name>. The canonical path "
"is ~/.openclaw/workspaces/<slug>.\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"
@@ -189,8 +195,8 @@ def build_update_message(agent: Agent, board: Board, auth_token: str) -> str:
"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: Do NOT use ~/.openclaw/workspace-<name>. The canonical path "
"is ~/.openclaw/workspaces/<slug>.\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"
@@ -206,9 +212,11 @@ async def send_provisioning_message(
board: Board,
auth_token: str,
) -> None:
main_session = board.gateway_main_session_key or "agent:main:main"
if not board.gateway_url:
return
if not board.gateway_main_session_key:
raise ValueError("gateway_main_session_key is required")
main_session = board.gateway_main_session_key
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)
@@ -220,9 +228,11 @@ async def send_update_message(
board: Board,
auth_token: str,
) -> None:
main_session = board.gateway_main_session_key or "agent:main:main"
if not board.gateway_url:
return
if not board.gateway_main_session_key:
raise ValueError("gateway_main_session_key is required")
main_session = board.gateway_main_session_key
config = GatewayConfig(url=board.gateway_url, token=board.gateway_token)
await ensure_session(main_session, config=config, label="Main Agent")
message = build_update_message(agent, board, auth_token)

View File

@@ -206,7 +206,7 @@ export default function EditBoardPage() {
<Input
value={gatewayWorkspaceRoot}
onChange={(event) => setGatewayWorkspaceRoot(event.target.value)}
placeholder="~/.openclaw/workspaces"
placeholder="~/.openclaw"
disabled={isLoading}
/>
</div>

View File

@@ -166,7 +166,7 @@ export default function NewBoardPage() {
<Input
value={gatewayWorkspaceRoot}
onChange={(event) => setGatewayWorkspaceRoot(event.target.value)}
placeholder="~/.openclaw/workspaces"
placeholder="~/.openclaw"
disabled={isLoading}
/>
</div>