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>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-22 05:28:37 +00:00
parent 6455a27176
commit 520e128777
12 changed files with 135 additions and 13 deletions

View File

@@ -94,7 +94,9 @@ 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."""
service = GatewayAdminLifecycleService(session) 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() data = payload.model_dump()
gateway_id = uuid4() gateway_id = uuid4()
data["id"] = gateway_id data["id"] = gateway_id
@@ -134,12 +136,15 @@ async def update_gateway(
organization_id=ctx.organization.id, organization_id=ctx.organization.id,
) )
updates = payload.model_dump(exclude_unset=True) 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) raw_next_url = updates.get("url", gateway.url)
next_url = raw_next_url.strip() if isinstance(raw_next_url, str) else "" next_url = raw_next_url.strip() if isinstance(raw_next_url, str) else ""
next_token = updates.get("token", gateway.token) next_token = updates.get("token", gateway.token)
next_allow_insecure_tls = updates.get("allow_insecure_tls", gateway.allow_insecure_tls)
if next_url: 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 crud.patch(session, gateway, updates)
await service.ensure_main_agent(gateway, auth, action="update") await service.ensure_main_agent(gateway, auth, action="update")
return gateway return gateway

View File

@@ -24,5 +24,6 @@ class Gateway(QueryModel, table=True):
url: str url: str
token: str | None = Field(default=None) token: str | None = Field(default=None)
workspace_root: str workspace_root: str
allow_insecure_tls: bool = Field(default=False)
created_at: datetime = Field(default_factory=utcnow) created_at: datetime = Field(default_factory=utcnow)
updated_at: datetime = Field(default_factory=utcnow) updated_at: datetime = Field(default_factory=utcnow)

View File

@@ -17,6 +17,7 @@ class GatewayBase(SQLModel):
name: str name: str
url: str url: str
workspace_root: str workspace_root: str
allow_insecure_tls: bool = False
class GatewayCreate(GatewayBase): class GatewayCreate(GatewayBase):
@@ -43,6 +44,7 @@ class GatewayUpdate(SQLModel):
url: str | None = None url: str | None = None
token: str | None = None token: str | None = None
workspace_root: str | None = None workspace_root: str | None = None
allow_insecure_tls: bool | None = None
@field_validator("token", mode="before") @field_validator("token", mode="before")
@classmethod @classmethod

View File

@@ -167,7 +167,9 @@ class GatewayAdminLifecycleService(OpenClawDBService):
async def gateway_has_main_agent_entry(self, gateway: Gateway) -> bool: async def gateway_has_main_agent_entry(self, gateway: Gateway) -> bool:
if not gateway.url: if not gateway.url:
return False 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) target_id = GatewayAgentIdentity.openclaw_agent_id(gateway)
try: try:
await openclaw_call("agents.files.list", {"agentId": target_id}, config=config) await openclaw_call("agents.files.list", {"agentId": target_id}, config=config)
@@ -178,9 +180,11 @@ class GatewayAdminLifecycleService(OpenClawDBService):
return True return True
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.""" """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: try:
result = await check_gateway_runtime_compatibility(config) result = await check_gateway_runtime_compatibility(config)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:

View File

@@ -32,7 +32,9 @@ def gateway_client_config(gateway: Gateway) -> GatewayClientConfig:
detail="Gateway url is required", detail="Gateway url is required",
) )
token = (gateway.token or "").strip() or 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 optional_gateway_client_config(gateway: Gateway | None) -> GatewayClientConfig | None: 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: if not url:
return None return None
token = (gateway.token or "").strip() or 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: def require_gateway_workspace_root(gateway: Gateway) -> str:

View File

@@ -9,6 +9,7 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import ssl
from dataclasses import dataclass from dataclasses import dataclass
from time import perf_counter from time import perf_counter
from typing import Any from typing import Any
@@ -160,6 +161,7 @@ class GatewayConfig:
url: str url: str
token: str | None = None token: str | None = None
allow_insecure_tls: bool = False
def _build_gateway_url(config: GatewayConfig) -> str: 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=""))) 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( async def _await_response(
ws: websockets.ClientConnection, ws: websockets.ClientConnection,
request_id: str, request_id: str,
@@ -283,14 +306,18 @@ async def openclaw_call(
) -> object: ) -> object:
"""Call a gateway RPC method and return the result payload.""" """Call a gateway RPC method and return the result payload."""
gateway_url = _build_gateway_url(config) gateway_url = _build_gateway_url(config)
ssl_context = _create_ssl_context(config)
started_at = perf_counter() started_at = perf_counter()
logger.debug( 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, method,
_redacted_url_for_log(gateway_url), _redacted_url_for_log(gateway_url),
config.allow_insecure_tls,
) )
try: 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 first_message = None
try: try:
first_message = await asyncio.wait_for(ws.recv(), timeout=2) first_message = await asyncio.wait_for(ws.recv(), timeout=2)

View File

@@ -970,7 +970,9 @@ def _control_plane_for_gateway(gateway: Gateway) -> OpenClawGatewayControlPlane:
msg = "Gateway url is required" msg = "Gateway url is required"
raise OpenClawGatewayError(msg) raise OpenClawGatewayError(msg)
return OpenClawGatewayControlPlane( 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: if not wake:
return 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) await ensure_session(session_key, config=client_config, label=agent.name)
verb = wakeup_verb or ("provisioned" if action == "provision" else "updated") verb = wakeup_verb or ("provisioned" if action == "provision" else "updated")
await send_message( await send_message(

View File

@@ -285,7 +285,11 @@ class OpenClawProvisioningService(OpenClawDBService):
return result return result
control_plane = OpenClawGatewayControlPlane( 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( ctx = _SyncContext(
session=self.session, session=self.session,

View File

@@ -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")

View File

@@ -43,6 +43,9 @@ export default function EditGatewayPage() {
const [workspaceRoot, setWorkspaceRoot] = useState<string | undefined>( const [workspaceRoot, setWorkspaceRoot] = useState<string | undefined>(
undefined, undefined,
); );
const [allowInsecureTls, setAllowInsecureTls] = useState<boolean | undefined>(
undefined,
);
const [gatewayUrlError, setGatewayUrlError] = useState<string | null>(null); const [gatewayUrlError, setGatewayUrlError] = useState<string | null>(null);
const [gatewayCheckStatus, setGatewayCheckStatus] = const [gatewayCheckStatus, setGatewayCheckStatus] =
@@ -84,6 +87,8 @@ export default function EditGatewayPage() {
const resolvedGatewayToken = gatewayToken ?? loadedGateway?.token ?? ""; const resolvedGatewayToken = gatewayToken ?? loadedGateway?.token ?? "";
const resolvedWorkspaceRoot = const resolvedWorkspaceRoot =
workspaceRoot ?? loadedGateway?.workspace_root ?? DEFAULT_WORKSPACE_ROOT; workspaceRoot ?? loadedGateway?.workspace_root ?? DEFAULT_WORKSPACE_ROOT;
const resolvedAllowInsecureTls =
allowInsecureTls ?? loadedGateway?.allow_insecure_tls ?? false;
const isLoading = gatewayQuery.isLoading || updateMutation.isPending; const isLoading = gatewayQuery.isLoading || updateMutation.isPending;
const errorMessage = error ?? gatewayQuery.error?.message ?? null; const errorMessage = error ?? gatewayQuery.error?.message ?? null;
@@ -140,6 +145,7 @@ export default function EditGatewayPage() {
url: resolvedGatewayUrl.trim(), url: resolvedGatewayUrl.trim(),
token: resolvedGatewayToken.trim() || null, token: resolvedGatewayToken.trim() || null,
workspace_root: resolvedWorkspaceRoot.trim(), workspace_root: resolvedWorkspaceRoot.trim(),
allow_insecure_tls: resolvedAllowInsecureTls,
}; };
updateMutation.mutate({ gatewayId, data: payload }); updateMutation.mutate({ gatewayId, data: payload });
@@ -165,6 +171,7 @@ export default function EditGatewayPage() {
gatewayUrl={resolvedGatewayUrl} gatewayUrl={resolvedGatewayUrl}
gatewayToken={resolvedGatewayToken} gatewayToken={resolvedGatewayToken}
workspaceRoot={resolvedWorkspaceRoot} workspaceRoot={resolvedWorkspaceRoot}
allowInsecureTls={resolvedAllowInsecureTls}
gatewayUrlError={gatewayUrlError} gatewayUrlError={gatewayUrlError}
gatewayCheckStatus={gatewayCheckStatus} gatewayCheckStatus={gatewayCheckStatus}
gatewayCheckMessage={gatewayCheckMessage} gatewayCheckMessage={gatewayCheckMessage}
@@ -191,6 +198,7 @@ export default function EditGatewayPage() {
setGatewayCheckMessage(null); setGatewayCheckMessage(null);
}} }}
onWorkspaceRootChange={setWorkspaceRoot} onWorkspaceRootChange={setWorkspaceRoot}
onAllowInsecureTlsChange={setAllowInsecureTls}
/> />
</DashboardPageLayout> </DashboardPageLayout>
); );

View File

@@ -29,6 +29,7 @@ export default function NewGatewayPage() {
const [gatewayUrl, setGatewayUrl] = useState(""); const [gatewayUrl, setGatewayUrl] = useState("");
const [gatewayToken, setGatewayToken] = useState(""); const [gatewayToken, setGatewayToken] = useState("");
const [workspaceRoot, setWorkspaceRoot] = useState(DEFAULT_WORKSPACE_ROOT); const [workspaceRoot, setWorkspaceRoot] = useState(DEFAULT_WORKSPACE_ROOT);
const [allowInsecureTls, setAllowInsecureTls] = useState(false);
const [gatewayUrlError, setGatewayUrlError] = useState<string | null>(null); const [gatewayUrlError, setGatewayUrlError] = useState<string | null>(null);
const [gatewayCheckStatus, setGatewayCheckStatus] = const [gatewayCheckStatus, setGatewayCheckStatus] =
@@ -106,6 +107,7 @@ export default function NewGatewayPage() {
url: gatewayUrl.trim(), url: gatewayUrl.trim(),
token: gatewayToken.trim() || null, token: gatewayToken.trim() || null,
workspace_root: workspaceRoot.trim(), workspace_root: workspaceRoot.trim(),
allow_insecure_tls: allowInsecureTls,
}, },
}); });
}; };
@@ -126,6 +128,7 @@ export default function NewGatewayPage() {
gatewayUrl={gatewayUrl} gatewayUrl={gatewayUrl}
gatewayToken={gatewayToken} gatewayToken={gatewayToken}
workspaceRoot={workspaceRoot} workspaceRoot={workspaceRoot}
allowInsecureTls={allowInsecureTls}
gatewayUrlError={gatewayUrlError} gatewayUrlError={gatewayUrlError}
gatewayCheckStatus={gatewayCheckStatus} gatewayCheckStatus={gatewayCheckStatus}
gatewayCheckMessage={gatewayCheckMessage} gatewayCheckMessage={gatewayCheckMessage}
@@ -152,6 +155,7 @@ export default function NewGatewayPage() {
setGatewayCheckMessage(null); setGatewayCheckMessage(null);
}} }}
onWorkspaceRootChange={setWorkspaceRoot} onWorkspaceRootChange={setWorkspaceRoot}
onAllowInsecureTlsChange={setAllowInsecureTls}
/> />
</DashboardPageLayout> </DashboardPageLayout>
); );

View File

@@ -10,6 +10,7 @@ type GatewayFormProps = {
gatewayUrl: string; gatewayUrl: string;
gatewayToken: string; gatewayToken: string;
workspaceRoot: string; workspaceRoot: string;
allowInsecureTls: boolean;
gatewayUrlError: string | null; gatewayUrlError: string | null;
gatewayCheckStatus: GatewayCheckStatus; gatewayCheckStatus: GatewayCheckStatus;
gatewayCheckMessage: string | null; gatewayCheckMessage: string | null;
@@ -27,6 +28,7 @@ type GatewayFormProps = {
onGatewayUrlChange: (next: string) => void; onGatewayUrlChange: (next: string) => void;
onGatewayTokenChange: (next: string) => void; onGatewayTokenChange: (next: string) => void;
onWorkspaceRootChange: (next: string) => void; onWorkspaceRootChange: (next: string) => void;
onAllowInsecureTlsChange: (next: boolean) => void;
}; };
export function GatewayForm({ export function GatewayForm({
@@ -34,6 +36,7 @@ export function GatewayForm({
gatewayUrl, gatewayUrl,
gatewayToken, gatewayToken,
workspaceRoot, workspaceRoot,
allowInsecureTls,
gatewayUrlError, gatewayUrlError,
gatewayCheckStatus, gatewayCheckStatus,
gatewayCheckMessage, gatewayCheckMessage,
@@ -51,6 +54,7 @@ export function GatewayForm({
onGatewayUrlChange, onGatewayUrlChange,
onGatewayTokenChange, onGatewayTokenChange,
onWorkspaceRootChange, onWorkspaceRootChange,
onAllowInsecureTlsChange,
}: GatewayFormProps) { }: GatewayFormProps) {
return ( return (
<form <form
@@ -140,6 +144,24 @@ export function GatewayForm({
/> />
</div> </div>
<div className="flex items-center gap-2">
<input
type="checkbox"
id="allow-insecure-tls"
className="h-4 w-4 rounded border-slate-300 text-blue-600"
checked={allowInsecureTls}
onChange={(event) => onAllowInsecureTlsChange(event.target.checked)}
disabled={isLoading}
/>
<label
htmlFor="allow-insecure-tls"
className="text-sm text-slate-700 cursor-pointer"
>
Allow self-signed TLS certificates (for localhost or trusted local
networks)
</label>
</div>
{errorMessage ? ( {errorMessage ? (
<p className="text-sm text-red-500">{errorMessage}</p> <p className="text-sm text-red-500">{errorMessage}</p>
) : null} ) : null}