mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
fix(onboard): preserve remote transport settings
This commit is contained in:
@@ -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 ?? "")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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 } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user