mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
e4ac7bbfc2
* fix: order remaining outputs independently of host locale
String.prototype.localeCompare without a pinned collation follows the
host's ICU default locale, so the same input could be ordered differently
on different machines. 614fb768 moved canonical domain projections to
compareUnicodeCodePoints; provider, application, catalog, and setup
orderings still used locale collation.
Order every remaining source site by Unicode code point, including Mach-O
symbols, firmware extraction paths, ASAR entries (previously pinned to
"en" collation), evidence ID sets, browser and Electron target, script,
and resource inventories, reconstruction obligations, capability and
provider listings, and setup environment details.
The current tool and prompt catalog names order identically under both
comparators, so the catalog identity digest is unchanged.
* fix(artifacts): order ASAR entries by their portable path
@electron/asar lists host-spelled paths, so code-point order over the raw
spelling still differed by host: "a\b" sorts after "a0" on Windows while
"a/b" sorts before it on POSIX. Pair each listing with its portable
archive path once, order by that path, and keep the host spelling for
@electron/asar calls and adapter keys.
342 lines
9.8 KiB
TypeScript
342 lines
9.8 KiB
TypeScript
import { readClientPolicyBlock } from "./GeminiClientSettings.js";
|
|
import {
|
|
effectiveClientConfiguration,
|
|
readClientConfigurationFiles,
|
|
} from "./ClientConfigurationFiles.js";
|
|
import {
|
|
clientServerForcedEnabled,
|
|
clientServerListedDisabled,
|
|
effectiveClientServer,
|
|
} from "./ClientConfigurationDocument.js";
|
|
import { access } from "node:fs/promises";
|
|
import { resolve } from "node:path";
|
|
|
|
import { z } from "zod";
|
|
|
|
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js";
|
|
import {
|
|
isOwnedClientRegistrationCommand,
|
|
packageRunnerRegistrationCommand,
|
|
} from "./ClientRegistrationIdentity.js";
|
|
import {
|
|
clientEvidencePaths,
|
|
manualRegistrationRemediation,
|
|
supportedClients,
|
|
} from "./SupportedClients.js";
|
|
import type { SetupClient } from "./SupportedClients.js";
|
|
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
|
|
|
interface ClientRegistrationStatusBase {
|
|
readonly client: string;
|
|
readonly config_path: string;
|
|
}
|
|
|
|
type RegistrationCommand = readonly [string, ...string[]];
|
|
|
|
export type ClientRegistrationStatus = ClientRegistrationStatusBase &
|
|
(
|
|
| {
|
|
readonly command: RegistrationCommand;
|
|
readonly state: "aligned";
|
|
readonly remediation: null;
|
|
}
|
|
| {
|
|
readonly command: RegistrationCommand;
|
|
readonly state: "stale";
|
|
readonly remediation: string;
|
|
}
|
|
| {
|
|
readonly command: readonly [];
|
|
readonly state: "missing" | "invalid" | "manual";
|
|
readonly remediation: string;
|
|
}
|
|
);
|
|
|
|
export type UnhealthyClientRegistrationStatus = Exclude<
|
|
ClientRegistrationStatus,
|
|
{ readonly state: "aligned" }
|
|
>;
|
|
|
|
const registrationSchema = z
|
|
.object({
|
|
command: z.string().min(1),
|
|
args: z.array(z.string()).default([]),
|
|
startup_timeout_sec: z.number().positive().optional(),
|
|
type: z.string().optional(),
|
|
transport: z.string().optional(),
|
|
tools: z.array(z.string()).optional(),
|
|
disabled: z.boolean().optional(),
|
|
enabled: z.boolean().optional(),
|
|
})
|
|
.passthrough();
|
|
|
|
/** Inspect supported client registrations without reading their environment. */
|
|
export const readClientRegistrationStatuses = async (
|
|
home: string,
|
|
currentCommandPath: string = resolve(process.argv[1] ?? "unknown"),
|
|
options: {
|
|
readonly platform?: NodeJS.Platform;
|
|
readonly environment?: NodeJS.ProcessEnv;
|
|
} = {},
|
|
): Promise<readonly ClientRegistrationStatus[]> => {
|
|
const statuses: ClientRegistrationStatus[] = [];
|
|
for (const client of supportedClients(
|
|
home,
|
|
options.platform,
|
|
options.environment,
|
|
)) {
|
|
if (client.configPathError !== undefined) {
|
|
statuses.push({
|
|
client: client.name,
|
|
config_path: client.configPath,
|
|
command: [],
|
|
state: "invalid",
|
|
remediation: client.configPathError,
|
|
});
|
|
continue;
|
|
}
|
|
if (!(await clientEvidencePresent(client))) continue;
|
|
const manualRemediation = manualRegistrationRemediation(client.name);
|
|
if (client.format === "unsupported") {
|
|
if (manualRemediation !== undefined)
|
|
statuses.push({
|
|
client: client.name,
|
|
config_path: client.markerPath ?? client.configPath,
|
|
command: [],
|
|
state: "manual",
|
|
remediation: manualRemediation,
|
|
});
|
|
continue;
|
|
}
|
|
try {
|
|
const files = await readClientConfigurationFiles(client);
|
|
if (!files.ok) {
|
|
statuses.push({
|
|
client: client.name,
|
|
config_path: client.configPath,
|
|
command: [],
|
|
state: "invalid",
|
|
remediation: files.error.detail,
|
|
});
|
|
continue;
|
|
}
|
|
const parsed = effectiveClientConfiguration(files.value);
|
|
if (parsed === undefined) {
|
|
statuses.push(
|
|
unavailableStatus(client.name, client.configPath, "missing"),
|
|
);
|
|
continue;
|
|
}
|
|
const policy = await readClientPolicyBlock(client, parsed);
|
|
if (!policy.ok) {
|
|
statuses.push({
|
|
client: client.name,
|
|
config_path: client.configPath,
|
|
command: [],
|
|
state: "invalid",
|
|
remediation: policy.error.detail,
|
|
});
|
|
continue;
|
|
}
|
|
const policyBlock = policy.value;
|
|
const raw = effectiveClientServer(parsed, PRODUCT_IDENTITY.mcpServerKey);
|
|
if (raw === undefined) {
|
|
statuses.push(
|
|
unavailableStatus(client.name, client.configPath, "missing"),
|
|
);
|
|
continue;
|
|
}
|
|
const registration = parseRegistration(raw, client);
|
|
const command: RegistrationCommand = [
|
|
registration.command,
|
|
...registration.args,
|
|
];
|
|
statuses.push(
|
|
configuredStatus(
|
|
client.name,
|
|
client.configPath,
|
|
command,
|
|
registrationAligned(
|
|
registration,
|
|
client,
|
|
currentCommandPath,
|
|
options.platform ?? process.platform,
|
|
clientServerForcedEnabled(parsed, PRODUCT_IDENTITY.mcpServerKey),
|
|
) &&
|
|
!clientServerListedDisabled(
|
|
parsed,
|
|
PRODUCT_IDENTITY.mcpServerKey,
|
|
) &&
|
|
policyBlock === undefined
|
|
? "aligned"
|
|
: "stale",
|
|
policyBlock,
|
|
),
|
|
);
|
|
} catch (cause: unknown) {
|
|
statuses.push(
|
|
unavailableStatus(
|
|
client.name,
|
|
client.configPath,
|
|
isMissing(cause) ? "missing" : "invalid",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
return statuses.sort((left, right) =>
|
|
compareUnicodeCodePoints(left.client, right.client),
|
|
);
|
|
};
|
|
|
|
const registrationAligned = (
|
|
registration: z.output<typeof registrationSchema>,
|
|
client: SetupClient,
|
|
currentCommandPath: string,
|
|
platform: NodeJS.Platform,
|
|
forcedEnabled = false,
|
|
): boolean => {
|
|
const command = [registration.command, ...registration.args];
|
|
if (
|
|
(client.format !== "grok" && registration.disabled === true) ||
|
|
(registration.enabled === false && !forcedEnabled)
|
|
)
|
|
return false;
|
|
if (!isOwnedClientRegistrationCommand(command, currentCommandPath))
|
|
return false;
|
|
if (
|
|
(client.name === "codex" || client.name === "grok_build") &&
|
|
registration.startup_timeout_sec !==
|
|
MCP_STARTUP_POLICY.codexStartupTimeoutSeconds
|
|
)
|
|
return false;
|
|
if (client.format === "vscode" && registration.type !== "stdio") return false;
|
|
// OMP and Pi infer stdio for a command entry without an explicit type.
|
|
if (
|
|
(client.format === "omp" || client.format === "pi") &&
|
|
registration.type !== undefined &&
|
|
registration.type !== "stdio"
|
|
)
|
|
return false;
|
|
if (
|
|
client.format === "copilot_cli" &&
|
|
(registration.type !== "stdio" ||
|
|
JSON.stringify(registration.tools) !== JSON.stringify(["*"]))
|
|
)
|
|
return false;
|
|
if (client.format === "commandcode" && registration.transport !== "stdio")
|
|
return false;
|
|
if (
|
|
command.length === 3 &&
|
|
command[2] === "mcp" &&
|
|
resolve(command[0] ?? "") === resolve(process.execPath) &&
|
|
resolve(command[1] ?? "") === currentCommandPath
|
|
)
|
|
return true;
|
|
if (
|
|
(["npx", "bunx"] as const).some((runner) => {
|
|
const runnerCommand = packageRunnerRegistrationCommand(runner, platform);
|
|
return (
|
|
command.length === runnerCommand.length &&
|
|
command.every((argument, index) => argument === runnerCommand[index])
|
|
);
|
|
})
|
|
)
|
|
return true;
|
|
return (
|
|
command.length === 2 &&
|
|
command[1] === "mcp" &&
|
|
resolve(command[0] ?? "") === currentCommandPath
|
|
);
|
|
};
|
|
|
|
const parseRegistration = (
|
|
value: unknown,
|
|
client: SetupClient,
|
|
): z.output<typeof registrationSchema> => {
|
|
if (client.format === "opencode") {
|
|
const entry = z
|
|
.object({
|
|
type: z.literal("local"),
|
|
command: z.array(z.string()).min(1),
|
|
environment: z.record(z.string(), z.string()).optional(),
|
|
})
|
|
.passthrough()
|
|
.parse(value);
|
|
const [command = "", ...args] = entry.command;
|
|
return registrationSchema.parse({
|
|
...entry,
|
|
command,
|
|
args,
|
|
});
|
|
}
|
|
const registration = registrationSchema.parse(value);
|
|
if (
|
|
(client.format === "vscode" || client.format === "copilot_cli") &&
|
|
registration.type !== "stdio"
|
|
)
|
|
throw new TypeError("Expected an stdio registration");
|
|
if (client.format === "commandcode" && registration.transport !== "stdio")
|
|
throw new TypeError("Expected an stdio registration");
|
|
if (
|
|
(client.format === "omp" || client.format === "pi") &&
|
|
registration.type !== undefined &&
|
|
registration.type !== "stdio"
|
|
)
|
|
throw new TypeError("Expected an stdio registration");
|
|
return registration;
|
|
};
|
|
|
|
const remediation =
|
|
"Run rea setup to refresh this registration, then restart the client.";
|
|
|
|
const configuredStatus = (
|
|
client: string,
|
|
configPath: string,
|
|
command: RegistrationCommand,
|
|
state: "aligned" | "stale",
|
|
policyRemediation?: string,
|
|
): ClientRegistrationStatus =>
|
|
state === "aligned"
|
|
? { client, config_path: configPath, command, state, remediation: null }
|
|
: {
|
|
client,
|
|
config_path: configPath,
|
|
command,
|
|
state,
|
|
remediation: policyRemediation ?? remediation,
|
|
};
|
|
|
|
const unavailableStatus = (
|
|
client: string,
|
|
configPath: string,
|
|
state: "missing" | "invalid",
|
|
): ClientRegistrationStatus => ({
|
|
client,
|
|
config_path: configPath,
|
|
command: [],
|
|
state,
|
|
remediation,
|
|
});
|
|
|
|
const clientEvidencePresent = async (client: SetupClient): Promise<boolean> => {
|
|
for (const path of clientEvidencePaths(client))
|
|
if (await exists(path)) return true;
|
|
return false;
|
|
};
|
|
|
|
const exists = async (path: string | undefined): Promise<boolean> => {
|
|
if (path === undefined) return false;
|
|
try {
|
|
await access(path);
|
|
return true;
|
|
} catch (cause: unknown) {
|
|
// best-effort cleanup: optional registration probing; absence means unregistered.
|
|
void cause;
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const isMissing = (cause: unknown): boolean =>
|
|
cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|