mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 14:26:07 +00:00
fix(setup): flag unresolved migrated Claude Desktop storage (#8971)
* fix(setup): flag unresolved migrated Claude Desktop storage * fix(setup): preserve Claude MSIX process view when config is unresolved
This commit is contained in:
1 parent
d854fb155a
commit
3484432fb1
5 files changed
+128
-17
No files matched your search
@@ -155,7 +155,7 @@ in the file the installed app reads:
|
||||
|
||||
- Without Claude Desktop's MSIX package storage, setup uses
|
||||
`%APPDATA%\Claude\claude_desktop_config.json`.
|
||||
- The MSIX build (package family `Claude_pzs8sxrjxfjjc`, installed under
|
||||
- The legacy MSIX storage layout (package family `Claude_pzs8sxrjxfjjc`, installed under
|
||||
`C:\Program Files\WindowsApps\Claude_<version>_x64__pzs8sxrjxfjjc`)
|
||||
virtualizes AppData writes. Once it has saved its configuration, it reads
|
||||
the package-private copy
|
||||
@@ -164,6 +164,11 @@ in the file the installed app reads:
|
||||
private copy when it exists. Before the package has saved one, they use the
|
||||
`%APPDATA%` file it still reads; with neither file, setup creates the
|
||||
private copy.
|
||||
- If `%LOCALAPPDATA%\Claude-Data\claude_pzs8sxrjxfjjc` exists, setup, doctor
|
||||
and uninstall report an unresolved location. Migrated MSIX installations
|
||||
have been observed reading `claude_desktop_config.json` in this directory.
|
||||
Quit Claude Desktop and verify its active configuration before registering
|
||||
manually. See [#8968](https://github.com/morluto/rea/issues/8968).
|
||||
- If `%LOCALAPPDATA%\Packages` holds a `Claude_<publisher ID>` package from
|
||||
another publisher, an `AnthropicPBC.Claude_<publisher ID>` package, or cannot
|
||||
be read, REA cannot establish which file Claude
|
||||
|
||||
@@ -21,6 +21,11 @@ const env = {
|
||||
LOCALAPPDATA: join(home, "AppData", "Local"),
|
||||
};
|
||||
const packages = join(env.LOCALAPPDATA, "Packages");
|
||||
const migratedDirectory = join(
|
||||
env.LOCALAPPDATA,
|
||||
"Claude-Data",
|
||||
CLAUDE_DESKTOP_PACKAGE_FAMILY.toLowerCase(),
|
||||
);
|
||||
const appDataConfig = join(env.APPDATA, "Claude", "claude_desktop_config.json");
|
||||
const privateConfig = join(
|
||||
packages,
|
||||
@@ -77,6 +82,33 @@ const packagedFileSystem = (
|
||||
});
|
||||
|
||||
describe("Claude Desktop MSIX process view", () => {
|
||||
it.each([true, false])(
|
||||
"reports migrated storage as unresolved with legacy package storage %s",
|
||||
(hasLegacyStorage) => {
|
||||
const fs = packagedFileSystem((path) => path);
|
||||
expect(
|
||||
resolveWindowsClaudeDesktopLocation(home, env, {
|
||||
...fs,
|
||||
readdir: (path) =>
|
||||
path === packages && !hasLegacyStorage ? [] : fs.readdir(path),
|
||||
lstat: (path) =>
|
||||
path === migratedDirectory ? folder : fs.lstat(path),
|
||||
}),
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error: {
|
||||
path: join(migratedDirectory, "claude_desktop_config.json"),
|
||||
message: expect.stringContaining("migrated Claude Desktop storage"),
|
||||
...(hasLegacyStorage
|
||||
? {
|
||||
msixPackage: expect.objectContaining({ processView: "direct" }),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("recognizes a process that sees the private copy at the AppData path", () => {
|
||||
const location = resolveWindowsClaudeDesktopLocation(
|
||||
home,
|
||||
@@ -535,7 +567,9 @@ describe("unresolved Claude Desktop package identity", () => {
|
||||
])("does not treat %s as a Claude package family", (name) => {
|
||||
const location = resolveWindowsClaudeDesktopLocation(home, env, {
|
||||
readdir: () => [name],
|
||||
lstat: () => file,
|
||||
lstat: () => {
|
||||
throw failure("ENOENT");
|
||||
},
|
||||
realpath: (path) => path,
|
||||
});
|
||||
expect(location).toEqual({
|
||||
@@ -564,8 +598,8 @@ describe("unresolved Claude Desktop package identity", () => {
|
||||
it("reports an uninspectable private copy instead of choosing a file", () => {
|
||||
const location = resolveWindowsClaudeDesktopLocation(home, env, {
|
||||
readdir: () => [CLAUDE_DESKTOP_PACKAGE_FAMILY],
|
||||
lstat: () => {
|
||||
throw failure("EPERM");
|
||||
lstat: (path) => {
|
||||
throw failure(path === privateConfig ? "EPERM" : "ENOENT");
|
||||
},
|
||||
realpath: (path) => path,
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface ClaudeDesktopLocation {
|
||||
export interface ClaudeDesktopLocationFailure {
|
||||
readonly path: string;
|
||||
readonly message: string;
|
||||
readonly msixPackage?: MsixPackageView;
|
||||
}
|
||||
|
||||
/** Synchronous probes; each throws for failures other than absence. */
|
||||
@@ -120,14 +121,46 @@ export const resolveWindowsClaudeDesktopLocation = (
|
||||
path: join(packagesDirectory, foreign[0] ?? ""),
|
||||
message: `MSIX package storage ${foreign.map((name) => join(packagesDirectory, name)).join(", ")} has an unrecognized Claude Desktop package identity or deployment (the supported identity is ${CLAUDE_DESKTOP_PACKAGE_FAMILY}). REA cannot establish which ${CONFIG_FILE} that package reads and does not fall back to ${appDataConfigPath}. Quit Claude Desktop fully and register REA in the configuration the installed app reads.`,
|
||||
});
|
||||
const migratedDirectory = join(
|
||||
roots.local,
|
||||
"Claude-Data",
|
||||
CLAUDE_DESKTOP_PACKAGE_FAMILY.toLowerCase(),
|
||||
);
|
||||
const migrated = present(fileSystem, migratedDirectory);
|
||||
const migratedConfigPath = join(migratedDirectory, CONFIG_FILE);
|
||||
const migrationFailure = !migrated.ok
|
||||
? migrated.error
|
||||
: migrated.value
|
||||
? {
|
||||
path: migratedConfigPath,
|
||||
message: `Found migrated Claude Desktop storage at ${migratedDirectory}. REA cannot verify which configuration the installed app reads. Quit Claude Desktop fully, verify whether it reads ${migratedConfigPath}, then register REA manually in the active file.`,
|
||||
}
|
||||
: undefined;
|
||||
const family = families[0];
|
||||
if (family === undefined)
|
||||
return ok({ configPath: appDataConfigPath, markerPath: appDataDirectory });
|
||||
return migrationFailure === undefined
|
||||
? ok({ configPath: appDataConfigPath, markerPath: appDataDirectory })
|
||||
: err(migrationFailure);
|
||||
const container = join(packagesDirectory, family);
|
||||
const privateRoaming = join(container, "LocalCache", "Roaming");
|
||||
const privateDirectory = join(privateRoaming, "Claude");
|
||||
const privateConfigPath = join(privateDirectory, CONFIG_FILE);
|
||||
const privateCopy = present(fileSystem, privateConfigPath);
|
||||
const packageView = (): MsixPackageView => ({
|
||||
packageFamilyName: family,
|
||||
privateConfigPath,
|
||||
appDataConfigPath,
|
||||
virtualizedRoots: [roots.roaming, roots.local],
|
||||
processView: processView(
|
||||
fileSystem,
|
||||
privateRoaming,
|
||||
roots.roaming,
|
||||
privateCopy.ok && privateCopy.value,
|
||||
),
|
||||
});
|
||||
// The current process view matters even when the active Claude config is unknown.
|
||||
if (migrationFailure !== undefined)
|
||||
return err({ ...migrationFailure, msixPackage: packageView() });
|
||||
if (!privateCopy.ok) return privateCopy;
|
||||
const appDataCopy = privateCopy.value
|
||||
? ok(false)
|
||||
@@ -139,18 +172,7 @@ export const resolveWindowsClaudeDesktopLocation = (
|
||||
? privateConfigPath
|
||||
: appDataConfigPath,
|
||||
markerPath: container,
|
||||
msixPackage: {
|
||||
packageFamilyName: family,
|
||||
privateConfigPath,
|
||||
appDataConfigPath,
|
||||
virtualizedRoots: [roots.roaming, roots.local],
|
||||
processView: processView(
|
||||
fileSystem,
|
||||
privateRoaming,
|
||||
roots.roaming,
|
||||
privateCopy.value,
|
||||
),
|
||||
},
|
||||
msixPackage: packageView(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mkdir, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { FakeSetupHost, options } from "./Setup.fixture.js";
|
||||
import { runSetup } from "./Setup.js";
|
||||
import { currentClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
|
||||
@@ -9,6 +11,7 @@ import { discoverSetupState } from "./SetupPlan.js";
|
||||
import type { MsixPackageView } from "./MsixPackageStorage.js";
|
||||
import {
|
||||
registrationWriteBlocker,
|
||||
supportedClients,
|
||||
type SetupClient,
|
||||
} from "./SupportedClients.js";
|
||||
|
||||
@@ -626,6 +629,49 @@ describe("MSIX Claude Desktop setup", () => {
|
||||
expect(host.configurations).toBe(1);
|
||||
});
|
||||
|
||||
it("blocks redirected AppData writes when migrated Claude storage is unresolved", async () => {
|
||||
const home = await createTestTempDirectory("setup-claude-migrated-");
|
||||
const local = join(home, "Local");
|
||||
const roaming = join(home, "Roaming");
|
||||
const family = "Claude_pzs8sxrjxfjjc";
|
||||
const privateRoaming = join(
|
||||
local,
|
||||
"Packages",
|
||||
family,
|
||||
"LocalCache",
|
||||
"Roaming",
|
||||
);
|
||||
const privateClaude = join(privateRoaming, "Claude");
|
||||
await mkdir(privateClaude, { recursive: true });
|
||||
await writeFile(join(privateClaude, "claude_desktop_config.json"), "{}");
|
||||
await mkdir(join(local, "Claude-Data", family.toLowerCase()), {
|
||||
recursive: true,
|
||||
});
|
||||
await symlink(privateRoaming, roaming, "junction");
|
||||
|
||||
const host = new FakeSetupHost("win32");
|
||||
host.hopper = join(home, "Hopper");
|
||||
host.skill = "unchanged";
|
||||
host.clients = supportedClients(home, "win32", {
|
||||
APPDATA: roaming,
|
||||
LOCALAPPDATA: local,
|
||||
});
|
||||
expect(
|
||||
host.clients.find(({ name }) => name === "claude_desktop")
|
||||
?.configPathError,
|
||||
).toContain("migrated Claude Desktop storage");
|
||||
|
||||
const blocked = await runSetup(
|
||||
{ ...options(true), clientIds: ["vscode"] },
|
||||
host,
|
||||
);
|
||||
expect(blocked.status).toBe("needs_human");
|
||||
expect(blocked.remediation).toContain(
|
||||
`Changes to ${join(roaming, "Code", "User", "mcp.json")} would not reach the files other apps read.`,
|
||||
);
|
||||
expect(host.configurations).toBe(0);
|
||||
});
|
||||
|
||||
it("refuses registrations whose launcher only one MSIX package can see", async () => {
|
||||
const host = new FakeSetupHost("win32");
|
||||
host.hopper = join(PROFILE, "Hopper");
|
||||
|
||||
@@ -70,6 +70,7 @@ export interface SetupClient {
|
||||
interface ClientPathFailure {
|
||||
readonly path: string;
|
||||
readonly message: string;
|
||||
readonly msixPackage?: MsixPackageView;
|
||||
}
|
||||
|
||||
type ClientPathResult = Result<string, ClientPathFailure>;
|
||||
@@ -904,6 +905,9 @@ export const supportedClients = (
|
||||
format: definition.format,
|
||||
configPath: location.error.path,
|
||||
configPathError: location.error.message,
|
||||
...(location.error.msixPackage === undefined
|
||||
? {}
|
||||
: { msixPackage: location.error.msixPackage }),
|
||||
};
|
||||
const { configPath, markerPath, msixPackage } = location.value;
|
||||
return {
|
||||
|
||||
Reference in new issue
Block a user