feat(agents): Require gateway workspace root and main session key for agent provisioning
This commit is contained in:
@@ -52,6 +52,16 @@ def _build_session_key(agent_name: str) -> str:
|
|||||||
return f"{AGENT_SESSION_PREFIX}:{_slugify(agent_name)}:main"
|
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:
|
def _require_board(session: Session, board_id: UUID | str | None) -> Board:
|
||||||
if not board_id:
|
if not board_id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -70,6 +80,16 @@ def _require_gateway_config(board: Board) -> GatewayConfig:
|
|||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
detail="Board gateway_url is required",
|
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)
|
return GatewayConfig(url=board.gateway_url, token=board.gateway_token)
|
||||||
|
|
||||||
|
|
||||||
@@ -459,11 +479,8 @@ def delete_agent(
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
async def _gateway_cleanup_request() -> None:
|
async def _gateway_cleanup_request() -> None:
|
||||||
main_session = board.gateway_main_session_key or "agent:main:main"
|
main_session = board.gateway_main_session_key
|
||||||
if not main_session:
|
workspace_path = _workspace_path(agent.name, board.gateway_workspace_root)
|
||||||
return
|
|
||||||
workspace_root = board.gateway_workspace_root or "~/.openclaw/workspaces"
|
|
||||||
workspace_path = f"{workspace_root.rstrip('/')}/{_slugify(agent.name)}"
|
|
||||||
base_url = settings.base_url or "REPLACE_WITH_BASE_URL"
|
base_url = settings.base_url or "REPLACE_WITH_BASE_URL"
|
||||||
cleanup_message = (
|
cleanup_message = (
|
||||||
"Cleanup request for deleted agent.\n\n"
|
"Cleanup request for deleted agent.\n\n"
|
||||||
|
|||||||
@@ -46,15 +46,29 @@ def _build_session_key(agent_name: str) -> str:
|
|||||||
def _board_gateway_config(board: Board) -> GatewayConfig | None:
|
def _board_gateway_config(board: Board) -> GatewayConfig | None:
|
||||||
if not board.gateway_url:
|
if not board.gateway_url:
|
||||||
return None
|
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)
|
return GatewayConfig(url=board.gateway_url, token=board.gateway_token)
|
||||||
|
|
||||||
|
|
||||||
async def _cleanup_agent_on_gateway(agent: Agent, board: Board, config: GatewayConfig) -> None:
|
async def _cleanup_agent_on_gateway(agent: Agent, board: Board, config: GatewayConfig) -> None:
|
||||||
if agent.openclaw_session_id:
|
if agent.openclaw_session_id:
|
||||||
await delete_session(agent.openclaw_session_id, config=config)
|
await delete_session(agent.openclaw_session_id, config=config)
|
||||||
main_session = board.gateway_main_session_key or "agent:main:main"
|
if not board.gateway_main_session_key:
|
||||||
workspace_root = board.gateway_workspace_root or "~/.openclaw/workspaces"
|
raise OpenClawGatewayError("Board gateway_main_session_key is required")
|
||||||
workspace_path = f"{workspace_root.rstrip('/')}/{_slugify(agent.name)}"
|
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_message = (
|
||||||
"Cleanup request for deleted agent.\n\n"
|
"Cleanup request for deleted agent.\n\n"
|
||||||
f"Agent name: {agent.name}\n"
|
f"Agent name: {agent.name}\n"
|
||||||
@@ -87,6 +101,17 @@ def create_board(
|
|||||||
data = payload.model_dump()
|
data = payload.model_dump()
|
||||||
if data.get("gateway_token") == "":
|
if data.get("gateway_token") == "":
|
||||||
data["gateway_token"] = None
|
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)
|
board = Board.model_validate(data)
|
||||||
session.add(board)
|
session.add(board)
|
||||||
session.commit()
|
session.commit()
|
||||||
@@ -114,6 +139,17 @@ def update_board(
|
|||||||
updates["gateway_token"] = None
|
updates["gateway_token"] = None
|
||||||
for key, value in updates.items():
|
for key, value in updates.items():
|
||||||
setattr(board, key, value)
|
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.add(board)
|
||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(board)
|
session.refresh(board)
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ def _require_board_config(session: Session, board_id: str | None) -> tuple[Board
|
|||||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
detail="Board gateway_url is required",
|
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)
|
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 [])
|
sessions_list = list(sessions.get("sessions") or [])
|
||||||
else:
|
else:
|
||||||
sessions_list = list(sessions or [])
|
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_entry: object | None = None
|
||||||
main_session_error: str | None = None
|
main_session_error: str | None = None
|
||||||
if main_session:
|
if main_session:
|
||||||
@@ -99,7 +104,7 @@ async def list_sessions(
|
|||||||
else:
|
else:
|
||||||
sessions_list = list(sessions or [])
|
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_entry: object | None = None
|
||||||
if main_session:
|
if main_session:
|
||||||
try:
|
try:
|
||||||
@@ -134,7 +139,7 @@ async def get_gateway_session(
|
|||||||
sessions_list = list(sessions.get("sessions") or [])
|
sessions_list = list(sessions.get("sessions") or [])
|
||||||
else:
|
else:
|
||||||
sessions_list = list(sessions or [])
|
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(
|
if main_session and not any(
|
||||||
session.get("key") == main_session for session in sessions_list
|
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)
|
board, config = _require_board_config(session, board_id)
|
||||||
try:
|
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:
|
if main_session and session_id == main_session:
|
||||||
await ensure_session(main_session, config=config, label="Main Agent")
|
await ensure_session(main_session, config=config, label="Main Agent")
|
||||||
await send_message(content, session_key=session_id, config=config)
|
await send_message(content, session_key=session_id, config=config)
|
||||||
|
|||||||
@@ -83,18 +83,24 @@ def _render_file_block(name: str, content: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _workspace_path(agent_name: str, workspace_root: 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("/")
|
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]:
|
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)
|
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)
|
workspace_path = _workspace_path(agent.name, workspace_root)
|
||||||
session_key = agent.openclaw_session_id or ""
|
session_key = agent.openclaw_session_id or ""
|
||||||
base_url = settings.base_url or "REPLACE_WITH_BASE_URL"
|
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 {
|
return {
|
||||||
"agent_name": agent.name,
|
"agent_name": agent.name,
|
||||||
"agent_id": agent_id,
|
"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"
|
"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 "
|
"5) Register agent id in OpenClaw so it uses this workspace path "
|
||||||
"(never overwrite the main agent session).\n"
|
"(never overwrite the main agent session).\n"
|
||||||
" IMPORTANT: Do NOT use ~/.openclaw/workspace-<name>. The canonical path "
|
" IMPORTANT: Use the configured gateway workspace root. "
|
||||||
"is ~/.openclaw/workspaces/<slug>.\n"
|
"Workspace path must be <root>/workspace-<slug>.\n"
|
||||||
"6) Add/update the per-agent heartbeat config in the gateway config "
|
"6) Add/update the per-agent heartbeat config in the gateway config "
|
||||||
"for this agent (merge into agents.list entry):\n"
|
"for this agent (merge into agents.list entry):\n"
|
||||||
"```json\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"
|
"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"
|
"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"
|
"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 "
|
" IMPORTANT: Use the configured gateway workspace root. "
|
||||||
"is ~/.openclaw/workspaces/<slug>.\n"
|
"Workspace path must be <root>/workspace-<slug>.\n"
|
||||||
"6) Update the per-agent heartbeat config in the gateway config for this agent:\n"
|
"6) Update the per-agent heartbeat config in the gateway config for this agent:\n"
|
||||||
"```json\n"
|
"```json\n"
|
||||||
f"{heartbeat_snippet}\n"
|
f"{heartbeat_snippet}\n"
|
||||||
@@ -206,9 +212,11 @@ async def send_provisioning_message(
|
|||||||
board: Board,
|
board: Board,
|
||||||
auth_token: str,
|
auth_token: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
main_session = board.gateway_main_session_key or "agent:main:main"
|
|
||||||
if not board.gateway_url:
|
if not board.gateway_url:
|
||||||
return
|
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)
|
config = GatewayConfig(url=board.gateway_url, token=board.gateway_token)
|
||||||
await ensure_session(main_session, config=config, label="Main Agent")
|
await ensure_session(main_session, config=config, label="Main Agent")
|
||||||
message = build_provisioning_message(agent, board, auth_token)
|
message = build_provisioning_message(agent, board, auth_token)
|
||||||
@@ -220,9 +228,11 @@ async def send_update_message(
|
|||||||
board: Board,
|
board: Board,
|
||||||
auth_token: str,
|
auth_token: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
main_session = board.gateway_main_session_key or "agent:main:main"
|
|
||||||
if not board.gateway_url:
|
if not board.gateway_url:
|
||||||
return
|
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)
|
config = GatewayConfig(url=board.gateway_url, token=board.gateway_token)
|
||||||
await ensure_session(main_session, config=config, label="Main Agent")
|
await ensure_session(main_session, config=config, label="Main Agent")
|
||||||
message = build_update_message(agent, board, auth_token)
|
message = build_update_message(agent, board, auth_token)
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ export default function EditBoardPage() {
|
|||||||
<Input
|
<Input
|
||||||
value={gatewayWorkspaceRoot}
|
value={gatewayWorkspaceRoot}
|
||||||
onChange={(event) => setGatewayWorkspaceRoot(event.target.value)}
|
onChange={(event) => setGatewayWorkspaceRoot(event.target.value)}
|
||||||
placeholder="~/.openclaw/workspaces"
|
placeholder="~/.openclaw"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export default function NewBoardPage() {
|
|||||||
<Input
|
<Input
|
||||||
value={gatewayWorkspaceRoot}
|
value={gatewayWorkspaceRoot}
|
||||||
onChange={(event) => setGatewayWorkspaceRoot(event.target.value)}
|
onChange={(event) => setGatewayWorkspaceRoot(event.target.value)}
|
||||||
placeholder="~/.openclaw/workspaces"
|
placeholder="~/.openclaw"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user