From b52fe11c53d200b3038e4e9f92d863bdbbf52152 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:03:08 +0000 Subject: [PATCH] fix(connection): operator client always connects as operator, never node role MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GatewayClientFactory created the operator (chat) client with bootstrapPairAsNode: credential.IsBootstrapToken. For bootstrap tokens (QR-code and setup-code pairing) this set bootstrapPairAsNode: true, which caused GetConnectRole() to return "node" — permanently locking the tray app in node role with no device-token upgrade path. Every reconnect re-used the bootstrap token, GetConnectRole() returned "node" again, and chat.send was rejected with "unauthorized role: node". Fix: set bootstrapPairAsNode: false unconditionally for the operator client. The operator (chat) client should always connect as "operator" regardless of credential type. The node-platform client has its own creation path (NodeConnector) which sets bootstrapPairAsNode as needed. Updated GatewayClientFactoryTests to reflect the corrected behaviour and added a regression test in OpenClawGatewayClientTests covering the bootstrap-token operator-role case. Closes #720 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../GatewayClientFactory.cs | 6 ++++- .../GatewayClientFactoryTests.cs | 10 ++++++-- .../OpenClawGatewayClientTests.cs | 25 +++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/OpenClaw.Connection/GatewayClientFactory.cs b/src/OpenClaw.Connection/GatewayClientFactory.cs index 5ea0c59d..9802579a 100644 --- a/src/OpenClaw.Connection/GatewayClientFactory.cs +++ b/src/OpenClaw.Connection/GatewayClientFactory.cs @@ -14,12 +14,16 @@ public IGatewayClientLifecycle Create( string identityPath, IOpenClawLogger logger) { + // bootstrapPairAsNode is always false for the operator (chat) client: the operator + // client must connect as "operator" role regardless of credential type so that chat + // works immediately after QR-code or setup-code pairing. The node-platform client + // has its own creation path (NodeConnector) which sets bootstrapPairAsNode as needed. var client = new OpenClawGatewayClient( gatewayUrl, credential.Token, logger, tokenIsBootstrapToken: credential.IsBootstrapToken, - bootstrapPairAsNode: credential.IsBootstrapToken, + bootstrapPairAsNode: false, identityPath: identityPath); return new GatewayClientLifecycleAdapter(client); diff --git a/tests/OpenClaw.Connection.Tests/GatewayClientFactoryTests.cs b/tests/OpenClaw.Connection.Tests/GatewayClientFactoryTests.cs index 73b6a2b8..318d31e4 100644 --- a/tests/OpenClaw.Connection.Tests/GatewayClientFactoryTests.cs +++ b/tests/OpenClaw.Connection.Tests/GatewayClientFactoryTests.cs @@ -6,8 +6,13 @@ namespace OpenClaw.Connection.Tests; public sealed class GatewayClientFactoryTests { + /// + /// Regression test for issue #720: the operator (chat) client must connect as "operator" + /// role even when the credential is a bootstrap token. Setting bootstrapPairAsNode: false + /// for the operator client ensures chat works immediately after QR-code or setup-code pairing. + /// [Fact] - public void Create_BootstrapCredential_PairsAsNode() + public void Create_BootstrapCredential_PairsAsOperator() { var tempDir = Path.Combine(Path.GetTempPath(), "openclaw-gateway-factory-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(tempDir); @@ -20,7 +25,8 @@ public void Create_BootstrapCredential_PairsAsNode() tempDir, NullLogger.Instance); - Assert.Equal("node", GetConnectRole(lifecycle.DataClient)); + // Operator client always connects as "operator", even with a bootstrap token. + Assert.Equal("operator", GetConnectRole(lifecycle.DataClient)); } finally { diff --git a/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs b/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs index ea9d62bc..8dba7368 100644 --- a/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs +++ b/tests/OpenClaw.Shared.Tests/OpenClawGatewayClientTests.cs @@ -598,6 +598,31 @@ public void BootstrapNodeHandoff_FreshDevice_RequestsNodeRoleWithoutScopes() Assert.False(auth.ContainsKey("deviceToken")); } + /// + /// Regression test for issue #720: the operator (chat) client must connect as "operator" + /// role even when the credential is a bootstrap token (QR-code or setup-code pairing). + /// GatewayClientFactory always passes bootstrapPairAsNode: false for the operator client. + /// + [Fact] + public void OperatorClient_WithBootstrapToken_ConnectsAsOperatorRole() + { + // bootstrapPairAsNode: false — this is what GatewayClientFactory now passes for the operator client. + var helper = new GatewayClientTestHelper( + tokenIsBootstrapToken: true, + bootstrapPairAsNode: false, + identityPath: CreateTempIdentityPath()); + helper.SetDeviceTokenForTest(null); + + var auth = helper.BuildAuthPayload(); + + // Must request "operator" role so chat works immediately after QR-code pairing. + Assert.Equal("operator", helper.GetConnectRole()); + // Must send bootstrapToken (not plain token) so the gateway performs bootstrap pairing. + Assert.Equal("test-token", auth["bootstrapToken"]); + Assert.False(auth.ContainsKey("token")); + Assert.False(auth.ContainsKey("deviceToken")); + } + [Fact] public void BootstrapNodeHandoff_HelloOkWithNodeRole_DoesNotStorePrimaryNodeTokenAsOperator() {