fix(onboard): preserve remote transport settings

This commit is contained in:
Amp
2026-08-21 08:21:58 +00:00
parent 3d119af588
commit d318b2b019
3 changed files with 114 additions and 8 deletions
+3 -2
View File
@@ -1,4 +1,4 @@
import { gatewayOriginScope } from "../../packages/gateway-client/src/gateway-origin-scope.js";
import { gatewayCredentialScope } from "../../packages/gateway-client/src/gateway-origin-scope.js";
/** Returns whether an explicit remote Gateway URL selects a different endpoint. */
export function remoteGatewayUrlChanged(
@@ -6,6 +6,7 @@ export function remoteGatewayUrlChanged(
previousUrl: string | undefined,
): boolean {
return (
nextUrl !== undefined && gatewayOriginScope(nextUrl) !== gatewayOriginScope(previousUrl ?? "")
nextUrl !== undefined &&
gatewayCredentialScope(nextUrl) !== gatewayCredentialScope(previousUrl ?? "")
);
}
+90 -2
View File
@@ -116,6 +116,84 @@ describe("promptRemoteGatewayConfig", () => {
expect(next.gateway?.remote?.edgeAuth).toEqual(expected);
});
it("preserves credentials and target settings for an equivalently spelled URL", async () => {
const cfg: OpenClawConfig = {
gateway: {
mode: "remote",
remote: {
url: "wss://gateway.example/rpc/",
token: "stored-token",
transport: "ssh",
remotePort: 18790,
tlsFingerprint: "sha256:stored",
sshTarget: "user@gateway.example",
sshIdentity: "~/.ssh/id_gateway",
sshHostKeyPolicy: "strict",
},
},
};
const prompter = createPrompter({
confirm: vi.fn(async (params) => params.message.startsWith("Use existing gateway token")),
select: createSelectPrompter({
"Gateway auth": "token",
"How do you want to provide this gateway token?": "plaintext",
}),
text: vi.fn(async (params) =>
params.message === "Gateway WebSocket URL" ? "wss://gateway.example/rpc" : "",
) as WizardPrompter["text"],
});
const next = await promptRemoteGatewayConfig(cfg, prompter);
expect(next.gateway?.remote).toEqual({
...cfg.gateway?.remote,
url: "wss://gateway.example/rpc",
});
});
it("clears credentials and target settings when the URL query changes", async () => {
const cfg: OpenClawConfig = {
gateway: {
mode: "remote",
remote: {
url: "wss://gateway.example/rpc?account=personal",
token: "stored-token",
transport: "ssh",
remotePort: 18790,
tlsFingerprint: "sha256:stored",
sshTarget: "user@gateway.example",
sshIdentity: "~/.ssh/id_gateway",
sshHostKeyPolicy: "strict",
},
},
};
const text: WizardPrompter["text"] = vi.fn(async (params) => {
if (params.message === "Gateway WebSocket URL") {
return "wss://gateway.example/rpc?account=work";
}
if (params.message === "Gateway token") {
return "replacement-token";
}
return "";
}) as WizardPrompter["text"];
const prompter = createPrompter({
confirm: vi.fn(async () => false),
select: createSelectPrompter({
"Gateway auth": "token",
"How do you want to provide this gateway token?": "plaintext",
}),
text,
});
const next = await promptRemoteGatewayConfig(cfg, prompter);
expect(next.gateway?.remote).toEqual({
url: "wss://gateway.example/rpc?account=work",
token: "replacement-token",
});
expect(vi.mocked(text).mock.calls.map(([params]) => params.message)).toContain("Gateway token");
});
it("defaults discovered direct remote URLs to wss://", async () => {
detectBinary.mockResolvedValue(true);
discoverGatewayBeacons.mockResolvedValue([createGatewayDiscoveryBeacon()]);
@@ -419,7 +497,12 @@ describe("promptRemoteGatewayConfig", () => {
});
const cfg = {
gateway: { remote: { token: "preexisting-remote-token" } },
gateway: {
remote: {
url: "wss://remote.example.com:18789",
token: "preexisting-remote-token",
},
},
} as OpenClawConfig;
const prompter = createPrompter({ confirm, select, text });
@@ -457,7 +540,12 @@ describe("promptRemoteGatewayConfig", () => {
});
const cfg = {
gateway: { remote: { password: "preexisting-remote-password" } },
gateway: {
remote: {
url: "wss://remote.example.com:18789",
password: "preexisting-remote-password",
},
},
} as OpenClawConfig;
const prompter = createPrompter({ confirm, select, text });
+21 -4
View File
@@ -1,5 +1,8 @@
import { parseStrictNonNegativeInteger } from "@openclaw/normalization-core/number-coercion";
import { gatewayOriginScope } from "../../packages/gateway-client/src/gateway-origin-scope.js";
import {
gatewayCredentialScope,
gatewayOriginScope,
} from "../../packages/gateway-client/src/gateway-origin-scope.js";
/**
* Interactive remote gateway onboarding.
*
@@ -175,6 +178,10 @@ export async function promptRemoteGatewayConfig(
validate: (value) => validateGatewayWebSocketUrl(value),
});
const url = ensureWsUrl(urlInput);
const storedRemote = cfg.gateway?.remote;
const sameCredentialTarget =
storedRemote?.url !== undefined &&
gatewayCredentialScope(url) === gatewayCredentialScope(storedRemote.url);
const pinnedDiscoveryFingerprint =
discoveryTlsFingerprint && url === trustedDiscoveryUrl ? discoveryTlsFingerprint : undefined;
@@ -187,8 +194,8 @@ export async function promptRemoteGatewayConfig(
],
});
let token: SecretInput | undefined = cfg.gateway?.remote?.token;
let password: SecretInput | undefined = cfg.gateway?.remote?.password;
let token: SecretInput | undefined = sameCredentialTarget ? storedRemote.token : undefined;
let password: SecretInput | undefined = sameCredentialTarget ? storedRemote.password : undefined;
if (authChoice === "token") {
const selectedMode = await resolveSecretInputModeForEnvSelection({
prompter,
@@ -290,6 +297,15 @@ export async function promptRemoteGatewayConfig(
edgeAuthOriginUrl && gatewayOriginScope(url) === gatewayOriginScope(edgeAuthOriginUrl)
? cfg.gateway?.remote?.edgeAuth
: undefined;
const {
url: _storedUrl,
token: _storedToken,
password: _storedPassword,
edgeAuth: _storedEdgeAuth,
tlsFingerprint: storedTlsFingerprint,
...storedTargetSettings
} = sameCredentialTarget && storedRemote ? storedRemote : {};
const tlsFingerprint = pinnedDiscoveryFingerprint ?? storedTlsFingerprint;
return {
...cfg,
@@ -297,11 +313,12 @@ export async function promptRemoteGatewayConfig(
...cfg.gateway,
mode: "remote",
remote: {
...storedTargetSettings,
url,
...(edgeAuth !== undefined ? { edgeAuth } : {}),
...(token !== undefined ? { token } : {}),
...(password !== undefined ? { password } : {}),
...(pinnedDiscoveryFingerprint ? { tlsFingerprint: pinnedDiscoveryFingerprint } : {}),
...(tlsFingerprint ? { tlsFingerprint } : {}),
},
},
};