diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index f6150c7e..e78807a8 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -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" diff --git a/backend/app/api/boards.py b/backend/app/api/boards.py index e98d6e7e..907ab4ca 100644 --- a/backend/app/api/boards.py +++ b/backend/app/api/boards.py @@ -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) diff --git a/backend/app/api/gateway.py b/backend/app/api/gateway.py index 77f8db34..cf127620 100644 --- a/backend/app/api/gateway.py +++ b/backend/app/api/gateway.py @@ -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) diff --git a/backend/app/services/agent_provisioning.py b/backend/app/services/agent_provisioning.py index 58852d75..a5cbc38c 100644 --- a/backend/app/services/agent_provisioning.py +++ b/backend/app/services/agent_provisioning.py @@ -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-. The canonical path " - "is ~/.openclaw/workspaces/.\n" + " IMPORTANT: Use the configured gateway workspace root. " + "Workspace path must be /workspace-.\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-. The canonical path " - "is ~/.openclaw/workspaces/.\n" + " IMPORTANT: Use the configured gateway workspace root. " + "Workspace path must be /workspace-.\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) diff --git a/frontend/src/app/boards/[boardId]/edit/page.tsx b/frontend/src/app/boards/[boardId]/edit/page.tsx index 20b07e5d..df763e41 100644 --- a/frontend/src/app/boards/[boardId]/edit/page.tsx +++ b/frontend/src/app/boards/[boardId]/edit/page.tsx @@ -206,7 +206,7 @@ export default function EditBoardPage() { setGatewayWorkspaceRoot(event.target.value)} - placeholder="~/.openclaw/workspaces" + placeholder="~/.openclaw" disabled={isLoading} /> diff --git a/frontend/src/app/boards/new/page.tsx b/frontend/src/app/boards/new/page.tsx index b08f2fa1..4823753e 100644 --- a/frontend/src/app/boards/new/page.tsx +++ b/frontend/src/app/boards/new/page.tsx @@ -166,7 +166,7 @@ export default function NewBoardPage() { setGatewayWorkspaceRoot(event.target.value)} - placeholder="~/.openclaw/workspaces" + placeholder="~/.openclaw" disabled={isLoading} />