fix(setup): preserve equivalent caller launcher paths

This commit is contained in:
morluto committed 2026-10-24 02:45:14 +08:00
1 parent f19aebab76
commit adafef2c9e
4 files changed
+94 -1

No files matched your search

+6
View File
@@ -265,6 +265,12 @@ the diagnostic identifies the setting to repair. See the
[Codex configuration reference](https://developers.openai.com/codex/config-reference/)
for the supported policy values.
When direct launcher paths prove the same executable or Node entrypoint,
setup retains the existing caller-selected spelling. An otherwise aligned
registration therefore needs no rewrite or recovery backup merely because
REA was invoked through another symlink. Unresolved identity does not prove
equivalence.
| Settings | Setup ownership |
| ----------------------------------------- | --------------------------------------------------------------------- |
| Launcher, transport and activation | Refreshes the REA stdio connection and enables it. |
+11 -1
View File
@@ -14,6 +14,7 @@ import {
type ClientRegistrationDialect,
} from "./ClientConfigurationDocument.js";
import type { SetupClient } from "./SupportedClients.js";
import { equivalentClientRegistrationCommands } from "./ClientRegistrationIdentity.js";
const tomlTimeoutSchema = z.union([
z.number().positive(),
@@ -181,8 +182,17 @@ export const refreshClientRegistration = (
const existing = source.success ? source.data : {};
const policy = validateClientPolicy(existing, client);
if (!policy.ok) return policy;
const prior = parseClientRegistration(existing, client);
const priorCommand = prior.ok
? [prior.value.command, ...prior.value.args]
: undefined;
const launcher =
priorCommand !== undefined &&
equivalentClientRegistrationCommands(priorCommand, command)
? priorCommand
: command;
const registration = {
...clientRegistrationEntry(dialect, command, environment),
...clientRegistrationEntry(dialect, launcher, environment),
...Object.fromEntries(
Object.entries(existing).filter(
([key]) => !managedConnectionKeys.has(key),
@@ -149,6 +149,40 @@ const directEntrypointIdentity = (
: "unknown";
};
/** Preserve caller spelling only when both direct launchers prove the same file. */
export const equivalentClientRegistrationCommands = (
previous: readonly string[],
requested: readonly string[],
): boolean => {
if (
previous.length === requested.length &&
previous.every((value, index) => value === requested[index])
)
return true;
const entrypoint = (command: readonly string[]): string | undefined => {
if (
command.length === 2 &&
command[1] === "mcp" &&
isAbsolute(command[0] ?? "")
)
return command[0];
if (
command.length === 3 &&
command[2] === "mcp" &&
isAbsolute(command[1] ?? "") &&
entrypointIdentity(command[0] ?? "", process.execPath) === "owned"
)
return normalizeEntrypointPath(command[1]);
return undefined;
};
const current = entrypoint(requested);
return (
current !== undefined &&
entrypoint(previous) !== undefined &&
classifyClientRegistrationCommand(previous, current) === "owned"
);
};
/** Classify the command once; uncertainty never grants positive ownership. */
export const classifyClientRegistrationCommand = (
command: readonly string[],
@@ -17,6 +17,7 @@ import { readConfigurationFile } from "../../../src/application/ClientConfigurat
import {
configureClientConfiguration,
inspectClientConfiguration,
clientConfigurationAligned,
} from "../../../src/application/SetupClientConfiguration.js";
import { resolveWindowsClaudeDesktopLocation } from "../../../src/application/ClaudeDesktopConfigLocation.js";
import { supportedClients } from "../../../src/application/SupportedClients.js";
@@ -26,6 +27,48 @@ import { PRODUCT_IDENTITY } from "../../../src/identity.js";
const pinnedPackage = PRODUCT_IDENTITY.registrationPackageSpecifier;
it.skipIf(process.platform === "win32")(
"preserves an aligned caller launcher alias without writes or a backup",
async () => {
const home = await createTestTempDirectory(
"rea-config-equivalent-launcher-",
);
const real = join(home, "rea-current");
const alias = join(home, "rea-alias");
await writeFile(real, "current launcher\n");
await symlink(real, alias);
await mkdir(join(home, ".cursor"));
const configPath = join(home, ".cursor", "mcp.json");
const original = JSON.stringify({
mcpServers: {
rea: { command: alias, args: ["mcp"], future_policy: { retain: true } },
},
});
await writeFile(configPath, original);
const client = { name: "cursor", format: "json", configPath } as const;
expect(await clientConfigurationAligned(client, {}, [real, "mcp"])).toBe(
true,
);
expect(await inspectClientConfiguration(client, {}, [real, "mcp"])).toEqual(
{ status: "already_current" },
);
expect(
await configureClientConfiguration(client, {}, [real, "mcp"]),
).toEqual({ status: "unchanged" });
expect(await readFile(configPath, "utf8")).toBe(original);
await expect(readFile(`${configPath}.rea.backup`)).rejects.toThrow();
const other = join(home, "another-launcher");
await writeFile(other, "different launcher\n");
expect(
await configureClientConfiguration(client, {}, [other, "mcp"]),
).toMatchObject({ status: "configured" });
expect(JSON.parse(await readFile(configPath, "utf8"))).toMatchObject({
mcpServers: { rea: { command: other, future_policy: { retain: true } } },
});
expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original);
},
);
describe("JSON client configuration transaction: writing configuration through safe paths", () => {
it.skipIf(typeof process.getuid !== "function")(
"rejects a symlink target not owned by the current user",