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()
{