Files
rea/src/application/ClientRegistrationIdentity.ts
T

350 lines
12 KiB
TypeScript

import { accessSync, constants, realpathSync } from "node:fs";
import { isAbsolute } from "node:path";
import { normalizeEntrypointPath } from "../process/CurrentEntrypointPath.js";
import { valid } from "semver";
import {
javaScriptRuntime,
type JavaScriptRuntime,
} from "../domain/runtimeVersion.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { PRODUCT_IDENTITY } from "../identity.js";
/** Package runners whose pinned launcher setup can register. */
export type PackageRunner = "npx" | "bunx";
/** Read one variable; Windows variable names are case-insensitive. */
const environmentValue = (
environment: Readonly<NodeJS.ProcessEnv>,
name: string,
platform: NodeJS.Platform,
): string | undefined => {
if (platform !== "win32") return environment[name];
const target = name.toLowerCase();
const key = Object.keys(environment).findLast(
(candidate) => candidate.toLowerCase() === target,
);
return key === undefined ? undefined : environment[key];
};
/** Identify the package runner that launched this process, if any. */
export const invokingPackageRunner = (
environment: Readonly<NodeJS.ProcessEnv>,
platform: NodeJS.Platform = process.platform,
): PackageRunner | undefined => {
if (environmentValue(environment, "npm_command", platform) !== "exec")
return undefined;
// `bunx` and `bun x` also report npm_command=exec, with a bun user agent.
return environmentValue(
environment,
"npm_config_user_agent",
platform,
)?.startsWith("bun/") === true
? "bunx"
: "npx";
};
/** Copy an environment without the variable that marks a package-runner launch. */
export const withoutPackageRunnerIdentity = (
environment: Readonly<NodeJS.ProcessEnv>,
platform: NodeJS.Platform = process.platform,
): NodeJS.ProcessEnv =>
Object.fromEntries(
Object.entries(environment).filter(([key]) =>
platform === "win32"
? key.toLowerCase() !== "npm_command"
: key !== "npm_command",
),
);
/** Resolve a pinned package-runner launcher that stdio clients can spawn on the host. */
export const packageRunnerRegistrationCommand = (
runner: PackageRunner,
platform: NodeJS.Platform = process.platform,
): readonly string[] => {
const command =
runner === "npx"
? ["npx", "-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"]
: ["bunx", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"];
// Both runners may be .cmd shims on Windows, which clients cannot spawn directly.
return platform === "win32" ? ["cmd.exe", "/d", "/c", ...command] : command;
};
/** Resolve a pinned npx launcher that stdio clients can spawn on the host. */
export const npxRegistrationCommand = (
platform: NodeJS.Platform = process.platform,
): readonly string[] => packageRunnerRegistrationCommand("npx", platform);
const unwrapCmdCommand = (command: readonly string[]): readonly string[] => {
const executable = command[0]?.toLowerCase();
if (executable !== "cmd" && executable !== "cmd.exe") return command;
if (command[1]?.toLowerCase() === "/c") return command.slice(2);
if (command[1]?.toLowerCase() === "/d" && command[2]?.toLowerCase() === "/c")
return command.slice(3);
return [];
};
/** Preserve the package argument from a supported package-runner command. */
const clientRegistrationPackageReference = (
command: readonly string[],
): string | undefined => {
const arguments_ = unwrapCmdCommand(command);
if (
arguments_.length === 4 &&
arguments_[0] === "npx" &&
arguments_[1] === "-y" &&
arguments_[3] === "mcp"
)
return arguments_[2];
if (
arguments_.length === 3 &&
arguments_[0] === "bunx" &&
arguments_[2] === "mcp"
)
return arguments_[1];
return undefined;
};
/** Check that one parsed command points to REA's MCP entry point. */
export type ClientRegistrationCommandIdentity = "owned" | "foreign" | "unknown";
/** Compare entrypoint paths without turning failed resolution into a foreign identity. */
const entrypointIdentity = (
candidate: string,
current: string,
): ClientRegistrationCommandIdentity => {
if (!isAbsolute(candidate) || !isAbsolute(current)) return "unknown";
if (candidate === current) return "owned";
// Bun's realpath can normalize parent segments differently from its launcher.
// Without matching traversal semantics it cannot prove ownership of an alias.
if (
process.platform !== "win32" &&
javaScriptRuntime(process.versions).id === "bun" &&
[candidate, current].some((path) => path.split("/").includes(".."))
)
return "unknown";
try {
return realpathSync.native(candidate) === realpathSync.native(current)
? "owned"
: "foreign";
} catch {
return "unknown";
}
};
/** A direct launcher may be a native executable or a Node shebang script. */
const directEntrypointIdentity = (
candidate: string,
current: string,
): ClientRegistrationCommandIdentity => {
const nativeIdentity = entrypointIdentity(candidate, current);
const scriptPath = normalizeEntrypointPath(candidate, "node");
if (scriptPath === candidate) return nativeIdentity;
// A shebang can pass the original spelling to Node, which selects its script
// lexically. Without executing the launcher, divergent interpretations are unknown.
return scriptPath !== undefined &&
entrypointIdentity(scriptPath, current) === nativeIdentity
? nativeIdentity
: "unknown";
};
/** Preserve a manager-owned executable alias only when it selects the same file. */
const sameFileExecutableAlias = (
selected: string,
name: string,
environment: Readonly<NodeJS.ProcessEnv>,
identify: typeof entrypointIdentity,
): string => {
for (const directory of (environment.PATH ?? "").split(":")) {
if (!isAbsolute(directory)) continue;
const candidate = absoluteCallerPath(`${directory}/${name}`);
if (identify(candidate, selected) !== "owned") continue;
try {
accessSync(candidate, constants.X_OK);
return candidate;
} catch {
// An unexecutable alias cannot launch the selected program.
}
}
return selected;
};
/** Prefer an executable PATH alias that proves the same selected REA file. */
export const registrationEntrypointPath = (
entrypoint: string,
environment: Readonly<NodeJS.ProcessEnv>,
platform: NodeJS.Platform,
): string =>
// A Windows command shim is a different program; file identity cannot prove
// its script target. Keep that platform's existing Node/script registration.
platform === "win32"
? entrypoint
: sameFileExecutableAlias(
entrypoint,
PRODUCT_IDENTITY.cliBinary,
environment,
directEntrypointIdentity,
);
/** Prefer a same-file runtime alias that can survive its manager's version switch. */
export const registrationRuntimePath = (
executable: string,
runtime: JavaScriptRuntime["id"],
environment: Readonly<NodeJS.ProcessEnv>,
platform: NodeJS.Platform,
): string =>
platform === "win32"
? executable
: sameFileExecutableAlias(
executable,
runtime,
environment,
entrypointIdentity,
);
/** Preserve aliases only when the execution form and selected file agree. */
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 requestedFacts = inspectClientRegistrationCommand(requested, undefined);
const current = requestedFacts.entrypoint;
if (current === undefined) return false;
const prior = inspectClientRegistrationCommand(previous, current);
return (
prior.entrypoint !== undefined &&
prior.identity === "owned" &&
prior.launch === requestedFacts.launch
);
};
export interface ClientRegistrationCommandFacts {
readonly identity: ClientRegistrationCommandIdentity;
/** Execution form is separate from ownership of the selected REA file. */
readonly launch: "runtime_script" | "direct" | "package_runner" | "unknown";
/** Selected absolute launcher/script path; excludes package and unresolved PATH commands. */
readonly entrypoint: string | undefined;
readonly package:
| {
readonly reference: string;
readonly exactVersion: string | undefined;
}
| undefined;
}
/** Parse package identity once, retaining exact source spelling and version facts. */
export const inspectClientRegistrationCommand = (
command: readonly string[],
currentCommandPath: string | undefined,
): ClientRegistrationCommandFacts => {
const reference = clientRegistrationPackageReference(command);
if (reference !== undefined) {
const prefix = `${PRODUCT_IDENTITY.packageName}@`;
const version = reference.startsWith(prefix)
? reference.slice(prefix.length)
: undefined;
const normalized = version === undefined ? null : valid(version);
const owned =
reference === PRODUCT_IDENTITY.packageName ||
reference === PRODUCT_IDENTITY.packageSpecifier ||
reference === PRODUCT_IDENTITY.registrationPackageSpecifier ||
normalized !== null;
return {
identity: owned ? "owned" : "foreign",
launch: "package_runner",
entrypoint: undefined,
package: {
reference,
exactVersion: normalized === version ? version : undefined,
},
};
}
return inspectDirectRegistrationCommand(command, currentCommandPath);
};
/** Ownership-only consumers share the same parsed command facts. */
export const classifyClientRegistrationCommand = (
command: readonly string[],
currentCommandPath: string | undefined,
): ClientRegistrationCommandIdentity =>
inspectClientRegistrationCommand(command, currentCommandPath).identity;
const inspectDirectRegistrationCommand = (
command: readonly string[],
currentCommandPath: string | undefined,
): ClientRegistrationCommandFacts => {
const currentPathAvailable =
currentCommandPath !== undefined && currentCommandPath.length > 0;
if (command.length === 3 && command[2] === "mcp") {
const interpreter = command[0] ?? "";
const entrypoint = command[1] ?? "";
if (!isAbsolute(interpreter) || !isAbsolute(entrypoint))
return {
identity: "unknown",
launch: "runtime_script",
entrypoint: undefined,
package: undefined,
};
const interpreterIdentity = entrypointIdentity(
interpreter,
process.execPath,
);
if (interpreterIdentity !== "owned")
return {
identity: currentPathAvailable ? interpreterIdentity : "unknown",
launch: "runtime_script",
entrypoint: undefined,
package: undefined,
};
// The proven current runtime owns script selection: Node normalizes argv,
// while Bun follows native traversal through symlink/.. on POSIX.
const selectedEntrypoint = normalizeEntrypointPath(entrypoint);
return {
launch: "runtime_script",
identity:
selectedEntrypoint === undefined || !currentPathAvailable
? "unknown"
: entrypointIdentity(selectedEntrypoint, currentCommandPath),
entrypoint: selectedEntrypoint,
package: undefined,
};
}
if (command.length === 2 && command[1] === "mcp") {
const executable = command[0] ?? "";
if (executable === PRODUCT_IDENTITY.cliBinary)
return {
identity: "owned",
launch: "direct",
entrypoint: undefined,
package: undefined,
};
if (isAbsolute(executable))
return {
launch: "direct",
identity: currentPathAvailable
? directEntrypointIdentity(executable, currentCommandPath)
: "unknown",
entrypoint: executable,
package: undefined,
};
if (/[./\\]/u.test(executable))
return {
identity: "unknown",
launch: "direct",
entrypoint: undefined,
package: undefined,
};
}
return {
identity: "foreign",
launch: "unknown",
entrypoint: undefined,
package: undefined,
};
};