From 520e128777ba7bfd5f9e34c47a87761de70f8078 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 05:28:37 +0000 Subject: [PATCH] feat: Add allow_insecure_tls field to gateway model and UI - Added allow_insecure_tls boolean field to Gateway model and schemas - Created database migration for the new field - Updated GatewayConfig to include allow_insecure_tls parameter - Modified openclaw_call to create SSL context that disables verification when allow_insecure_tls is true - Updated all GatewayConfig instantiations throughout the backend - Added checkbox to frontend gateway form (create and edit pages) - Updated API endpoints to handle the new field Co-authored-by: abhi1693 <5083532+abhi1693@users.noreply.github.com> --- backend/app/api/gateways.py | 11 ++++-- backend/app/models/gateways.py | 1 + backend/app/schemas/gateways.py | 2 + .../app/services/openclaw/admin_service.py | 10 +++-- .../app/services/openclaw/gateway_resolver.py | 8 +++- backend/app/services/openclaw/gateway_rpc.py | 31 +++++++++++++++- backend/app/services/openclaw/provisioning.py | 8 +++- .../app/services/openclaw/provisioning_db.py | 6 ++- ...4a5b6c7d_add_gateway_allow_insecure_tls.py | 37 +++++++++++++++++++ .../app/gateways/[gatewayId]/edit/page.tsx | 8 ++++ frontend/src/app/gateways/new/page.tsx | 4 ++ .../src/components/gateways/GatewayForm.tsx | 22 +++++++++++ 12 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 backend/migrations/versions/2f3e4a5b6c7d_add_gateway_allow_insecure_tls.py diff --git a/backend/app/api/gateways.py b/backend/app/api/gateways.py index 3f756ce7..1ba59e44 100644 --- a/backend/app/api/gateways.py +++ b/backend/app/api/gateways.py @@ -94,7 +94,9 @@ async def create_gateway( ) -> Gateway: """Create a gateway and provision or refresh its main agent.""" service = GatewayAdminLifecycleService(session) - await service.assert_gateway_runtime_compatible(url=payload.url, token=payload.token) + await service.assert_gateway_runtime_compatible( + url=payload.url, token=payload.token, allow_insecure_tls=payload.allow_insecure_tls + ) data = payload.model_dump() gateway_id = uuid4() data["id"] = gateway_id @@ -134,12 +136,15 @@ async def update_gateway( organization_id=ctx.organization.id, ) updates = payload.model_dump(exclude_unset=True) - if "url" in updates or "token" in updates: + if "url" in updates or "token" in updates or "allow_insecure_tls" in updates: raw_next_url = updates.get("url", gateway.url) next_url = raw_next_url.strip() if isinstance(raw_next_url, str) else "" next_token = updates.get("token", gateway.token) + next_allow_insecure_tls = updates.get("allow_insecure_tls", gateway.allow_insecure_tls) if next_url: - await service.assert_gateway_runtime_compatible(url=next_url, token=next_token) + await service.assert_gateway_runtime_compatible( + url=next_url, token=next_token, allow_insecure_tls=next_allow_insecure_tls + ) await crud.patch(session, gateway, updates) await service.ensure_main_agent(gateway, auth, action="update") return gateway diff --git a/backend/app/models/gateways.py b/backend/app/models/gateways.py index 954f144f..19240567 100644 --- a/backend/app/models/gateways.py +++ b/backend/app/models/gateways.py @@ -24,5 +24,6 @@ class Gateway(QueryModel, table=True): url: str token: str | None = Field(default=None) workspace_root: str + allow_insecure_tls: bool = Field(default=False) created_at: datetime = Field(default_factory=utcnow) updated_at: datetime = Field(default_factory=utcnow) diff --git a/backend/app/schemas/gateways.py b/backend/app/schemas/gateways.py index 233a44d5..241d370b 100644 --- a/backend/app/schemas/gateways.py +++ b/backend/app/schemas/gateways.py @@ -17,6 +17,7 @@ class GatewayBase(SQLModel): name: str url: str workspace_root: str + allow_insecure_tls: bool = False class GatewayCreate(GatewayBase): @@ -43,6 +44,7 @@ class GatewayUpdate(SQLModel): url: str | None = None token: str | None = None workspace_root: str | None = None + allow_insecure_tls: bool | None = None @field_validator("token", mode="before") @classmethod diff --git a/backend/app/services/openclaw/admin_service.py b/backend/app/services/openclaw/admin_service.py index dfc0c2b6..e248f888 100644 --- a/backend/app/services/openclaw/admin_service.py +++ b/backend/app/services/openclaw/admin_service.py @@ -167,7 +167,9 @@ class GatewayAdminLifecycleService(OpenClawDBService): async def gateway_has_main_agent_entry(self, gateway: Gateway) -> bool: if not gateway.url: return False - config = GatewayClientConfig(url=gateway.url, token=gateway.token) + config = GatewayClientConfig( + url=gateway.url, token=gateway.token, allow_insecure_tls=gateway.allow_insecure_tls + ) target_id = GatewayAgentIdentity.openclaw_agent_id(gateway) try: await openclaw_call("agents.files.list", {"agentId": target_id}, config=config) @@ -178,9 +180,11 @@ class GatewayAdminLifecycleService(OpenClawDBService): return True return True - async def assert_gateway_runtime_compatible(self, *, url: str, token: str | None) -> None: + async def assert_gateway_runtime_compatible( + self, *, url: str, token: str | None, allow_insecure_tls: bool = False + ) -> None: """Validate that a gateway runtime meets minimum supported version.""" - config = GatewayClientConfig(url=url, token=token) + config = GatewayClientConfig(url=url, token=token, allow_insecure_tls=allow_insecure_tls) try: result = await check_gateway_runtime_compatibility(config) except OpenClawGatewayError as exc: diff --git a/backend/app/services/openclaw/gateway_resolver.py b/backend/app/services/openclaw/gateway_resolver.py index 7e31814f..6a91b63d 100644 --- a/backend/app/services/openclaw/gateway_resolver.py +++ b/backend/app/services/openclaw/gateway_resolver.py @@ -32,7 +32,9 @@ def gateway_client_config(gateway: Gateway) -> GatewayClientConfig: detail="Gateway url is required", ) token = (gateway.token or "").strip() or None - return GatewayClientConfig(url=url, token=token) + return GatewayClientConfig( + url=url, token=token, allow_insecure_tls=gateway.allow_insecure_tls + ) def optional_gateway_client_config(gateway: Gateway | None) -> GatewayClientConfig | None: @@ -43,7 +45,9 @@ def optional_gateway_client_config(gateway: Gateway | None) -> GatewayClientConf if not url: return None token = (gateway.token or "").strip() or None - return GatewayClientConfig(url=url, token=token) + return GatewayClientConfig( + url=url, token=token, allow_insecure_tls=gateway.allow_insecure_tls + ) def require_gateway_workspace_root(gateway: Gateway) -> str: diff --git a/backend/app/services/openclaw/gateway_rpc.py b/backend/app/services/openclaw/gateway_rpc.py index a4fdefa1..62a2844d 100644 --- a/backend/app/services/openclaw/gateway_rpc.py +++ b/backend/app/services/openclaw/gateway_rpc.py @@ -9,6 +9,7 @@ from __future__ import annotations import asyncio import json +import ssl from dataclasses import dataclass from time import perf_counter from typing import Any @@ -160,6 +161,7 @@ class GatewayConfig: url: str token: str | None = None + allow_insecure_tls: bool = False def _build_gateway_url(config: GatewayConfig) -> str: @@ -180,6 +182,27 @@ def _redacted_url_for_log(raw_url: str) -> str: return str(urlunparse(parsed._replace(query="", fragment=""))) +def _create_ssl_context(config: GatewayConfig) -> ssl.SSLContext | None: + """Create SSL context for websocket connection. + + Returns None for non-SSL connections (ws://) or an SSL context for wss://. + If allow_insecure_tls is True, the context will not verify certificates. + """ + parsed = urlparse(config.url) + if parsed.scheme != "wss": + return None + + if config.allow_insecure_tls: + # Create SSL context that doesn't verify certificates + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + return ssl_context + + # Use default SSL context with certificate verification + return None + + async def _await_response( ws: websockets.ClientConnection, request_id: str, @@ -283,14 +306,18 @@ async def openclaw_call( ) -> object: """Call a gateway RPC method and return the result payload.""" gateway_url = _build_gateway_url(config) + ssl_context = _create_ssl_context(config) started_at = perf_counter() logger.debug( - "gateway.rpc.call.start method=%s gateway_url=%s", + "gateway.rpc.call.start method=%s gateway_url=%s allow_insecure_tls=%s", method, _redacted_url_for_log(gateway_url), + config.allow_insecure_tls, ) try: - async with websockets.connect(gateway_url, ping_interval=None) as ws: + async with websockets.connect( + gateway_url, ping_interval=None, ssl=ssl_context + ) as ws: first_message = None try: first_message = await asyncio.wait_for(ws.recv(), timeout=2) diff --git a/backend/app/services/openclaw/provisioning.py b/backend/app/services/openclaw/provisioning.py index 0d2534a3..6544214f 100644 --- a/backend/app/services/openclaw/provisioning.py +++ b/backend/app/services/openclaw/provisioning.py @@ -970,7 +970,9 @@ def _control_plane_for_gateway(gateway: Gateway) -> OpenClawGatewayControlPlane: msg = "Gateway url is required" raise OpenClawGatewayError(msg) return OpenClawGatewayControlPlane( - GatewayClientConfig(url=gateway.url, token=gateway.token), + GatewayClientConfig( + url=gateway.url, token=gateway.token, allow_insecure_tls=gateway.allow_insecure_tls + ), ) @@ -1099,7 +1101,9 @@ class OpenClawGatewayProvisioner: if not wake: return - client_config = GatewayClientConfig(url=gateway.url, token=gateway.token) + client_config = GatewayClientConfig( + url=gateway.url, token=gateway.token, allow_insecure_tls=gateway.allow_insecure_tls + ) await ensure_session(session_key, config=client_config, label=agent.name) verb = wakeup_verb or ("provisioned" if action == "provision" else "updated") await send_message( diff --git a/backend/app/services/openclaw/provisioning_db.py b/backend/app/services/openclaw/provisioning_db.py index 97fcb8f6..139fd8c0 100644 --- a/backend/app/services/openclaw/provisioning_db.py +++ b/backend/app/services/openclaw/provisioning_db.py @@ -285,7 +285,11 @@ class OpenClawProvisioningService(OpenClawDBService): return result control_plane = OpenClawGatewayControlPlane( - GatewayClientConfig(url=gateway.url, token=gateway.token), + GatewayClientConfig( + url=gateway.url, + token=gateway.token, + allow_insecure_tls=gateway.allow_insecure_tls, + ), ) ctx = _SyncContext( session=self.session, diff --git a/backend/migrations/versions/2f3e4a5b6c7d_add_gateway_allow_insecure_tls.py b/backend/migrations/versions/2f3e4a5b6c7d_add_gateway_allow_insecure_tls.py new file mode 100644 index 00000000..48174e8c --- /dev/null +++ b/backend/migrations/versions/2f3e4a5b6c7d_add_gateway_allow_insecure_tls.py @@ -0,0 +1,37 @@ +"""Add allow_insecure_tls field to gateways. + +Revision ID: 2f3e4a5b6c7d +Revises: 1a7b2c3d4e5f +Create Date: 2026-02-22 05:30:00.000000 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "2f3e4a5b6c7d" +down_revision = "1a7b2c3d4e5f" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Add gateways.allow_insecure_tls column with default False.""" + op.add_column( + "gateways", + sa.Column( + "allow_insecure_tls", + sa.Boolean(), + nullable=False, + server_default=sa.text("false"), + ), + ) + op.alter_column("gateways", "allow_insecure_tls", server_default=None) + + +def downgrade() -> None: + """Remove gateways.allow_insecure_tls column.""" + op.drop_column("gateways", "allow_insecure_tls") diff --git a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx index 6b4e2120..c699c675 100644 --- a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx +++ b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx @@ -43,6 +43,9 @@ export default function EditGatewayPage() { const [workspaceRoot, setWorkspaceRoot] = useState( undefined, ); + const [allowInsecureTls, setAllowInsecureTls] = useState( + undefined, + ); const [gatewayUrlError, setGatewayUrlError] = useState(null); const [gatewayCheckStatus, setGatewayCheckStatus] = @@ -84,6 +87,8 @@ export default function EditGatewayPage() { const resolvedGatewayToken = gatewayToken ?? loadedGateway?.token ?? ""; const resolvedWorkspaceRoot = workspaceRoot ?? loadedGateway?.workspace_root ?? DEFAULT_WORKSPACE_ROOT; + const resolvedAllowInsecureTls = + allowInsecureTls ?? loadedGateway?.allow_insecure_tls ?? false; const isLoading = gatewayQuery.isLoading || updateMutation.isPending; const errorMessage = error ?? gatewayQuery.error?.message ?? null; @@ -140,6 +145,7 @@ export default function EditGatewayPage() { url: resolvedGatewayUrl.trim(), token: resolvedGatewayToken.trim() || null, workspace_root: resolvedWorkspaceRoot.trim(), + allow_insecure_tls: resolvedAllowInsecureTls, }; updateMutation.mutate({ gatewayId, data: payload }); @@ -165,6 +171,7 @@ export default function EditGatewayPage() { gatewayUrl={resolvedGatewayUrl} gatewayToken={resolvedGatewayToken} workspaceRoot={resolvedWorkspaceRoot} + allowInsecureTls={resolvedAllowInsecureTls} gatewayUrlError={gatewayUrlError} gatewayCheckStatus={gatewayCheckStatus} gatewayCheckMessage={gatewayCheckMessage} @@ -191,6 +198,7 @@ export default function EditGatewayPage() { setGatewayCheckMessage(null); }} onWorkspaceRootChange={setWorkspaceRoot} + onAllowInsecureTlsChange={setAllowInsecureTls} /> ); diff --git a/frontend/src/app/gateways/new/page.tsx b/frontend/src/app/gateways/new/page.tsx index f72db43e..3d6bc1e3 100644 --- a/frontend/src/app/gateways/new/page.tsx +++ b/frontend/src/app/gateways/new/page.tsx @@ -29,6 +29,7 @@ export default function NewGatewayPage() { const [gatewayUrl, setGatewayUrl] = useState(""); const [gatewayToken, setGatewayToken] = useState(""); const [workspaceRoot, setWorkspaceRoot] = useState(DEFAULT_WORKSPACE_ROOT); + const [allowInsecureTls, setAllowInsecureTls] = useState(false); const [gatewayUrlError, setGatewayUrlError] = useState(null); const [gatewayCheckStatus, setGatewayCheckStatus] = @@ -106,6 +107,7 @@ export default function NewGatewayPage() { url: gatewayUrl.trim(), token: gatewayToken.trim() || null, workspace_root: workspaceRoot.trim(), + allow_insecure_tls: allowInsecureTls, }, }); }; @@ -126,6 +128,7 @@ export default function NewGatewayPage() { gatewayUrl={gatewayUrl} gatewayToken={gatewayToken} workspaceRoot={workspaceRoot} + allowInsecureTls={allowInsecureTls} gatewayUrlError={gatewayUrlError} gatewayCheckStatus={gatewayCheckStatus} gatewayCheckMessage={gatewayCheckMessage} @@ -152,6 +155,7 @@ export default function NewGatewayPage() { setGatewayCheckMessage(null); }} onWorkspaceRootChange={setWorkspaceRoot} + onAllowInsecureTlsChange={setAllowInsecureTls} /> ); diff --git a/frontend/src/components/gateways/GatewayForm.tsx b/frontend/src/components/gateways/GatewayForm.tsx index f5068faf..81854918 100644 --- a/frontend/src/components/gateways/GatewayForm.tsx +++ b/frontend/src/components/gateways/GatewayForm.tsx @@ -10,6 +10,7 @@ type GatewayFormProps = { gatewayUrl: string; gatewayToken: string; workspaceRoot: string; + allowInsecureTls: boolean; gatewayUrlError: string | null; gatewayCheckStatus: GatewayCheckStatus; gatewayCheckMessage: string | null; @@ -27,6 +28,7 @@ type GatewayFormProps = { onGatewayUrlChange: (next: string) => void; onGatewayTokenChange: (next: string) => void; onWorkspaceRootChange: (next: string) => void; + onAllowInsecureTlsChange: (next: boolean) => void; }; export function GatewayForm({ @@ -34,6 +36,7 @@ export function GatewayForm({ gatewayUrl, gatewayToken, workspaceRoot, + allowInsecureTls, gatewayUrlError, gatewayCheckStatus, gatewayCheckMessage, @@ -51,6 +54,7 @@ export function GatewayForm({ onGatewayUrlChange, onGatewayTokenChange, onWorkspaceRootChange, + onAllowInsecureTlsChange, }: GatewayFormProps) { return (
+
+ onAllowInsecureTlsChange(event.target.checked)} + disabled={isLoading} + /> + +
+ {errorMessage ? (

{errorMessage}

) : null}