refactor: update gateway agent session key handling and improve related logic

This commit is contained in:
Abhimanyu Saharan
2026-02-10 00:45:15 +05:30
parent 79f7ad8ba3
commit ba73ce8bfd
27 changed files with 233 additions and 208 deletions

View File

@@ -57,6 +57,7 @@ from app.schemas.pagination import DefaultLimitOffsetPage
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
from app.services.activity_log import record_activity from app.services.activity_log import record_activity
from app.services.board_leads import LeadAgentOptions, LeadAgentRequest, ensure_board_lead_agent from app.services.board_leads import LeadAgentOptions, LeadAgentRequest, ensure_board_lead_agent
from app.services.gateway_agents import gateway_agent_session_key, parse_gateway_agent_session_key
from app.services.task_dependencies import ( from app.services.task_dependencies import (
blocked_by_dependency_ids, blocked_by_dependency_ids,
dependency_status_by_id, dependency_status_by_id,
@@ -177,13 +178,22 @@ async def _require_gateway_main(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Agent missing session key", detail="Agent missing session key",
) )
gateway = await Gateway.objects.filter_by(main_session_key=session_key).first( gateway_id = parse_gateway_agent_session_key(session_key)
session, if gateway_id is None:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only the dedicated gateway agent may call this endpoint.",
) )
gateway = await Gateway.objects.by_id(gateway_id).first(session)
if gateway is None: if gateway is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="Only the gateway main agent may call this endpoint.", detail="Only the dedicated gateway agent may call this endpoint.",
)
if gateway_agent_session_key(gateway) != session_key:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only the dedicated gateway agent may call this endpoint.",
) )
if not gateway.url: if not gateway.url:
raise HTTPException( raise HTTPException(
@@ -729,7 +739,7 @@ async def ask_user_via_gateway_main(
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
agent_ctx: AgentAuthContext = AGENT_CTX_DEP, agent_ctx: AgentAuthContext = AGENT_CTX_DEP,
) -> GatewayMainAskUserResponse: ) -> GatewayMainAskUserResponse:
"""Route a lead's ask-user request through the gateway main agent.""" """Route a lead's ask-user request through the dedicated gateway agent."""
import json import json
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
@@ -747,11 +757,11 @@ async def ask_user_via_gateway_main(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway is not configured for this board", detail="Gateway is not configured for this board",
) )
main_session_key = (gateway.main_session_key or "").strip() main_session_key = gateway_agent_session_key(gateway)
if not main_session_key: if not main_session_key:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway main session key is required", detail="Gateway agent session key is required",
) )
config = GatewayClientConfig(url=gateway.url, token=gateway.token) config = GatewayClientConfig(url=gateway.url, token=gateway.token)
@@ -785,13 +795,8 @@ async def ask_user_via_gateway_main(
) )
try: try:
await ensure_session(main_session_key, config=config, label="Main Agent") await ensure_session(main_session_key, config=config, label="Gateway Agent")
await send_message( await send_message(message, session_key=main_session_key, config=config, deliver=True)
message,
session_key=main_session_key,
config=config,
deliver=True,
)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
record_activity( record_activity(
session, session,
@@ -808,7 +813,7 @@ async def ask_user_via_gateway_main(
record_activity( record_activity(
session, session,
event_type="gateway.lead.ask_user.sent", event_type="gateway.lead.ask_user.sent",
message=f"Lead requested user info via gateway main for board: {board.name}.", message=f"Lead requested user info via gateway agent for board: {board.name}.",
agent_id=agent_ctx.agent.id, agent_id=agent_ctx.agent.id,
) )
@@ -871,7 +876,7 @@ async def message_gateway_board_lead(
f"From agent: {agent_ctx.agent.name}\n" f"From agent: {agent_ctx.agent.name}\n"
f"{correlation_line}\n" f"{correlation_line}\n"
f"{payload.content.strip()}\n\n" f"{payload.content.strip()}\n\n"
"Reply to the gateway main by writing a NON-chat memory item on this board:\n" "Reply to the gateway agent by writing a NON-chat memory item on this board:\n"
f"POST {base_url}/api/v1/agent/boards/{board.id}/memory\n" f"POST {base_url}/api/v1/agent/boards/{board.id}/memory\n"
f'Body: {{"content":"...","tags":{tags_json},"source":"{reply_source}"}}\n' f'Body: {{"content":"...","tags":{tags_json},"source":"{reply_source}"}}\n'
"Do NOT reply in OpenClaw chat." "Do NOT reply in OpenClaw chat."
@@ -964,7 +969,7 @@ async def broadcast_gateway_lead_message(
f"From agent: {agent_ctx.agent.name}\n" f"From agent: {agent_ctx.agent.name}\n"
f"{correlation_line}\n" f"{correlation_line}\n"
f"{payload.content.strip()}\n\n" f"{payload.content.strip()}\n\n"
"Reply to the gateway main by writing a NON-chat memory item " "Reply to the gateway agent by writing a NON-chat memory item "
"on this board:\n" "on this board:\n"
f"POST {base_url}/api/v1/agent/boards/{target_board.id}/memory\n" f"POST {base_url}/api/v1/agent/boards/{target_board.id}/memory\n"
f'Body: {{"content":"...","tags":{tags_json},' f'Body: {{"content":"...","tags":{tags_json},'

View File

@@ -49,6 +49,7 @@ from app.services.agent_provisioning import (
provision_agent, provision_agent,
provision_main_agent, provision_main_agent,
) )
from app.services.gateway_agents import gateway_agent_session_key
from app.services.organizations import ( from app.services.organizations import (
OrganizationContext, OrganizationContext,
get_active_membership, get_active_membership,
@@ -178,11 +179,6 @@ async def _require_gateway(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_id is invalid", detail="Board gateway_id is invalid",
) )
if not gateway.main_session_key:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway main_session_key is required",
)
if not gateway.url: if not gateway.url:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -206,8 +202,8 @@ def _gateway_client_config(gateway: Gateway) -> GatewayClientConfig:
async def _get_gateway_main_session_keys(session: AsyncSession) -> set[str]: async def _get_gateway_main_session_keys(session: AsyncSession) -> set[str]:
keys = (await session.exec(select(Gateway.main_session_key))).all() gateways = await Gateway.objects.all().all(session)
return {key for key in keys if key} return {gateway_agent_session_key(gateway) for gateway in gateways}
def _is_gateway_main(agent: Agent, main_session_keys: set[str]) -> bool: def _is_gateway_main(agent: Agent, main_session_keys: set[str]) -> bool:
@@ -249,7 +245,11 @@ async def _find_gateway_for_main_session(
) -> Gateway | None: ) -> Gateway | None:
if not session_key: if not session_key:
return None return None
return await Gateway.objects.filter_by(main_session_key=session_key).first(session) gateways = await Gateway.objects.all().all(session)
for gateway in gateways:
if gateway_agent_session_key(gateway) == session_key:
return gateway
return None
async def _ensure_gateway_session( async def _ensure_gateway_session(
@@ -605,7 +605,7 @@ async def _apply_agent_update_mutations(
gateway_for_main, _ = await _require_gateway(session, board_for_main) gateway_for_main, _ = await _require_gateway(session, board_for_main)
updates["board_id"] = None updates["board_id"] = None
agent.is_board_lead = False agent.is_board_lead = False
agent.openclaw_session_id = gateway_for_main.main_session_key agent.openclaw_session_id = gateway_agent_session_key(gateway_for_main)
main_gateway = gateway_for_main main_gateway = gateway_for_main
elif make_main is not None: elif make_main is not None:
agent.openclaw_session_id = None agent.openclaw_session_id = None
@@ -639,12 +639,7 @@ async def _resolve_agent_update_target(
if gateway_for_main is None: if gateway_for_main is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Main agent requires a gateway main_session_key", detail="Gateway agent requires a gateway configuration",
)
if not gateway_for_main.main_session_key:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway main_session_key is required",
) )
return _AgentUpdateProvisionTarget( return _AgentUpdateProvisionTarget(
is_main_agent=True, is_main_agent=True,
@@ -654,11 +649,6 @@ async def _resolve_agent_update_target(
) )
if make_main is None and agent.board_id is None and main_gateway is not None: if make_main is None and agent.board_id is None and main_gateway is not None:
if not main_gateway.main_session_key:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway main_session_key is required",
)
return _AgentUpdateProvisionTarget( return _AgentUpdateProvisionTarget(
is_main_agent=True, is_main_agent=True,
board=None, board=None,
@@ -723,6 +713,7 @@ async def _provision_updated_agent(
gateway=request.target.gateway, gateway=request.target.gateway,
auth_token=request.raw_token, auth_token=request.raw_token,
user=request.user, user=request.user,
session_key=agent.openclaw_session_id,
options=ProvisionOptions( options=ProvisionOptions(
action="update", action="update",
force_bootstrap=request.force_bootstrap, force_bootstrap=request.force_bootstrap,
@@ -970,9 +961,11 @@ async def list_agents(
else: else:
base_filter: ColumnElement[bool] = col(Agent.board_id).in_(board_ids) base_filter: ColumnElement[bool] = col(Agent.board_id).in_(board_ids)
if is_org_admin(ctx.member): if is_org_admin(ctx.member):
gateway_keys = select(Gateway.main_session_key).where( gateways = await Gateway.objects.filter_by(
col(Gateway.organization_id) == ctx.organization.id, organization_id=ctx.organization.id,
) ).all(session)
gateway_keys = [gateway_agent_session_key(gateway) for gateway in gateways]
if gateway_keys:
base_filter = or_( base_filter = or_(
base_filter, base_filter,
col(Agent.openclaw_session_id).in_(gateway_keys), col(Agent.openclaw_session_id).in_(gateway_keys),
@@ -1309,9 +1302,9 @@ async def delete_agent(
await session.delete(agent) await session.delete(agent)
await session.commit() await session.commit()
# Always ask the main agent to confirm workspace cleanup. # Always ask the gateway agent to confirm workspace cleanup.
try: try:
main_session = gateway.main_session_key main_session = gateway_agent_session_key(gateway)
if main_session and workspace_path: if main_session and workspace_path:
cleanup_message = ( cleanup_message = (
"Cleanup request for deleted agent.\n\n" "Cleanup request for deleted agent.\n\n"
@@ -1322,7 +1315,7 @@ async def delete_agent(
"1) Remove the workspace directory.\n" "1) Remove the workspace directory.\n"
"2) Reply NO_REPLY.\n" "2) Reply NO_REPLY.\n"
) )
await ensure_session(main_session, config=client_config, label="Main Agent") await ensure_session(main_session, config=client_config, label="Gateway Agent")
await send_message( await send_message(
cleanup_message, cleanup_message,
session_key=main_session, session_key=main_session,

View File

@@ -36,6 +36,7 @@ from app.schemas.board_onboarding import (
) )
from app.schemas.boards import BoardRead from app.schemas.boards import BoardRead
from app.services.board_leads import LeadAgentOptions, LeadAgentRequest, ensure_board_lead_agent from app.services.board_leads import LeadAgentOptions, LeadAgentRequest, ensure_board_lead_agent
from app.services.gateway_agents import gateway_agent_session_key
if TYPE_CHECKING: if TYPE_CHECKING:
from sqlmodel.ext.asyncio.session import AsyncSession from sqlmodel.ext.asyncio.session import AsyncSession
@@ -60,7 +61,7 @@ async def _gateway_config(
if not board.gateway_id: if not board.gateway_id:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
gateway = await Gateway.objects.by_id(board.gateway_id).first(session) gateway = await Gateway.objects.by_id(board.gateway_id).first(session)
if gateway is None or not gateway.url or not gateway.main_session_key: if gateway is None or not gateway.url:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token) return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token)
@@ -168,7 +169,7 @@ async def start_onboarding(
board: Board = BOARD_USER_WRITE_DEP, board: Board = BOARD_USER_WRITE_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
) -> BoardOnboardingSession: ) -> BoardOnboardingSession:
"""Start onboarding and send instructions to the gateway main agent.""" """Start onboarding and send instructions to the gateway agent."""
onboarding = ( onboarding = (
await BoardOnboardingSession.objects.filter_by(board_id=board.id) await BoardOnboardingSession.objects.filter_by(board_id=board.id)
.filter(col(BoardOnboardingSession.status) == "active") .filter(col(BoardOnboardingSession.status) == "active")
@@ -178,12 +179,12 @@ async def start_onboarding(
return onboarding return onboarding
gateway, config = await _gateway_config(session, board) gateway, config = await _gateway_config(session, board)
session_key = gateway.main_session_key session_key = gateway_agent_session_key(gateway)
base_url = settings.base_url or "http://localhost:8000" base_url = settings.base_url or "http://localhost:8000"
prompt = ( prompt = (
"BOARD ONBOARDING REQUEST\n\n" "BOARD ONBOARDING REQUEST\n\n"
f"Board Name: {board.name}\n" f"Board Name: {board.name}\n"
"You are the main agent. Ask the user 6-10 focused questions total:\n" "You are the gateway agent. Ask the user 6-10 focused questions total:\n"
"- 3-6 questions to clarify the board goal.\n" "- 3-6 questions to clarify the board goal.\n"
"- 1 question to choose a unique name for the board lead agent " "- 1 question to choose a unique name for the board lead agent "
"(first-name style).\n" "(first-name style).\n"
@@ -246,7 +247,7 @@ async def start_onboarding(
) )
try: try:
await ensure_session(session_key, config=config, label="Main Agent") await ensure_session(session_key, config=config, label="Gateway Agent")
await send_message( await send_message(
prompt, prompt,
session_key=session_key, session_key=session_key,
@@ -279,7 +280,7 @@ async def answer_onboarding(
board: Board = BOARD_USER_WRITE_DEP, board: Board = BOARD_USER_WRITE_DEP,
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
) -> BoardOnboardingSession: ) -> BoardOnboardingSession:
"""Send a user onboarding answer to the gateway main agent.""" """Send a user onboarding answer to the gateway agent."""
onboarding = ( onboarding = (
await BoardOnboardingSession.objects.filter_by(board_id=board.id) await BoardOnboardingSession.objects.filter_by(board_id=board.id)
.order_by(col(BoardOnboardingSession.updated_at).desc()) .order_by(col(BoardOnboardingSession.updated_at).desc())
@@ -299,7 +300,7 @@ async def answer_onboarding(
) )
try: try:
await ensure_session(onboarding.session_key, config=config, label="Main Agent") await ensure_session(onboarding.session_key, config=config, label="Gateway Agent")
await send_message( await send_message(
answer_text, answer_text,
session_key=onboarding.session_key, session_key=onboarding.session_key,
@@ -327,7 +328,7 @@ async def agent_onboarding_update(
session: AsyncSession = SESSION_DEP, session: AsyncSession = SESSION_DEP,
actor: ActorContext = ACTOR_DEP, actor: ActorContext = ACTOR_DEP,
) -> BoardOnboardingSession: ) -> BoardOnboardingSession:
"""Store onboarding updates submitted by the gateway main agent.""" """Store onboarding updates submitted by the gateway agent."""
if actor.actor_type != "agent" or actor.agent is None: if actor.actor_type != "agent" or actor.agent is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
agent = actor.agent agent = actor.agent
@@ -338,9 +339,8 @@ async def agent_onboarding_update(
gateway = await Gateway.objects.by_id(board.gateway_id).first(session) gateway = await Gateway.objects.by_id(board.gateway_id).first(session)
if ( if (
gateway gateway
and gateway.main_session_key
and agent.openclaw_session_id and agent.openclaw_session_id
and agent.openclaw_session_id != gateway.main_session_key and agent.openclaw_session_id != gateway_agent_session_key(gateway)
): ):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)

View File

@@ -47,6 +47,7 @@ from app.schemas.pagination import DefaultLimitOffsetPage
from app.schemas.view_models import BoardGroupSnapshot, BoardSnapshot from app.schemas.view_models import BoardGroupSnapshot, BoardSnapshot
from app.services.board_group_snapshot import build_board_group_snapshot from app.services.board_group_snapshot import build_board_group_snapshot
from app.services.board_snapshot import build_board_snapshot from app.services.board_snapshot import build_board_snapshot
from app.services.gateway_agents import gateway_agent_session_key
from app.services.organizations import OrganizationContext, board_access_filter from app.services.organizations import OrganizationContext, board_access_filter
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -195,11 +196,6 @@ async def _board_gateway(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_id is invalid", detail="Board gateway_id is invalid",
) )
if not config.main_session_key:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Gateway main_session_key is required",
)
if not config.url: if not config.url:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -220,7 +216,7 @@ async def _cleanup_agent_on_gateway(
) -> None: ) -> None:
if agent.openclaw_session_id: if agent.openclaw_session_id:
await delete_session(agent.openclaw_session_id, config=client_config) await delete_session(agent.openclaw_session_id, config=client_config)
main_session = config.main_session_key main_session = gateway_agent_session_key(config)
workspace_root = config.workspace_root workspace_root = config.workspace_root
workspace_path = f"{workspace_root.rstrip('/')}/workspace-{_slugify(agent.name)}" workspace_path = f"{workspace_root.rstrip('/')}/workspace-{_slugify(agent.name)}"
cleanup_message = ( cleanup_message = (
@@ -234,7 +230,7 @@ async def _cleanup_agent_on_gateway(
"2) Delete any lingering session artifacts.\n" "2) Delete any lingering session artifacts.\n"
"Reply NO_REPLY." "Reply NO_REPLY."
) )
await ensure_session(main_session, config=client_config, label="Main Agent") await ensure_session(main_session, config=client_config, label="Gateway Agent")
await send_message( await send_message(
cleanup_message, cleanup_message,
session_key=main_session, session_key=main_session,

View File

@@ -35,6 +35,7 @@ from app.schemas.gateway_api import (
GatewaySessionsResponse, GatewaySessionsResponse,
GatewaysStatusResponse, GatewaysStatusResponse,
) )
from app.services.gateway_agents import gateway_agent_session_key
from app.services.organizations import OrganizationContext, require_board_access from app.services.organizations import OrganizationContext, require_board_access
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -47,11 +48,18 @@ SESSION_DEP = Depends(get_session)
AUTH_DEP = Depends(get_auth_context) AUTH_DEP = Depends(get_auth_context)
ORG_ADMIN_DEP = Depends(require_org_admin) ORG_ADMIN_DEP = Depends(require_org_admin)
BOARD_ID_QUERY = Query(default=None) BOARD_ID_QUERY = Query(default=None)
RESOLVE_QUERY_DEP = Depends()
def _query_to_resolve_input(params: GatewayResolveQuery) -> GatewayResolveQuery: def _query_to_resolve_input(
return params board_id: str | None = Query(default=None),
gateway_url: str | None = Query(default=None),
gateway_token: str | None = Query(default=None),
) -> GatewayResolveQuery:
return GatewayResolveQuery(
board_id=board_id,
gateway_url=gateway_url,
gateway_token=gateway_token,
)
RESOLVE_INPUT_DEP = Depends(_query_to_resolve_input) RESOLVE_INPUT_DEP = Depends(_query_to_resolve_input)
@@ -81,7 +89,7 @@ async def _resolve_gateway(
return ( return (
None, None,
GatewayClientConfig(url=params.gateway_url, token=params.gateway_token), GatewayClientConfig(url=params.gateway_url, token=params.gateway_token),
params.gateway_main_session_key, None,
) )
if not params.board_id: if not params.board_id:
raise HTTPException( raise HTTPException(
@@ -115,7 +123,7 @@ async def _resolve_gateway(
return ( return (
board, board,
GatewayClientConfig(url=gateway.url, token=gateway.token), GatewayClientConfig(url=gateway.url, token=gateway.token),
gateway.main_session_key, gateway_agent_session_key(gateway),
) )
@@ -167,7 +175,7 @@ async def gateways_status(
ensured = await ensure_session( ensured = await ensure_session(
main_session, main_session,
config=config, config=config,
label="Main Agent", label="Gateway Agent",
) )
if isinstance(ensured, dict): if isinstance(ensured, dict):
main_session_entry = ensured.get("entry") or ensured main_session_entry = ensured.get("entry") or ensured
@@ -224,7 +232,7 @@ async def list_gateway_sessions(
ensured = await ensure_session( ensured = await ensure_session(
main_session, main_session,
config=config, config=config,
label="Main Agent", label="Gateway Agent",
) )
if isinstance(ensured, dict): if isinstance(ensured, dict):
main_session_entry = ensured.get("entry") or ensured main_session_entry = ensured.get("entry") or ensured
@@ -256,7 +264,7 @@ async def _with_main_session(
if not main_session or any(item.get("key") == main_session for item in sessions_list): if not main_session or any(item.get("key") == main_session for item in sessions_list):
return sessions_list return sessions_list
try: try:
await ensure_session(main_session, config=config, label="Main Agent") await ensure_session(main_session, config=config, label="Gateway Agent")
return await _list_sessions(config) return await _list_sessions(config)
except OpenClawGatewayError: except OpenClawGatewayError:
return sessions_list return sessions_list
@@ -300,7 +308,7 @@ async def get_gateway_session(
ensured = await ensure_session( ensured = await ensure_session(
main_session, main_session,
config=config, config=config,
label="Main Agent", label="Gateway Agent",
) )
if isinstance(ensured, dict): if isinstance(ensured, dict):
session_entry = ensured.get("entry") or ensured session_entry = ensured.get("entry") or ensured
@@ -360,7 +368,7 @@ async def send_gateway_session_message(
await require_board_access(session, user=auth.user, board=board, write=True) await require_board_access(session, user=auth.user, board=board, write=True)
try: try:
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="Gateway Agent")
await send_message(payload.content, session_key=session_id, config=config) await send_message(payload.content, session_key=session_id, config=config)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
raise HTTPException( raise HTTPException(

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from uuid import UUID from uuid import UUID, uuid4
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import col from sqlmodel import col
@@ -34,6 +34,7 @@ from app.services.agent_provisioning import (
ProvisionOptions, ProvisionOptions,
provision_main_agent, provision_main_agent,
) )
from app.services.gateway_agents import gateway_agent_session_key, gateway_agent_session_key_for_id
from app.services.template_sync import GatewayTemplateSyncOptions from app.services.template_sync import GatewayTemplateSyncOptions
from app.services.template_sync import sync_gateway_templates as sync_gateway_templates_service from app.services.template_sync import sync_gateway_templates as sync_gateway_templates_service
@@ -85,7 +86,7 @@ SYNC_QUERY_DEP = Depends(_template_sync_query)
def _main_agent_name(gateway: Gateway) -> str: def _main_agent_name(gateway: Gateway) -> str:
return f"{gateway.name} Main" return f"{gateway.name} Gateway Agent"
async def _require_gateway( async def _require_gateway(
@@ -113,6 +114,15 @@ async def _find_main_agent(
previous_name: str | None = None, previous_name: str | None = None,
previous_session_key: str | None = None, previous_session_key: str | None = None,
) -> Agent | None: ) -> Agent | None:
preferred_session_key = gateway_agent_session_key(gateway)
if preferred_session_key:
agent = await Agent.objects.filter_by(
openclaw_session_id=preferred_session_key,
).first(
session,
)
if agent:
return agent
if gateway.main_session_key: if gateway.main_session_key:
agent = await Agent.objects.filter_by( agent = await Agent.objects.filter_by(
openclaw_session_id=gateway.main_session_key, openclaw_session_id=gateway.main_session_key,
@@ -147,8 +157,13 @@ async def _ensure_main_agent(
previous: tuple[str | None, str | None] | None = None, previous: tuple[str | None, str | None] | None = None,
action: str = "provision", action: str = "provision",
) -> Agent | None: ) -> Agent | None:
if not gateway.url or not gateway.main_session_key: if not gateway.url:
return None return None
session_key = gateway_agent_session_key(gateway)
if gateway.main_session_key != session_key:
gateway.main_session_key = session_key
gateway.updated_at = utcnow()
session.add(gateway)
agent = await _find_main_agent( agent = await _find_main_agent(
session, session,
gateway, gateway,
@@ -161,17 +176,17 @@ async def _ensure_main_agent(
status="provisioning", status="provisioning",
board_id=None, board_id=None,
is_board_lead=False, is_board_lead=False,
openclaw_session_id=gateway.main_session_key, openclaw_session_id=session_key,
heartbeat_config=DEFAULT_HEARTBEAT_CONFIG.copy(), heartbeat_config=DEFAULT_HEARTBEAT_CONFIG.copy(),
identity_profile={ identity_profile={
"role": "Main Agent", "role": "Gateway Agent",
"communication_style": "direct, concise, practical", "communication_style": "direct, concise, practical",
"emoji": ":compass:", "emoji": ":compass:",
}, },
) )
session.add(agent) session.add(agent)
agent.name = _main_agent_name(gateway) agent.name = _main_agent_name(gateway)
agent.openclaw_session_id = gateway.main_session_key agent.openclaw_session_id = session_key
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)
agent.provision_requested_at = utcnow() agent.provision_requested_at = utcnow()
@@ -189,11 +204,12 @@ async def _ensure_main_agent(
gateway=gateway, gateway=gateway,
auth_token=raw_token, auth_token=raw_token,
user=auth.user, user=auth.user,
session_key=session_key,
options=ProvisionOptions(action=action), options=ProvisionOptions(action=action),
), ),
) )
await ensure_session( await ensure_session(
gateway.main_session_key, session_key,
config=GatewayClientConfig(url=gateway.url, token=gateway.token), config=GatewayClientConfig(url=gateway.url, token=gateway.token),
label=agent.name, label=agent.name,
) )
@@ -204,7 +220,7 @@ async def _ensure_main_agent(
"If BOOTSTRAP.md exists, run it once then delete it. " "If BOOTSTRAP.md exists, run it once then delete it. "
"Begin heartbeats after startup." "Begin heartbeats after startup."
), ),
session_key=gateway.main_session_key, session_key=session_key,
config=GatewayClientConfig(url=gateway.url, token=gateway.token), config=GatewayClientConfig(url=gateway.url, token=gateway.token),
deliver=True, deliver=True,
) )
@@ -237,7 +253,10 @@ async def create_gateway(
) -> Gateway: ) -> Gateway:
"""Create a gateway and provision or refresh its main agent.""" """Create a gateway and provision or refresh its main agent."""
data = payload.model_dump() data = payload.model_dump()
gateway_id = uuid4()
data["id"] = gateway_id
data["organization_id"] = ctx.organization.id data["organization_id"] = ctx.organization.id
data["main_session_key"] = gateway_agent_session_key_for_id(gateway_id)
gateway = await crud.create(session, Gateway, **data) gateway = await crud.create(session, Gateway, **data)
await _ensure_main_agent(session, gateway, auth, action="provision") await _ensure_main_agent(session, gateway, auth, action="provision")
return gateway return gateway

View File

@@ -21,7 +21,6 @@ class GatewayResolveQuery(SQLModel):
board_id: str | None = None board_id: str | None = None
gateway_url: str | None = None gateway_url: str | None = None
gateway_token: str | None = None gateway_token: str | None = None
gateway_main_session_key: str | None = None
class GatewaysStatusResponse(SQLModel): class GatewaysStatusResponse(SQLModel):

View File

@@ -16,7 +16,6 @@ class GatewayBase(SQLModel):
name: str name: str
url: str url: str
main_session_key: str
workspace_root: str workspace_root: str
@@ -43,7 +42,6 @@ class GatewayUpdate(SQLModel):
name: str | None = None name: str | None = None
url: str | None = None url: str | None = None
token: str | None = None token: str | None = None
main_session_key: str | None = None
workspace_root: str | None = None workspace_root: str | None = None
@field_validator("token", mode="before") @field_validator("token", mode="before")
@@ -64,6 +62,7 @@ class GatewayRead(GatewayBase):
id: UUID id: UUID
organization_id: UUID organization_id: UUID
token: str | None = None token: str | None = None
main_session_key: str
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime

View File

@@ -16,6 +16,7 @@ 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 OpenClawGatewayError, ensure_session, openclaw_call from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, openclaw_call
from app.services.gateway_agents import gateway_agent_session_key
if TYPE_CHECKING: if TYPE_CHECKING:
from app.models.agents import Agent from app.models.agents import Agent
@@ -124,6 +125,7 @@ class MainAgentProvisionRequest:
gateway: Gateway gateway: Gateway
auth_token: str auth_token: str
user: User | None user: User | None
session_key: str | None = None
options: ProvisionOptions = field(default_factory=ProvisionOptions) options: ProvisionOptions = field(default_factory=ProvisionOptions)
@@ -307,15 +309,12 @@ def _build_context(
if not gateway.workspace_root: if not gateway.workspace_root:
msg = "gateway_workspace_root is required" msg = "gateway_workspace_root is required"
raise ValueError(msg) raise ValueError(msg)
if not gateway.main_session_key:
msg = "gateway_main_session_key is required"
raise ValueError(msg)
agent_id = str(agent.id) agent_id = str(agent.id)
workspace_root = gateway.workspace_root workspace_root = gateway.workspace_root
workspace_path = _workspace_path(agent, workspace_root) workspace_path = _workspace_path(agent, 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 = gateway.main_session_key main_session_key = gateway_agent_session_key(gateway)
identity_profile: dict[str, Any] = {} identity_profile: dict[str, Any] = {}
if isinstance(agent.identity_profile, dict): if isinstance(agent.identity_profile, dict):
identity_profile = agent.identity_profile identity_profile = agent.identity_profile
@@ -411,7 +410,7 @@ def _build_main_context(
"session_key": agent.openclaw_session_id or "", "session_key": agent.openclaw_session_id or "",
"base_url": base_url, "base_url": base_url,
"auth_token": auth_token, "auth_token": auth_token,
"main_session_key": gateway.main_session_key or "", "main_session_key": gateway_agent_session_key(gateway),
"workspace_root": gateway.workspace_root or "", "workspace_root": gateway.workspace_root or "",
"user_name": (user.name or "") if user else "", "user_name": (user.name or "") if user else "",
"user_preferred_name": preferred_name, "user_preferred_name": preferred_name,
@@ -870,19 +869,29 @@ async def provision_main_agent(
gateway = request.gateway gateway = request.gateway
if not gateway.url: if not gateway.url:
return return
if not gateway.main_session_key: session_key = (request.session_key or gateway.main_session_key or "").strip()
msg = "gateway main_session_key is required" if not session_key:
msg = "gateway main agent session_key is required"
raise ValueError(msg) raise ValueError(msg)
client_config = GatewayClientConfig(url=gateway.url, token=gateway.token) client_config = GatewayClientConfig(url=gateway.url, token=gateway.token)
await ensure_session( await ensure_session(
gateway.main_session_key, session_key,
config=client_config, config=client_config,
label="Main Agent", label=agent.name or "Gateway Agent",
) )
agent_id = _agent_id_from_session_key(session_key)
if agent_id:
if not gateway.workspace_root:
msg = "gateway_workspace_root is required"
raise ValueError(msg)
workspace_path = _workspace_path(agent, gateway.workspace_root)
heartbeat = _heartbeat_config(agent)
await _patch_gateway_agent_list(agent_id, workspace_path, heartbeat, client_config)
else:
agent_id = await _gateway_default_agent_id( agent_id = await _gateway_default_agent_id(
client_config, client_config,
fallback_session_key=gateway.main_session_key, fallback_session_key=session_key,
) )
if not agent_id: if not agent_id:
msg = "Unable to resolve gateway main agent id" msg = "Unable to resolve gateway main agent id"
@@ -912,7 +921,7 @@ async def provision_main_agent(
client_config=client_config, client_config=client_config,
) )
if request.options.reset_session: if request.options.reset_session:
await _reset_session(gateway.main_session_key, client_config) await _reset_session(session_key, client_config)
async def cleanup_agent( async def cleanup_agent(

View File

@@ -19,6 +19,7 @@ from app.schemas.approvals import ApprovalRead
from app.schemas.board_memory import BoardMemoryRead from app.schemas.board_memory import BoardMemoryRead
from app.schemas.boards import BoardRead from app.schemas.boards import BoardRead
from app.schemas.view_models import BoardSnapshot, TaskCardRead from app.schemas.view_models import BoardSnapshot, TaskCardRead
from app.services.gateway_agents import gateway_agent_session_key
from app.services.task_dependencies import ( from app.services.task_dependencies import (
blocked_by_dependency_ids, blocked_by_dependency_ids,
dependency_ids_by_task_id, dependency_ids_by_task_id,
@@ -47,8 +48,8 @@ def _computed_agent_status(agent: Agent) -> str:
async def _gateway_main_session_keys(session: AsyncSession) -> set[str]: async def _gateway_main_session_keys(session: AsyncSession) -> set[str]:
keys = (await session.exec(select(Gateway.main_session_key))).all() gateways = await Gateway.objects.all().all(session)
return {key for key in keys if key} return {gateway_agent_session_key(gateway) for gateway in gateways}
def _agent_to_read(agent: Agent, main_session_keys: set[str]) -> AgentRead: def _agent_to_read(agent: Agent, main_session_keys: set[str]) -> AgentRead:

View File

@@ -0,0 +1,34 @@
"""Helpers for dedicated gateway-scoped agent identity/session keys."""
from __future__ import annotations
from uuid import UUID
from app.models.gateways import Gateway
_GATEWAY_AGENT_PREFIX = "agent:gateway-"
_GATEWAY_AGENT_SUFFIX = ":main"
def gateway_agent_session_key_for_id(gateway_id: UUID) -> str:
"""Return the dedicated Mission Control gateway-agent session key for an id."""
return f"{_GATEWAY_AGENT_PREFIX}{gateway_id}{_GATEWAY_AGENT_SUFFIX}"
def gateway_agent_session_key(gateway: Gateway) -> str:
"""Return the dedicated Mission Control gateway-agent session key."""
return gateway_agent_session_key_for_id(gateway.id)
def parse_gateway_agent_session_key(session_key: str | None) -> UUID | None:
"""Parse a gateway id from a dedicated gateway-agent session key."""
value = (session_key or "").strip()
if not (value.startswith(_GATEWAY_AGENT_PREFIX) and value.endswith(_GATEWAY_AGENT_SUFFIX)):
return None
gateway_id = value[len(_GATEWAY_AGENT_PREFIX) : -len(_GATEWAY_AGENT_SUFFIX)]
if not gateway_id:
return None
try:
return UUID(gateway_id)
except ValueError:
return None

View File

@@ -31,6 +31,7 @@ from app.services.agent_provisioning import (
provision_agent, provision_agent,
provision_main_agent, provision_main_agent,
) )
from app.services.gateway_agents import gateway_agent_session_key
_TOOLS_KV_RE = re.compile(r"^(?P<key>[A-Z0-9_]+)=(?P<value>.*)$") _TOOLS_KV_RE = re.compile(r"^(?P<key>[A-Z0-9_]+)=(?P<value>.*)$")
SESSION_KEY_PARTS_MIN = 2 SESSION_KEY_PARTS_MIN = 2
@@ -520,21 +521,22 @@ async def _sync_main_agent(
ctx: _SyncContext, ctx: _SyncContext,
result: GatewayTemplatesSyncResult, result: GatewayTemplatesSyncResult,
) -> bool: ) -> bool:
main_session_key = gateway_agent_session_key(ctx.gateway)
main_agent = ( main_agent = (
await Agent.objects.all() await Agent.objects.all()
.filter(col(Agent.openclaw_session_id) == ctx.gateway.main_session_key) .filter(col(Agent.openclaw_session_id) == main_session_key)
.first(ctx.session) .first(ctx.session)
) )
if main_agent is None: if main_agent is None:
_append_sync_error( _append_sync_error(
result, result,
message=("Gateway main agent record not found; " "skipping main agent template sync."), message=("Gateway agent record not found; " "skipping gateway agent template sync."),
) )
return True return True
try: try:
main_gateway_agent_id = await _gateway_default_agent_id( main_gateway_agent_id = await _gateway_default_agent_id(
ctx.config, ctx.config,
fallback_session_key=ctx.gateway.main_session_key, fallback_session_key=main_session_key,
backoff=ctx.backoff, backoff=ctx.backoff,
) )
except TimeoutError as exc: except TimeoutError as exc:
@@ -544,7 +546,7 @@ async def _sync_main_agent(
_append_sync_error( _append_sync_error(
result, result,
agent=main_agent, agent=main_agent,
message="Unable to resolve gateway default agent id for main agent.", message="Unable to resolve gateway agent id.",
) )
return True return True
@@ -561,7 +563,7 @@ async def _sync_main_agent(
_append_sync_error( _append_sync_error(
result, result,
agent=main_agent, agent=main_agent,
message="Skipping main agent: unable to read AUTH_TOKEN from TOOLS.md.", message="Skipping gateway agent: unable to read AUTH_TOKEN from TOOLS.md.",
) )
return True return True
stop_sync = False stop_sync = False
@@ -574,6 +576,7 @@ async def _sync_main_agent(
gateway=ctx.gateway, gateway=ctx.gateway,
auth_token=token, auth_token=token,
user=ctx.options.user, user=ctx.options.user,
session_key=main_session_key,
options=ProvisionOptions( options=ProvisionOptions(
action="update", action="update",
force_bootstrap=ctx.options.force_bootstrap, force_bootstrap=ctx.options.force_bootstrap,
@@ -590,7 +593,7 @@ async def _sync_main_agent(
_append_sync_error( _append_sync_error(
result, result,
agent=main_agent, agent=main_agent,
message=f"Failed to sync main agent templates: {exc}", message=f"Failed to sync gateway agent templates: {exc}",
) )
else: else:
result.main_updated = True result.main_updated = True

View File

@@ -18,6 +18,7 @@ async def run() -> None:
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
from app.services.gateway_agents import gateway_agent_session_key
await init_db() await init_db()
async with async_session_maker() as session: async with async_session_maker() as session:
@@ -26,9 +27,10 @@ async def run() -> None:
name="Demo Gateway", name="Demo Gateway",
url="http://localhost:8080", url="http://localhost:8080",
token=None, token=None,
main_session_key="demo:main", main_session_key="placeholder",
workspace_root=str(demo_workspace_root), workspace_root=str(demo_workspace_root),
) )
gateway.main_session_key = gateway_agent_session_key(gateway)
session.add(gateway) session.add(gateway)
await session.commit() await session.commit()
await session.refresh(gateway) await session.refresh(gateway)

View File

@@ -3498,7 +3498,7 @@ export const useUpdateAgentSoulApiV1AgentBoardsBoardIdAgentsAgentIdSoulPut = <
); );
}; };
/** /**
* Route a lead's ask-user request through the gateway main agent. * Route a lead's ask-user request through the dedicated gateway agent.
* @summary Ask User Via Gateway Main * @summary Ask User Via Gateway Main
*/ */
export type askUserViaGatewayMainApiV1AgentBoardsBoardIdGatewayMainAskUserPostResponse200 = export type askUserViaGatewayMainApiV1AgentBoardsBoardIdGatewayMainAskUserPostResponse200 =

View File

@@ -275,7 +275,7 @@ export function useGetOnboardingApiV1BoardsBoardIdOnboardingGet<
} }
/** /**
* Start onboarding and send instructions to the gateway main agent. * Start onboarding and send instructions to the gateway agent.
* @summary Start Onboarding * @summary Start Onboarding
*/ */
export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse200 = { export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse200 = {
@@ -417,7 +417,7 @@ export const useStartOnboardingApiV1BoardsBoardIdOnboardingStartPost = <
); );
}; };
/** /**
* Send a user onboarding answer to the gateway main agent. * Send a user onboarding answer to the gateway agent.
* @summary Answer Onboarding * @summary Answer Onboarding
*/ */
export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse200 = export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse200 =
@@ -567,7 +567,7 @@ export const useAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost = <
); );
}; };
/** /**
* Store onboarding updates submitted by the gateway main agent. * Store onboarding updates submitted by the gateway agent.
* @summary Agent Onboarding Update * @summary Agent Onboarding Update
*/ */
export type agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponse200 = export type agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponse200 =

View File

@@ -24,13 +24,13 @@ import type {
GatewayCommandsResponse, GatewayCommandsResponse,
GatewayCreate, GatewayCreate,
GatewayRead, GatewayRead,
GatewayResolveQuery,
GatewaySessionHistoryResponse, GatewaySessionHistoryResponse,
GatewaySessionMessageRequest, GatewaySessionMessageRequest,
GatewaySessionResponse, GatewaySessionResponse,
GatewaySessionsResponse, GatewaySessionsResponse,
GatewayTemplatesSyncResult, GatewayTemplatesSyncResult,
GatewayUpdate, GatewayUpdate,
GatewaysStatusApiV1GatewaysStatusGetParams,
GatewaysStatusResponse, GatewaysStatusResponse,
GetGatewaySessionApiV1GatewaysSessionsSessionIdGetParams, GetGatewaySessionApiV1GatewaysSessionsSessionIdGetParams,
GetSessionHistoryApiV1GatewaysSessionsSessionIdHistoryGetParams, GetSessionHistoryApiV1GatewaysSessionsSessionIdHistoryGetParams,
@@ -74,36 +74,48 @@ export type gatewaysStatusApiV1GatewaysStatusGetResponse =
| gatewaysStatusApiV1GatewaysStatusGetResponseSuccess | gatewaysStatusApiV1GatewaysStatusGetResponseSuccess
| gatewaysStatusApiV1GatewaysStatusGetResponseError; | gatewaysStatusApiV1GatewaysStatusGetResponseError;
export const getGatewaysStatusApiV1GatewaysStatusGetUrl = () => { export const getGatewaysStatusApiV1GatewaysStatusGetUrl = (
return `/api/v1/gateways/status`; params?: GatewaysStatusApiV1GatewaysStatusGetParams,
) => {
const normalizedParams = new URLSearchParams();
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined) {
normalizedParams.append(key, value === null ? "null" : value.toString());
}
});
const stringifiedParams = normalizedParams.toString();
return stringifiedParams.length > 0
? `/api/v1/gateways/status?${stringifiedParams}`
: `/api/v1/gateways/status`;
}; };
export const gatewaysStatusApiV1GatewaysStatusGet = async ( export const gatewaysStatusApiV1GatewaysStatusGet = async (
gatewayResolveQuery: GatewayResolveQuery, params?: GatewaysStatusApiV1GatewaysStatusGetParams,
options?: RequestInit, options?: RequestInit,
): Promise<gatewaysStatusApiV1GatewaysStatusGetResponse> => { ): Promise<gatewaysStatusApiV1GatewaysStatusGetResponse> => {
return customFetch<gatewaysStatusApiV1GatewaysStatusGetResponse>( return customFetch<gatewaysStatusApiV1GatewaysStatusGetResponse>(
getGatewaysStatusApiV1GatewaysStatusGetUrl(), getGatewaysStatusApiV1GatewaysStatusGetUrl(params),
{ {
...options, ...options,
method: "GET", method: "GET",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(gatewayResolveQuery),
}, },
); );
}; };
export const getGatewaysStatusApiV1GatewaysStatusGetQueryKey = ( export const getGatewaysStatusApiV1GatewaysStatusGetQueryKey = (
gatewayResolveQuery?: GatewayResolveQuery, params?: GatewaysStatusApiV1GatewaysStatusGetParams,
) => { ) => {
return [`/api/v1/gateways/status`, gatewayResolveQuery] as const; return [`/api/v1/gateways/status`, ...(params ? [params] : [])] as const;
}; };
export const getGatewaysStatusApiV1GatewaysStatusGetQueryOptions = < export const getGatewaysStatusApiV1GatewaysStatusGetQueryOptions = <
TData = Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>, TData = Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>,
TError = HTTPValidationError, TError = HTTPValidationError,
>( >(
gatewayResolveQuery: GatewayResolveQuery, params?: GatewaysStatusApiV1GatewaysStatusGetParams,
options?: { options?: {
query?: Partial< query?: Partial<
UseQueryOptions< UseQueryOptions<
@@ -119,15 +131,12 @@ export const getGatewaysStatusApiV1GatewaysStatusGetQueryOptions = <
const queryKey = const queryKey =
queryOptions?.queryKey ?? queryOptions?.queryKey ??
getGatewaysStatusApiV1GatewaysStatusGetQueryKey(gatewayResolveQuery); getGatewaysStatusApiV1GatewaysStatusGetQueryKey(params);
const queryFn: QueryFunction< const queryFn: QueryFunction<
Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>> Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>
> = ({ signal }) => > = ({ signal }) =>
gatewaysStatusApiV1GatewaysStatusGet(gatewayResolveQuery, { gatewaysStatusApiV1GatewaysStatusGet(params, { signal, ...requestOptions });
signal,
...requestOptions,
});
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>, Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>,
@@ -146,7 +155,7 @@ export function useGatewaysStatusApiV1GatewaysStatusGet<
TData = Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>, TData = Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>,
TError = HTTPValidationError, TError = HTTPValidationError,
>( >(
gatewayResolveQuery: GatewayResolveQuery, params: undefined | GatewaysStatusApiV1GatewaysStatusGetParams,
options: { options: {
query: Partial< query: Partial<
UseQueryOptions< UseQueryOptions<
@@ -173,7 +182,7 @@ export function useGatewaysStatusApiV1GatewaysStatusGet<
TData = Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>, TData = Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>,
TError = HTTPValidationError, TError = HTTPValidationError,
>( >(
gatewayResolveQuery: GatewayResolveQuery, params?: GatewaysStatusApiV1GatewaysStatusGetParams,
options?: { options?: {
query?: Partial< query?: Partial<
UseQueryOptions< UseQueryOptions<
@@ -200,7 +209,7 @@ export function useGatewaysStatusApiV1GatewaysStatusGet<
TData = Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>, TData = Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>,
TError = HTTPValidationError, TError = HTTPValidationError,
>( >(
gatewayResolveQuery: GatewayResolveQuery, params?: GatewaysStatusApiV1GatewaysStatusGetParams,
options?: { options?: {
query?: Partial< query?: Partial<
UseQueryOptions< UseQueryOptions<
@@ -223,7 +232,7 @@ export function useGatewaysStatusApiV1GatewaysStatusGet<
TData = Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>, TData = Awaited<ReturnType<typeof gatewaysStatusApiV1GatewaysStatusGet>>,
TError = HTTPValidationError, TError = HTTPValidationError,
>( >(
gatewayResolveQuery: GatewayResolveQuery, params?: GatewaysStatusApiV1GatewaysStatusGetParams,
options?: { options?: {
query?: Partial< query?: Partial<
UseQueryOptions< UseQueryOptions<
@@ -239,7 +248,7 @@ export function useGatewaysStatusApiV1GatewaysStatusGet<
queryKey: DataTag<QueryKey, TData, TError>; queryKey: DataTag<QueryKey, TData, TError>;
} { } {
const queryOptions = getGatewaysStatusApiV1GatewaysStatusGetQueryOptions( const queryOptions = getGatewaysStatusApiV1GatewaysStatusGetQueryOptions(
gatewayResolveQuery, params,
options, options,
); );

View File

@@ -11,7 +11,6 @@
export interface GatewayCreate { export interface GatewayCreate {
name: string; name: string;
url: string; url: string;
main_session_key: string;
workspace_root: string; workspace_root: string;
token?: string | null; token?: string | null;
} }

View File

@@ -11,11 +11,11 @@
export interface GatewayRead { export interface GatewayRead {
name: string; name: string;
url: string; url: string;
main_session_key: string;
workspace_root: string; workspace_root: string;
id: string; id: string;
organization_id: string; organization_id: string;
token?: string | null; token?: string | null;
main_session_key: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }

View File

@@ -12,5 +12,4 @@ export interface GatewayResolveQuery {
board_id?: string | null; board_id?: string | null;
gateway_url?: string | null; gateway_url?: string | null;
gateway_token?: string | null; gateway_token?: string | null;
gateway_main_session_key?: string | null;
} }

View File

@@ -12,6 +12,5 @@ export interface GatewayUpdate {
name?: string | null; name?: string | null;
url?: string | null; url?: string | null;
token?: string | null; token?: string | null;
main_session_key?: string | null;
workspace_root?: string | null; workspace_root?: string | null;
} }

View File

@@ -9,5 +9,4 @@ export type GatewaysStatusApiV1GatewaysStatusGetParams = {
board_id?: string | null; board_id?: string | null;
gateway_url?: string | null; gateway_url?: string | null;
gateway_token?: string | null; gateway_token?: string | null;
gateway_main_session_key?: string | null;
}; };

View File

@@ -18,7 +18,6 @@ import type { GatewayUpdate } from "@/api/generated/model";
import { GatewayForm } from "@/components/gateways/GatewayForm"; import { GatewayForm } from "@/components/gateways/GatewayForm";
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
import { import {
DEFAULT_MAIN_SESSION_KEY,
DEFAULT_WORKSPACE_ROOT, DEFAULT_WORKSPACE_ROOT,
checkGatewayConnection, checkGatewayConnection,
type GatewayCheckStatus, type GatewayCheckStatus,
@@ -41,9 +40,6 @@ export default function EditGatewayPage() {
const [gatewayToken, setGatewayToken] = useState<string | undefined>( const [gatewayToken, setGatewayToken] = useState<string | undefined>(
undefined, undefined,
); );
const [mainSessionKey, setMainSessionKey] = useState<string | undefined>(
undefined,
);
const [workspaceRoot, setWorkspaceRoot] = useState<string | undefined>( const [workspaceRoot, setWorkspaceRoot] = useState<string | undefined>(
undefined, undefined,
); );
@@ -86,10 +82,7 @@ export default function EditGatewayPage() {
const resolvedName = name ?? loadedGateway?.name ?? ""; const resolvedName = name ?? loadedGateway?.name ?? "";
const resolvedGatewayUrl = gatewayUrl ?? loadedGateway?.url ?? ""; const resolvedGatewayUrl = gatewayUrl ?? loadedGateway?.url ?? "";
const resolvedGatewayToken = gatewayToken ?? loadedGateway?.token ?? ""; const resolvedGatewayToken = gatewayToken ?? loadedGateway?.token ?? "";
const resolvedMainSessionKey = const resolvedMainSessionKey = loadedGateway?.main_session_key ?? null;
mainSessionKey ??
loadedGateway?.main_session_key ??
DEFAULT_MAIN_SESSION_KEY;
const resolvedWorkspaceRoot = const resolvedWorkspaceRoot =
workspaceRoot ?? loadedGateway?.workspace_root ?? DEFAULT_WORKSPACE_ROOT; workspaceRoot ?? loadedGateway?.workspace_root ?? DEFAULT_WORKSPACE_ROOT;
@@ -99,7 +92,6 @@ export default function EditGatewayPage() {
const canSubmit = const canSubmit =
Boolean(resolvedName.trim()) && Boolean(resolvedName.trim()) &&
Boolean(resolvedGatewayUrl.trim()) && Boolean(resolvedGatewayUrl.trim()) &&
Boolean(resolvedMainSessionKey.trim()) &&
Boolean(resolvedWorkspaceRoot.trim()) && Boolean(resolvedWorkspaceRoot.trim()) &&
gatewayCheckStatus === "success"; gatewayCheckStatus === "success";
@@ -117,7 +109,6 @@ export default function EditGatewayPage() {
const { ok, message } = await checkGatewayConnection({ const { ok, message } = await checkGatewayConnection({
gatewayUrl: resolvedGatewayUrl, gatewayUrl: resolvedGatewayUrl,
gatewayToken: resolvedGatewayToken, gatewayToken: resolvedGatewayToken,
mainSessionKey: resolvedMainSessionKey,
}); });
setGatewayCheckStatus(ok ? "success" : "error"); setGatewayCheckStatus(ok ? "success" : "error");
setGatewayCheckMessage(message); setGatewayCheckMessage(message);
@@ -138,10 +129,6 @@ export default function EditGatewayPage() {
setGatewayCheckMessage(gatewayValidation); setGatewayCheckMessage(gatewayValidation);
return; return;
} }
if (!resolvedMainSessionKey.trim()) {
setError("Main session key is required.");
return;
}
if (!resolvedWorkspaceRoot.trim()) { if (!resolvedWorkspaceRoot.trim()) {
setError("Workspace root is required."); setError("Workspace root is required.");
return; return;
@@ -153,7 +140,6 @@ export default function EditGatewayPage() {
name: resolvedName.trim(), name: resolvedName.trim(),
url: resolvedGatewayUrl.trim(), url: resolvedGatewayUrl.trim(),
token: resolvedGatewayToken.trim() || null, token: resolvedGatewayToken.trim() || null,
main_session_key: resolvedMainSessionKey.trim(),
workspace_root: resolvedWorkspaceRoot.trim(), workspace_root: resolvedWorkspaceRoot.trim(),
}; };
@@ -187,7 +173,6 @@ export default function EditGatewayPage() {
errorMessage={errorMessage} errorMessage={errorMessage}
isLoading={isLoading} isLoading={isLoading}
canSubmit={canSubmit} canSubmit={canSubmit}
mainSessionKeyPlaceholder={DEFAULT_MAIN_SESSION_KEY}
workspaceRootPlaceholder={DEFAULT_WORKSPACE_ROOT} workspaceRootPlaceholder={DEFAULT_WORKSPACE_ROOT}
cancelLabel="Back" cancelLabel="Back"
submitLabel="Save changes" submitLabel="Save changes"
@@ -207,11 +192,6 @@ export default function EditGatewayPage() {
setGatewayCheckStatus("idle"); setGatewayCheckStatus("idle");
setGatewayCheckMessage(null); setGatewayCheckMessage(null);
}} }}
onMainSessionKeyChange={(next) => {
setMainSessionKey(next);
setGatewayCheckStatus("idle");
setGatewayCheckMessage(null);
}}
onWorkspaceRootChange={setWorkspaceRoot} onWorkspaceRootChange={setWorkspaceRoot}
/> />
</DashboardPageLayout> </DashboardPageLayout>

View File

@@ -67,7 +67,6 @@ export default function GatewayDetailPage() {
? { ? {
gateway_url: gateway.url, gateway_url: gateway.url,
gateway_token: gateway.token ?? undefined, gateway_token: gateway.token ?? undefined,
gateway_main_session_key: gateway.main_session_key ?? undefined,
} }
: {}; : {};

View File

@@ -13,7 +13,6 @@ import { useOrganizationMembership } from "@/lib/use-organization-membership";
import { GatewayForm } from "@/components/gateways/GatewayForm"; import { GatewayForm } from "@/components/gateways/GatewayForm";
import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"; import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout";
import { import {
DEFAULT_MAIN_SESSION_KEY,
DEFAULT_WORKSPACE_ROOT, DEFAULT_WORKSPACE_ROOT,
checkGatewayConnection, checkGatewayConnection,
type GatewayCheckStatus, type GatewayCheckStatus,
@@ -29,9 +28,6 @@ export default function NewGatewayPage() {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [gatewayUrl, setGatewayUrl] = useState(""); const [gatewayUrl, setGatewayUrl] = useState("");
const [gatewayToken, setGatewayToken] = useState(""); const [gatewayToken, setGatewayToken] = useState("");
const [mainSessionKey, setMainSessionKey] = useState(
DEFAULT_MAIN_SESSION_KEY,
);
const [workspaceRoot, setWorkspaceRoot] = useState(DEFAULT_WORKSPACE_ROOT); const [workspaceRoot, setWorkspaceRoot] = useState(DEFAULT_WORKSPACE_ROOT);
const [gatewayUrlError, setGatewayUrlError] = useState<string | null>(null); const [gatewayUrlError, setGatewayUrlError] = useState<string | null>(null);
@@ -61,7 +57,6 @@ export default function NewGatewayPage() {
const canSubmit = const canSubmit =
Boolean(name.trim()) && Boolean(name.trim()) &&
Boolean(gatewayUrl.trim()) && Boolean(gatewayUrl.trim()) &&
Boolean(mainSessionKey.trim()) &&
Boolean(workspaceRoot.trim()) && Boolean(workspaceRoot.trim()) &&
gatewayCheckStatus === "success"; gatewayCheckStatus === "success";
@@ -79,7 +74,6 @@ export default function NewGatewayPage() {
const { ok, message } = await checkGatewayConnection({ const { ok, message } = await checkGatewayConnection({
gatewayUrl, gatewayUrl,
gatewayToken, gatewayToken,
mainSessionKey,
}); });
setGatewayCheckStatus(ok ? "success" : "error"); setGatewayCheckStatus(ok ? "success" : "error");
setGatewayCheckMessage(message); setGatewayCheckMessage(message);
@@ -100,10 +94,6 @@ export default function NewGatewayPage() {
setGatewayCheckMessage(gatewayValidation); setGatewayCheckMessage(gatewayValidation);
return; return;
} }
if (!mainSessionKey.trim()) {
setError("Main session key is required.");
return;
}
if (!workspaceRoot.trim()) { if (!workspaceRoot.trim()) {
setError("Workspace root is required."); setError("Workspace root is required.");
return; return;
@@ -115,7 +105,6 @@ export default function NewGatewayPage() {
name: name.trim(), name: name.trim(),
url: gatewayUrl.trim(), url: gatewayUrl.trim(),
token: gatewayToken.trim() || null, token: gatewayToken.trim() || null,
main_session_key: mainSessionKey.trim(),
workspace_root: workspaceRoot.trim(), workspace_root: workspaceRoot.trim(),
}, },
}); });
@@ -136,7 +125,7 @@ export default function NewGatewayPage() {
name={name} name={name}
gatewayUrl={gatewayUrl} gatewayUrl={gatewayUrl}
gatewayToken={gatewayToken} gatewayToken={gatewayToken}
mainSessionKey={mainSessionKey} mainSessionKey={null}
workspaceRoot={workspaceRoot} workspaceRoot={workspaceRoot}
gatewayUrlError={gatewayUrlError} gatewayUrlError={gatewayUrlError}
gatewayCheckStatus={gatewayCheckStatus} gatewayCheckStatus={gatewayCheckStatus}
@@ -144,7 +133,6 @@ export default function NewGatewayPage() {
errorMessage={error} errorMessage={error}
isLoading={isLoading} isLoading={isLoading}
canSubmit={canSubmit} canSubmit={canSubmit}
mainSessionKeyPlaceholder={DEFAULT_MAIN_SESSION_KEY}
workspaceRootPlaceholder={DEFAULT_WORKSPACE_ROOT} workspaceRootPlaceholder={DEFAULT_WORKSPACE_ROOT}
cancelLabel="Cancel" cancelLabel="Cancel"
submitLabel="Create gateway" submitLabel="Create gateway"
@@ -164,11 +152,6 @@ export default function NewGatewayPage() {
setGatewayCheckStatus("idle"); setGatewayCheckStatus("idle");
setGatewayCheckMessage(null); setGatewayCheckMessage(null);
}} }}
onMainSessionKeyChange={(next) => {
setMainSessionKey(next);
setGatewayCheckStatus("idle");
setGatewayCheckMessage(null);
}}
onWorkspaceRootChange={setWorkspaceRoot} onWorkspaceRootChange={setWorkspaceRoot}
/> />
</DashboardPageLayout> </DashboardPageLayout>

View File

@@ -9,7 +9,7 @@ type GatewayFormProps = {
name: string; name: string;
gatewayUrl: string; gatewayUrl: string;
gatewayToken: string; gatewayToken: string;
mainSessionKey: string; mainSessionKey: string | null;
workspaceRoot: string; workspaceRoot: string;
gatewayUrlError: string | null; gatewayUrlError: string | null;
gatewayCheckStatus: GatewayCheckStatus; gatewayCheckStatus: GatewayCheckStatus;
@@ -17,7 +17,6 @@ type GatewayFormProps = {
errorMessage: string | null; errorMessage: string | null;
isLoading: boolean; isLoading: boolean;
canSubmit: boolean; canSubmit: boolean;
mainSessionKeyPlaceholder: string;
workspaceRootPlaceholder: string; workspaceRootPlaceholder: string;
cancelLabel: string; cancelLabel: string;
submitLabel: string; submitLabel: string;
@@ -28,7 +27,6 @@ type GatewayFormProps = {
onNameChange: (next: string) => void; onNameChange: (next: string) => void;
onGatewayUrlChange: (next: string) => void; onGatewayUrlChange: (next: string) => void;
onGatewayTokenChange: (next: string) => void; onGatewayTokenChange: (next: string) => void;
onMainSessionKeyChange: (next: string) => void;
onWorkspaceRootChange: (next: string) => void; onWorkspaceRootChange: (next: string) => void;
}; };
@@ -44,7 +42,6 @@ export function GatewayForm({
errorMessage, errorMessage,
isLoading, isLoading,
canSubmit, canSubmit,
mainSessionKeyPlaceholder,
workspaceRootPlaceholder, workspaceRootPlaceholder,
cancelLabel, cancelLabel,
submitLabel, submitLabel,
@@ -55,7 +52,6 @@ export function GatewayForm({
onNameChange, onNameChange,
onGatewayUrlChange, onGatewayUrlChange,
onGatewayTokenChange, onGatewayTokenChange,
onMainSessionKeyChange,
onWorkspaceRootChange, onWorkspaceRootChange,
}: GatewayFormProps) { }: GatewayFormProps) {
return ( return (
@@ -137,13 +133,12 @@ export function GatewayForm({
<div className="grid gap-6 md:grid-cols-2"> <div className="grid gap-6 md:grid-cols-2">
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium text-slate-900"> <label className="text-sm font-medium text-slate-900">
Main session key <span className="text-red-500">*</span> Main session key (read-only)
</label> </label>
<Input <Input
value={mainSessionKey} value={mainSessionKey ?? "Auto-generated by server"}
onChange={(event) => onMainSessionKeyChange(event.target.value)} readOnly
placeholder={mainSessionKeyPlaceholder} disabled
disabled={isLoading}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">

View File

@@ -1,6 +1,5 @@
import { gatewaysStatusApiV1GatewaysStatusGet } from "@/api/generated/gateways/gateways"; import { gatewaysStatusApiV1GatewaysStatusGet } from "@/api/generated/gateways/gateways";
export const DEFAULT_MAIN_SESSION_KEY = "agent:main:main";
export const DEFAULT_WORKSPACE_ROOT = "~/.openclaw"; export const DEFAULT_WORKSPACE_ROOT = "~/.openclaw";
export type GatewayCheckStatus = "idle" | "checking" | "success" | "error"; export type GatewayCheckStatus = "idle" | "checking" | "success" | "error";
@@ -25,7 +24,6 @@ export const validateGatewayUrl = (value: string) => {
export async function checkGatewayConnection(params: { export async function checkGatewayConnection(params: {
gatewayUrl: string; gatewayUrl: string;
gatewayToken: string; gatewayToken: string;
mainSessionKey: string;
}): Promise<{ ok: boolean; message: string }> { }): Promise<{ ok: boolean; message: string }> {
try { try {
const requestParams: Record<string, string> = { const requestParams: Record<string, string> = {
@@ -34,9 +32,6 @@ export async function checkGatewayConnection(params: {
if (params.gatewayToken.trim()) { if (params.gatewayToken.trim()) {
requestParams.gateway_token = params.gatewayToken.trim(); requestParams.gateway_token = params.gatewayToken.trim();
} }
if (params.mainSessionKey.trim()) {
requestParams.gateway_main_session_key = params.mainSessionKey.trim();
}
const response = await gatewaysStatusApiV1GatewaysStatusGet(requestParams); const response = await gatewaysStatusApiV1GatewaysStatusGet(requestParams);
if (response.status !== 200) { if (response.status !== 200) {