fix(device-pair): reject invalid remote setup URLs

Fail setup-code generation when gateway.remote.url is configured but malformed, instead of falling back to a bind-derived URL and issuing a bootstrap token.
This commit is contained in:
clawsweeper[bot]
2026-04-29 20:45:33 +01:00
committed by GitHub
parent 21b3eb5c34
commit 7c51cd2baf
5 changed files with 80 additions and 10 deletions

View File

@@ -70,6 +70,7 @@ type ApprovedPairingDevice = ApprovedPairingResult["device"];
const INTERNAL_PAIRING_SCOPES = ["operator.write", "operator.pairing"];
function createApi(params?: {
config?: OpenClawPluginApi["config"];
runtime?: OpenClawPluginApi["runtime"];
pluginConfig?: Record<string, unknown>;
registerCommand?: (command: OpenClawPluginCommandDefinition) => void;
@@ -78,7 +79,7 @@ function createApi(params?: {
id: "device-pair",
name: "device-pair",
source: "test",
config: {
config: params?.config ?? {
gateway: {
auth: {
mode: "token",
@@ -96,6 +97,7 @@ function createApi(params?: {
}
function registerPairCommand(params?: {
config?: OpenClawPluginApi["config"];
runtime?: OpenClawPluginApi["runtime"];
pluginConfig?: Record<string, unknown>;
}): OpenClawPluginCommandDefinition {
@@ -649,6 +651,36 @@ describe("device-pair /pair default setup code", () => {
expect(result).toEqual({ text: "Error: Configured publicUrl is invalid." });
});
it("rejects invalid gateway.remote.url before falling back to bind-derived setup urls", async () => {
const command = registerPairCommand({
config: {
gateway: {
bind: "custom",
customBindHost: "127.0.0.1",
remote: { url: "http://localhost:notaport" },
auth: {
mode: "token",
token: "gateway-token",
},
},
},
pluginConfig: {
publicUrl: undefined,
},
});
const result = await command.handler(
createCommandContext({
channel: "webchat",
args: "",
commandBody: "/pair",
gatewayClientScopes: ["operator.write", "operator.pairing"],
}),
);
expect(pluginApiMocks.issueDeviceBootstrapToken).not.toHaveBeenCalled();
expect(result).toEqual({ text: "Error: Configured gateway.remote.url is invalid." });
});
it.each([
"http://localhost:notaport",
"http:gateway.example.test",

View File

@@ -321,6 +321,12 @@ async function resolveGatewayUrl(api: OpenClawPluginApi): Promise<ResolveUrlResu
return { error: "Configured publicUrl is invalid." };
}
const configuredRemoteUrl = normalizeOptionalString(cfg.gateway?.remote?.url);
const remoteUrl = configuredRemoteUrl ? normalizeUrl(configuredRemoteUrl, scheme) : null;
if (configuredRemoteUrl && !remoteUrl) {
return { error: "Configured gateway.remote.url is invalid." };
}
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
if (tailscaleMode === "serve" || tailscaleMode === "funnel") {
const host = await resolveTailnetHost();
@@ -330,12 +336,8 @@ async function resolveGatewayUrl(api: OpenClawPluginApi): Promise<ResolveUrlResu
return { url: `wss://${host}`, source: `gateway.tailscale.mode=${tailscaleMode}` };
}
const remoteUrl = normalizeOptionalString(cfg.gateway?.remote?.url);
if (remoteUrl) {
const url = normalizeUrl(remoteUrl, scheme);
if (url) {
return { url, source: "gateway.remote.url" };
}
return { url: remoteUrl, source: "gateway.remote.url" };
}
const bindResult = resolveGatewayBindUrl({