refactor(gateway): update gateway parameters to use None as default #169
This commit is contained in:
@@ -37,8 +37,8 @@ def _query_to_resolve_input(
|
|||||||
board_id: str | None = Query(default=None),
|
board_id: str | None = Query(default=None),
|
||||||
gateway_url: str | None = Query(default=None),
|
gateway_url: str | None = Query(default=None),
|
||||||
gateway_token: str | None = Query(default=None),
|
gateway_token: str | None = Query(default=None),
|
||||||
gateway_disable_device_pairing: bool = Query(default=False),
|
gateway_disable_device_pairing: bool | None = Query(default=None),
|
||||||
gateway_allow_insecure_tls: bool = Query(default=False),
|
gateway_allow_insecure_tls: bool | None = Query(default=None),
|
||||||
) -> GatewayResolveQuery:
|
) -> GatewayResolveQuery:
|
||||||
return GatewaySessionService.to_resolve_query(
|
return GatewaySessionService.to_resolve_query(
|
||||||
board_id=board_id,
|
board_id=board_id,
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ 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_disable_device_pairing: bool = False
|
gateway_disable_device_pairing: bool | None = None
|
||||||
gateway_allow_insecure_tls: bool = False
|
gateway_allow_insecure_tls: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
class GatewaysStatusResponse(SQLModel):
|
class GatewaysStatusResponse(SQLModel):
|
||||||
|
|||||||
@@ -406,7 +406,9 @@ async def _openclaw_call_once(
|
|||||||
connect_kwargs: dict[str, Any] = {"ping_interval": None}
|
connect_kwargs: dict[str, Any] = {"ping_interval": None}
|
||||||
if origin is not None:
|
if origin is not None:
|
||||||
connect_kwargs["origin"] = origin
|
connect_kwargs["origin"] = origin
|
||||||
async with websockets.connect(gateway_url, ssl=ssl_context, **connect_kwargs) as ws:
|
if ssl_context is not None:
|
||||||
|
connect_kwargs["ssl"] = ssl_context
|
||||||
|
async with websockets.connect(gateway_url, **connect_kwargs) as ws:
|
||||||
first_message = await _recv_first_message_or_none(ws)
|
first_message = await _recv_first_message_or_none(ws)
|
||||||
await _ensure_connected(ws, first_message, config)
|
await _ensure_connected(ws, first_message, config)
|
||||||
return await _send_request(ws, method, params)
|
return await _send_request(ws, method, params)
|
||||||
@@ -422,7 +424,9 @@ async def _openclaw_connect_metadata_once(
|
|||||||
connect_kwargs: dict[str, Any] = {"ping_interval": None}
|
connect_kwargs: dict[str, Any] = {"ping_interval": None}
|
||||||
if origin is not None:
|
if origin is not None:
|
||||||
connect_kwargs["origin"] = origin
|
connect_kwargs["origin"] = origin
|
||||||
async with websockets.connect(gateway_url, ssl=ssl_context, **connect_kwargs) as ws:
|
if ssl_context is not None:
|
||||||
|
connect_kwargs["ssl"] = ssl_context
|
||||||
|
async with websockets.connect(gateway_url, **connect_kwargs) as ws:
|
||||||
first_message = await _recv_first_message_or_none(ws)
|
first_message = await _recv_first_message_or_none(ws)
|
||||||
return await _ensure_connected(ws, first_message, config)
|
return await _ensure_connected(ws, first_message, config)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from fastapi import HTTPException, status
|
|||||||
|
|
||||||
from app.core.logging import TRACE_LEVEL
|
from app.core.logging import TRACE_LEVEL
|
||||||
from app.models.boards import Board
|
from app.models.boards import Board
|
||||||
|
from app.models.gateways import Gateway
|
||||||
from app.schemas.gateway_api import (
|
from app.schemas.gateway_api import (
|
||||||
GatewayResolveQuery,
|
GatewayResolveQuery,
|
||||||
GatewaySessionHistoryResponse,
|
GatewaySessionHistoryResponse,
|
||||||
@@ -65,8 +66,8 @@ class GatewaySessionService(OpenClawDBService):
|
|||||||
board_id: str | None,
|
board_id: str | None,
|
||||||
gateway_url: str | None,
|
gateway_url: str | None,
|
||||||
gateway_token: str | None,
|
gateway_token: str | None,
|
||||||
gateway_disable_device_pairing: bool = False,
|
gateway_disable_device_pairing: bool | None = None,
|
||||||
gateway_allow_insecure_tls: bool = False,
|
gateway_allow_insecure_tls: bool | None = None,
|
||||||
) -> GatewayResolveQuery:
|
) -> GatewayResolveQuery:
|
||||||
return GatewayResolveQuery(
|
return GatewayResolveQuery(
|
||||||
board_id=board_id,
|
board_id=board_id,
|
||||||
@@ -95,6 +96,7 @@ class GatewaySessionService(OpenClawDBService):
|
|||||||
params: GatewayResolveQuery,
|
params: GatewayResolveQuery,
|
||||||
*,
|
*,
|
||||||
user: User | None = None,
|
user: User | None = None,
|
||||||
|
organization_id: UUID | None = None,
|
||||||
) -> tuple[Board | None, GatewayClientConfig, str | None]:
|
) -> tuple[Board | None, GatewayClientConfig, str | None]:
|
||||||
self.logger.log(
|
self.logger.log(
|
||||||
TRACE_LEVEL,
|
TRACE_LEVEL,
|
||||||
@@ -109,13 +111,34 @@ class GatewaySessionService(OpenClawDBService):
|
|||||||
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
detail="board_id or gateway_url is required",
|
detail="board_id or gateway_url is required",
|
||||||
)
|
)
|
||||||
|
token = (params.gateway_token or "").strip() or None
|
||||||
|
gateway: Gateway | None = None
|
||||||
|
can_query_saved_gateway = organization_id is not None and hasattr(self.session, "exec")
|
||||||
|
if can_query_saved_gateway and (
|
||||||
|
params.gateway_allow_insecure_tls is None
|
||||||
|
or params.gateway_disable_device_pairing is None
|
||||||
|
):
|
||||||
|
gateway_query = Gateway.objects.filter_by(url=raw_url)
|
||||||
|
if organization_id is not None:
|
||||||
|
gateway_query = gateway_query.filter_by(organization_id=organization_id)
|
||||||
|
gateway = await gateway_query.first(self.session)
|
||||||
|
allow_insecure_tls = (
|
||||||
|
params.gateway_allow_insecure_tls
|
||||||
|
if params.gateway_allow_insecure_tls is not None
|
||||||
|
else (gateway.allow_insecure_tls if gateway is not None else False)
|
||||||
|
)
|
||||||
|
disable_device_pairing = (
|
||||||
|
params.gateway_disable_device_pairing
|
||||||
|
if params.gateway_disable_device_pairing is not None
|
||||||
|
else (gateway.disable_device_pairing if gateway is not None else False)
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
None,
|
None,
|
||||||
GatewayClientConfig(
|
GatewayClientConfig(
|
||||||
url=raw_url,
|
url=raw_url,
|
||||||
token=(params.gateway_token or "").strip() or None,
|
token=token,
|
||||||
allow_insecure_tls=params.gateway_allow_insecure_tls,
|
allow_insecure_tls=allow_insecure_tls,
|
||||||
disable_device_pairing=params.gateway_disable_device_pairing,
|
disable_device_pairing=disable_device_pairing,
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -194,7 +217,11 @@ class GatewaySessionService(OpenClawDBService):
|
|||||||
organization_id: UUID,
|
organization_id: UUID,
|
||||||
user: User | None,
|
user: User | None,
|
||||||
) -> GatewaysStatusResponse:
|
) -> GatewaysStatusResponse:
|
||||||
board, config, main_session = await self.resolve_gateway(params, user=user)
|
board, config, main_session = await self.resolve_gateway(
|
||||||
|
params,
|
||||||
|
user=user,
|
||||||
|
organization_id=organization_id,
|
||||||
|
)
|
||||||
self._require_same_org(board, organization_id)
|
self._require_same_org(board, organization_id)
|
||||||
try:
|
try:
|
||||||
compatibility = await check_gateway_version_compatibility(config)
|
compatibility = await check_gateway_version_compatibility(config)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from uuid import uuid4
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import app.services.openclaw.session_service as session_service
|
||||||
from app.models.gateways import Gateway
|
from app.models.gateways import Gateway
|
||||||
from app.schemas.gateway_api import GatewayResolveQuery
|
from app.schemas.gateway_api import GatewayResolveQuery
|
||||||
from app.services.openclaw.gateway_resolver import (
|
from app.services.openclaw.gateway_resolver import (
|
||||||
@@ -100,3 +101,82 @@ async def test_resolve_gateway_keeps_gateway_allow_insecure_tls_for_direct_url()
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert config.allow_insecure_tls is True
|
assert config.allow_insecure_tls is True
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeGatewayQuery:
|
||||||
|
def __init__(self, gateway: Gateway | None) -> None:
|
||||||
|
self._gateway = gateway
|
||||||
|
self.filters: list[dict[str, object]] = []
|
||||||
|
|
||||||
|
def filter_by(self, **kwargs: object) -> _FakeGatewayQuery:
|
||||||
|
self.filters.append(kwargs)
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def first(self, _session: object) -> Gateway | None:
|
||||||
|
return self._gateway
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeAsyncSession:
|
||||||
|
async def exec(
|
||||||
|
self, *_args: object, **_kwargs: object
|
||||||
|
) -> None: # pragma: no cover - guard only
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_gateway_uses_saved_gateway_settings_when_direct_flags_missing(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
gateway = _gateway(
|
||||||
|
disable_device_pairing=True,
|
||||||
|
allow_insecure_tls=True,
|
||||||
|
url="wss://gateway.example:18789/ws",
|
||||||
|
token=" db-token ",
|
||||||
|
)
|
||||||
|
fake_query = _FakeGatewayQuery(gateway)
|
||||||
|
monkeypatch.setattr(session_service.Gateway, "objects", fake_query)
|
||||||
|
|
||||||
|
service = GatewaySessionService(session=_FakeAsyncSession()) # type: ignore[arg-type]
|
||||||
|
_, config, _ = await service.resolve_gateway(
|
||||||
|
GatewayResolveQuery(gateway_url=gateway.url),
|
||||||
|
user=None,
|
||||||
|
organization_id=gateway.organization_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert config.token is None
|
||||||
|
assert config.allow_insecure_tls is True
|
||||||
|
assert config.disable_device_pairing is True
|
||||||
|
assert fake_query.filters == [
|
||||||
|
{"url": gateway.url},
|
||||||
|
{"organization_id": gateway.organization_id},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_gateway_prefers_explicit_direct_flags_over_saved_settings(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
gateway = _gateway(
|
||||||
|
disable_device_pairing=True,
|
||||||
|
allow_insecure_tls=True,
|
||||||
|
url="wss://gateway.example:18789/ws",
|
||||||
|
token="db-token",
|
||||||
|
)
|
||||||
|
fake_query = _FakeGatewayQuery(gateway)
|
||||||
|
monkeypatch.setattr(session_service.Gateway, "objects", fake_query)
|
||||||
|
|
||||||
|
service = GatewaySessionService(session=object()) # type: ignore[arg-type]
|
||||||
|
_, config, _ = await service.resolve_gateway(
|
||||||
|
GatewayResolveQuery(
|
||||||
|
gateway_url=gateway.url,
|
||||||
|
gateway_token="explicit-token",
|
||||||
|
gateway_allow_insecure_tls=False,
|
||||||
|
gateway_disable_device_pairing=False,
|
||||||
|
),
|
||||||
|
user=None,
|
||||||
|
organization_id=gateway.organization_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert config.token == "explicit-token"
|
||||||
|
assert config.allow_insecure_tls is False
|
||||||
|
assert config.disable_device_pairing is False
|
||||||
|
|||||||
@@ -192,3 +192,93 @@ async def test_openclaw_call_surfaces_scope_error_without_device_fallback(
|
|||||||
"status",
|
"status",
|
||||||
config=GatewayConfig(url="ws://gateway.example/ws", token="secret-token"),
|
config=GatewayConfig(url="ws://gateway.example/ws", token="secret-token"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeConnectContext:
|
||||||
|
async def __aenter__(self) -> object:
|
||||||
|
return object()
|
||||||
|
|
||||||
|
async def __aexit__(self, _exc_type: object, _exc: object, _tb: object) -> bool:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_openclaw_call_once_does_not_pass_ssl_none_for_wss(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
def _fake_connect(url: str, **kwargs: object) -> _FakeConnectContext:
|
||||||
|
captured["url"] = url
|
||||||
|
captured["kwargs"] = kwargs
|
||||||
|
return _FakeConnectContext()
|
||||||
|
|
||||||
|
async def _fake_recv_first(_ws: object) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _fake_ensure_connected(
|
||||||
|
_ws: object, _first_message: object, _config: GatewayConfig
|
||||||
|
) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _fake_send_request(_ws: object, _method: str, _params: object) -> object:
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
monkeypatch.setattr(gateway_rpc.websockets, "connect", _fake_connect)
|
||||||
|
monkeypatch.setattr(gateway_rpc, "_recv_first_message_or_none", _fake_recv_first)
|
||||||
|
monkeypatch.setattr(gateway_rpc, "_ensure_connected", _fake_ensure_connected)
|
||||||
|
monkeypatch.setattr(gateway_rpc, "_send_request", _fake_send_request)
|
||||||
|
|
||||||
|
payload = await gateway_rpc._openclaw_call_once(
|
||||||
|
"status",
|
||||||
|
None,
|
||||||
|
config=GatewayConfig(url="wss://gateway.example/ws", allow_insecure_tls=False),
|
||||||
|
gateway_url="wss://gateway.example/ws",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert payload == {"ok": True}
|
||||||
|
assert captured["url"] == "wss://gateway.example/ws"
|
||||||
|
kwargs = captured["kwargs"]
|
||||||
|
assert isinstance(kwargs, dict)
|
||||||
|
assert "ssl" not in kwargs
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_openclaw_call_once_passes_ssl_context_for_insecure_wss(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
def _fake_connect(url: str, **kwargs: object) -> _FakeConnectContext:
|
||||||
|
captured["url"] = url
|
||||||
|
captured["kwargs"] = kwargs
|
||||||
|
return _FakeConnectContext()
|
||||||
|
|
||||||
|
async def _fake_recv_first(_ws: object) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _fake_ensure_connected(
|
||||||
|
_ws: object, _first_message: object, _config: GatewayConfig
|
||||||
|
) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _fake_send_request(_ws: object, _method: str, _params: object) -> object:
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
monkeypatch.setattr(gateway_rpc.websockets, "connect", _fake_connect)
|
||||||
|
monkeypatch.setattr(gateway_rpc, "_recv_first_message_or_none", _fake_recv_first)
|
||||||
|
monkeypatch.setattr(gateway_rpc, "_ensure_connected", _fake_ensure_connected)
|
||||||
|
monkeypatch.setattr(gateway_rpc, "_send_request", _fake_send_request)
|
||||||
|
|
||||||
|
payload = await gateway_rpc._openclaw_call_once(
|
||||||
|
"status",
|
||||||
|
None,
|
||||||
|
config=GatewayConfig(url="wss://gateway.example/ws", allow_insecure_tls=True),
|
||||||
|
gateway_url="wss://gateway.example/ws",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert payload == {"ok": True}
|
||||||
|
assert captured["url"] == "wss://gateway.example/ws"
|
||||||
|
kwargs = captured["kwargs"]
|
||||||
|
assert isinstance(kwargs, dict)
|
||||||
|
assert kwargs.get("ssl") is not None
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export default function GatewayDetailPage() {
|
|||||||
gateway_url: gateway.url,
|
gateway_url: gateway.url,
|
||||||
gateway_token: gateway.token ?? undefined,
|
gateway_token: gateway.token ?? undefined,
|
||||||
gateway_disable_device_pairing: gateway.disable_device_pairing,
|
gateway_disable_device_pairing: gateway.disable_device_pairing,
|
||||||
|
gateway_allow_insecure_tls: gateway.allow_insecure_tls,
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user