fix(onboard): preserve auth for equivalent remote URLs

This commit is contained in:
Amp
2026-08-21 05:17:47 +00:00
parent 5570c5ffac
commit 3d119af588
5 changed files with 83 additions and 4 deletions
@@ -201,6 +201,34 @@ describe("runNonInteractiveRemoteSetup", () => {
expect(commit?.nextConfig.gateway?.remote).toEqual(remote);
});
it("preserves endpoint-bound config when the remote URL has equivalent spelling", async () => {
const existingRemote = {
url: `${remoteUrl}/`,
token: { source: "env" as const, provider: "default", id: "EXISTING_REMOTE_TOKEN" },
tlsFingerprint: "sha256:test-fingerprint",
sshTarget: "operator@gateway.example.test",
edgeAuth: { "X-Edge-Auth": "existing-edge-secret" },
};
await runNonInteractiveRemoteSetup({
opts: {
nonInteractive: true,
mode: "remote",
remoteUrl,
secretInputMode: "ref",
skipHooks: true,
},
runtime,
baseConfig: { gateway: { mode: "remote", remote: existingRemote } },
});
const commit = commitNonInteractiveOnboardConfigMock.mock.calls[0]?.[0];
expect(commit?.nextConfig.gateway?.remote).toEqual({
...existingRemote,
url: remoteUrl,
});
});
it("preserves an existing remote password SecretRef when no replacement is provided", async () => {
const remote = {
url: remoteUrl,
@@ -13,6 +13,7 @@ import { createGatewayEnvSecretRef } from "../../secrets/ref-contract.js";
import { applySkipBootstrapConfig } from "../onboard-config.js";
import { applyWizardMetadata } from "../onboard-helpers.js";
import { enableDefaultOnboardingInternalHooks } from "../onboard-hooks.js";
import { remoteGatewayUrlChanged } from "../onboard-remote-url.js";
import type { OnboardOptions } from "../onboard-types.js";
import { commitNonInteractiveOnboardConfig } from "./config-write.js";
@@ -54,7 +55,7 @@ export async function runNonInteractiveRemoteSetup(params: {
return;
}
const existingRemote = baseConfig.gateway?.remote;
const remoteUrlChanged = normalizeOptionalString(existingRemote?.url) !== remoteUrl;
const remoteUrlChanged = remoteGatewayUrlChanged(remoteUrl, existingRemote?.url);
// A remote block belongs to one endpoint. Reusing it for a different URL can
// send old credentials or keep routing through the old SSH target.
const preservedRemote = remoteUrlChanged ? {} : { ...existingRemote };
+11
View File
@@ -0,0 +1,11 @@
import { gatewayOriginScope } from "../../packages/gateway-client/src/gateway-origin-scope.js";
/** Returns whether an explicit remote Gateway URL selects a different endpoint. */
export function remoteGatewayUrlChanged(
nextUrl: string | undefined,
previousUrl: string | undefined,
): boolean {
return (
nextUrl !== undefined && gatewayOriginScope(nextUrl) !== gatewayOriginScope(previousUrl ?? "")
);
}
+39
View File
@@ -1287,6 +1287,45 @@ describe("runSetupWizard", () => {
);
});
it("reuses stored remote credentials for an equivalent URL spelling", async () => {
const storedRemote = {
url: "wss://stored.example.com:18789",
token: { source: "env" as const, provider: "default", id: "STORED_GATEWAY_TOKEN" },
tlsFingerprint: "sha256:test-fingerprint",
edgeAuth: { "X-Edge-Auth": "test-secret" },
};
readConfigFileSnapshot.mockResolvedValueOnce(
configSnapshot({ gateway: { mode: "remote", remote: storedRemote } }),
);
await runSetupWizard(
{
acceptRisk: true,
flow: "advanced",
mode: "remote",
remoteUrl: `${storedRemote.url}/`,
},
createRuntime(),
buildWizardPrompter({}),
);
expect(promptRemoteGatewayConfig).toHaveBeenCalledWith(
expect.objectContaining({
gateway: expect.objectContaining({
remote: {
...storedRemote,
url: `${storedRemote.url}/`,
},
}),
}),
expect.any(Object),
{
secretInputMode: undefined,
edgeAuthOriginUrl: storedRemote.url,
},
);
});
it("does not probe an invalid CLI remote URL with its token", async () => {
const remoteToken = "REDACTED";
validateGatewayWebSocketUrl.mockReturnValueOnce("Use wss:// for public gateways");
+3 -3
View File
@@ -4,6 +4,7 @@ import { listAgentEntries } from "../agents/agent-scope-config.js";
import { formatCliCommand } from "../cli/command-format.js";
import { resolveOnboardingSetupTarget } from "../commands/onboard-agent-target.js";
import * as firstAgentOnboarding from "../commands/onboard-first-agent.js";
import { remoteGatewayUrlChanged } from "../commands/onboard-remote-url.js";
import type { OnboardMode, OnboardOptions } from "../commands/onboard-types.js";
import { hasResolvedRosterBeforeMigrations } from "../config/agent-roster-provenance.js";
import { ConfigMutationConflictError } from "../config/config.js";
@@ -16,8 +17,7 @@ import {
buildPluginCompatibilitySnapshotNotices,
formatPluginCompatibilityNotice,
} from "../plugins/status.js";
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { createLazyRuntimeModule } from "../shared/lazy-runtime.js";
import { resolveUserPath } from "../utils.js";
import { t } from "./i18n/index.js";
@@ -366,7 +366,7 @@ async function runSetupWizardOnce(
const optionRemoteUrl = normalizeOptionalString(opts.remoteUrl);
const optionRemoteToken = normalizeOptionalString(opts.remoteToken);
const optionRemotePassword = normalizeOptionalString(opts.remotePassword);
const remoteUrlChanged = opts.remoteUrl !== undefined && optionRemoteUrl !== storedRemoteUrl;
const remoteUrlChanged = remoteGatewayUrlChanged(opts.remoteUrl, storedRemoteUrl);
const remoteSeedConfig: OpenClawConfig =
opts.remoteUrl === undefined &&
opts.remoteToken === undefined &&