mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 22:06:10 +00:00
fix(setup): register explicit MCP runtimes and preserve launcher identity
This commit is contained in:
1 parent
b8ce3c74cd
commit
d69a54d592
19 files changed
+637
-100
No files matched your search
@@ -1,8 +1,13 @@
|
||||
import { accessSync, constants, realpathSync } from "node:fs";
|
||||
import { isAbsolute, join } from "node:path";
|
||||
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. */
|
||||
@@ -110,6 +115,14 @@ const entrypointIdentity = (
|
||||
): 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"
|
||||
@@ -125,7 +138,7 @@ const directEntrypointIdentity = (
|
||||
current: string,
|
||||
): ClientRegistrationCommandIdentity => {
|
||||
const nativeIdentity = entrypointIdentity(candidate, current);
|
||||
const scriptPath = normalizeEntrypointPath(candidate);
|
||||
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.
|
||||
@@ -135,19 +148,17 @@ const directEntrypointIdentity = (
|
||||
: "unknown";
|
||||
};
|
||||
|
||||
/** Prefer an executable PATH alias that proves the same selected REA file. */
|
||||
export const registrationEntrypointPath = (
|
||||
entrypoint: string,
|
||||
/** Preserve a manager-owned executable alias only when it selects the same file. */
|
||||
const sameFileExecutableAlias = (
|
||||
selected: string,
|
||||
name: string,
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
platform: NodeJS.Platform,
|
||||
identify: typeof entrypointIdentity,
|
||||
): string => {
|
||||
// A Windows command shim is a different program; file identity cannot prove
|
||||
// its script target. Keep that platform's existing Node/script registration.
|
||||
if (platform === "win32") return entrypoint;
|
||||
for (const directory of (environment.PATH ?? "").split(":")) {
|
||||
if (!isAbsolute(directory)) continue;
|
||||
const candidate = join(directory, PRODUCT_IDENTITY.cliBinary);
|
||||
if (directEntrypointIdentity(candidate, entrypoint) !== "owned") continue;
|
||||
const candidate = absoluteCallerPath(`${directory}/${name}`);
|
||||
if (identify(candidate, selected) !== "owned") continue;
|
||||
try {
|
||||
accessSync(candidate, constants.X_OK);
|
||||
return candidate;
|
||||
@@ -155,10 +166,43 @@ export const registrationEntrypointPath = (
|
||||
// An unexecutable alias cannot launch the selected program.
|
||||
}
|
||||
}
|
||||
return entrypoint;
|
||||
return selected;
|
||||
};
|
||||
|
||||
/** Preserve caller spelling only when both direct launchers prove the same file. */
|
||||
/** 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[],
|
||||
@@ -168,17 +212,21 @@ export const equivalentClientRegistrationCommands = (
|
||||
previous.every((value, index) => value === requested[index])
|
||||
)
|
||||
return true;
|
||||
const current = inspectClientRegistrationCommand(
|
||||
requested,
|
||||
undefined,
|
||||
).entrypoint;
|
||||
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";
|
||||
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:
|
||||
@@ -208,6 +256,7 @@ export const inspectClientRegistrationCommand = (
|
||||
normalized !== null;
|
||||
return {
|
||||
identity: owned ? "owned" : "foreign",
|
||||
launch: "package_runner",
|
||||
entrypoint: undefined,
|
||||
package: {
|
||||
reference,
|
||||
@@ -235,7 +284,12 @@ const inspectDirectRegistrationCommand = (
|
||||
const interpreter = command[0] ?? "";
|
||||
const entrypoint = command[1] ?? "";
|
||||
if (!isAbsolute(interpreter) || !isAbsolute(entrypoint))
|
||||
return { identity: "unknown", entrypoint: undefined, package: undefined };
|
||||
return {
|
||||
identity: "unknown",
|
||||
launch: "runtime_script",
|
||||
entrypoint: undefined,
|
||||
package: undefined,
|
||||
};
|
||||
const interpreterIdentity = entrypointIdentity(
|
||||
interpreter,
|
||||
process.execPath,
|
||||
@@ -243,13 +297,15 @@ const inspectDirectRegistrationCommand = (
|
||||
if (interpreterIdentity !== "owned")
|
||||
return {
|
||||
identity: currentPathAvailable ? interpreterIdentity : "unknown",
|
||||
launch: "runtime_script",
|
||||
entrypoint: undefined,
|
||||
package: undefined,
|
||||
};
|
||||
// Node selects its script from normalized argv; executable paths use native
|
||||
// traversal semantics, where symlink/.. can denote a different file.
|
||||
// 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"
|
||||
@@ -261,9 +317,15 @@ const inspectDirectRegistrationCommand = (
|
||||
if (command.length === 2 && command[1] === "mcp") {
|
||||
const executable = command[0] ?? "";
|
||||
if (executable === PRODUCT_IDENTITY.cliBinary)
|
||||
return { identity: "owned", entrypoint: undefined, package: undefined };
|
||||
return {
|
||||
identity: "owned",
|
||||
launch: "direct",
|
||||
entrypoint: undefined,
|
||||
package: undefined,
|
||||
};
|
||||
if (isAbsolute(executable))
|
||||
return {
|
||||
launch: "direct",
|
||||
identity: currentPathAvailable
|
||||
? directEntrypointIdentity(executable, currentCommandPath)
|
||||
: "unknown",
|
||||
@@ -271,7 +333,17 @@ const inspectDirectRegistrationCommand = (
|
||||
package: undefined,
|
||||
};
|
||||
if (/[./\\]/u.test(executable))
|
||||
return { identity: "unknown", entrypoint: undefined, package: undefined };
|
||||
return {
|
||||
identity: "unknown",
|
||||
launch: "direct",
|
||||
entrypoint: undefined,
|
||||
package: undefined,
|
||||
};
|
||||
}
|
||||
return { identity: "foreign", entrypoint: undefined, package: undefined };
|
||||
return {
|
||||
identity: "foreign",
|
||||
launch: "unknown",
|
||||
entrypoint: undefined,
|
||||
package: undefined,
|
||||
};
|
||||
};
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from "./ClientConfigurationDocument.js";
|
||||
import { lstat, stat } from "node:fs/promises";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
||||
@@ -478,9 +477,9 @@ const registrationAligned = ({
|
||||
return "aligned";
|
||||
// Other owned package references remain stale until pinned to this version.
|
||||
if (commandFacts.package !== undefined) return "stale";
|
||||
// The identity owner already established an exact direct launcher match.
|
||||
// A bare command still depends on the client's PATH, so cannot prove alignment.
|
||||
return isAbsolute(command[0] ?? "") ? "aligned" : "stale";
|
||||
// Owning the script does not establish how its shebang interpreter resolves
|
||||
// in the client. Setup migrates these launchers without losing ownership.
|
||||
return commandFacts.launch === "runtime_script" ? "aligned" : "stale";
|
||||
};
|
||||
|
||||
/** A client that rewrites its file from memory must be quit before setup. */
|
||||
|
||||
@@ -12,6 +12,7 @@ import { PRODUCT_IDENTITY } from "../identity.js";
|
||||
import {
|
||||
invokingPackageRunner,
|
||||
registrationEntrypointPath,
|
||||
registrationRuntimePath,
|
||||
packageRunnerRegistrationCommand,
|
||||
type PackageRunner,
|
||||
} from "./ClientRegistrationIdentity.js";
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
} from "./SetupRegistrationPrewarm.js";
|
||||
import {
|
||||
SUPPORTED_NODE_VERSION_PROSE,
|
||||
javaScriptRuntime,
|
||||
supportsNodeVersion,
|
||||
} from "../domain/runtimeVersion.js";
|
||||
import {
|
||||
@@ -91,12 +93,20 @@ export const setupRegistrationCommand = (
|
||||
if (packageRunner !== undefined)
|
||||
return packageRunnerRegistrationCommand(packageRunner, platform);
|
||||
if (entrypointPath === undefined || entrypointPath.length === 0) return [];
|
||||
return platform === "win32"
|
||||
? [process.execPath, entrypointPath, "mcp"]
|
||||
: [
|
||||
registrationEntrypointPath(entrypointPath, environment, platform),
|
||||
"mcp",
|
||||
];
|
||||
const script = registrationEntrypointPath(
|
||||
entrypointPath,
|
||||
environment,
|
||||
platform,
|
||||
);
|
||||
const runtime = registrationRuntimePath(
|
||||
process.execPath,
|
||||
javaScriptRuntime(process.versions).id,
|
||||
environment,
|
||||
platform,
|
||||
);
|
||||
// The script's env-node shebang depends on the client's PATH, which may
|
||||
// differ from setup's shell. Bind the runtime that is executing setup.
|
||||
return [runtime, script, "mcp"];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+6
-3
@@ -50,7 +50,9 @@ const MCP_DOCTOR_OPTIONS = z.object({
|
||||
const MCP_DOCTOR_ALIAS = { help: "h" } as const;
|
||||
|
||||
const MCP_DOCTOR_COMMAND = "rea mcp doctor";
|
||||
const MCP_DOCTOR_DESCRIPTION = "Validate MCP server startup and tool listing";
|
||||
const MCP_DOCTOR_DESCRIPTION = "Validate MCP adapter startup and tool listing";
|
||||
const MCP_DOCTOR_SCOPE =
|
||||
"Checks this REA adapter using the current runtime; does not verify a client's registered command or launch environment.";
|
||||
|
||||
/** Help flags derived from `MCP_DOCTOR_OPTIONS` plus its alias. */
|
||||
const HELP_FLAGS: ReadonlySet<string> = new Set(["--help", "-h"]);
|
||||
@@ -67,7 +69,7 @@ const renderMcpDoctorHelp = (): string =>
|
||||
hideGlobalOptions: true,
|
||||
options: MCP_DOCTOR_OPTIONS,
|
||||
alias: MCP_DOCTOR_ALIAS,
|
||||
hint: "This entry accepts only the options above.",
|
||||
hint: `${MCP_DOCTOR_SCOPE} This entry accepts only the options above.`,
|
||||
});
|
||||
|
||||
interface McpDoctorOptions {
|
||||
@@ -353,12 +355,13 @@ export const runProductionMcpDoctorCli = async (
|
||||
exitCode: 1,
|
||||
};
|
||||
}
|
||||
const result = await runProductionMcpDoctor({
|
||||
const report = await runProductionMcpDoctor({
|
||||
command: process.execPath,
|
||||
args: [resolve(input.dispatcherPath), "mcp"],
|
||||
cwd: input.packageRoot,
|
||||
environment: process.env,
|
||||
});
|
||||
const result = { ...report, scope: MCP_DOCTOR_SCOPE };
|
||||
const outputValue = parsed.fullOutput
|
||||
? {
|
||||
ok: result.healthy,
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { resolve } from "node:path";
|
||||
import {
|
||||
javaScriptRuntime,
|
||||
type JavaScriptRuntime,
|
||||
} from "../domain/runtimeVersion.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
|
||||
/** Node's absolute argv script selection, or undefined when none was supplied.
|
||||
/** Runtime-specific absolute argv script selection, or undefined when none was supplied.
|
||||
* Native executable paths have different traversal semantics and must not use this.
|
||||
*/
|
||||
export const normalizeEntrypointPath = (
|
||||
entrypoint: string | undefined,
|
||||
runtime: JavaScriptRuntime["id"] = javaScriptRuntime(process.versions).id,
|
||||
): string | undefined =>
|
||||
entrypoint === undefined || entrypoint.length === 0
|
||||
? undefined
|
||||
: resolve(entrypoint);
|
||||
: runtime === "bun"
|
||||
? absoluteCallerPath(entrypoint)
|
||||
: resolve(entrypoint);
|
||||
|
||||
export const CURRENT_ENTRYPOINT_PATH = normalizeEntrypointPath(process.argv[1]);
|
||||
Reference in new issue
Block a user