Merge remote-tracking branch 'origin/main' into feat/recorder-saved-values

This commit is contained in:
Edgar Arakelyan committed 2026-10-25 16:38:44 -07:00
commit 8218b0b246
36 files changed
+976 -214

No files matched your search

+43
View File
@@ -6,6 +6,40 @@ for the reviewed setup transaction and [client IDs](installation.md#supported-ag
The versioned verification notes below describe their stated runs; live service
acceptance is verified only where explicitly recorded.
## Launchers and PATH
A client launched from a desktop app, IDE or service can inherit a different
`PATH` from your terminal. An absolute path to `rea-agents` does not resolve its
interpreter: the npm launcher's `#!/usr/bin/env node` still searches `PATH` for
Node. Running that command successfully in a terminal does not establish that
the client can launch it.
For direct JavaScript installations, setup registers the selected runtime
executable and REA script by absolute path. On POSIX, it prefers executable
`node` or `bun` and REA aliases on setup's `PATH` only when they resolve to the
same selected files. This preserves manager-owned aliases across version updates;
otherwise setup uses the selected paths directly. Package-runner registrations keep
their selected runner. Setup does not copy your ambient environment into client
settings. To refresh an older registration, preview
`rea setup --client <client-id> --skill=false --dry-run`, then run the same command
without `--dry-run` and approve the displayed changes. Setup preserves unrelated
settings, backs up changed configuration and leaves an aligned registration
unchanged. Restart the client after applying the update.
If a runtime manager removes the registered executable and no surviving alias
was available, doctor reports its identity as unknown and setup retains the
registration. REA does not infer that an arbitrary missing executable was Node
from its script argument. Back up the client's configuration, verify the absolute
path and supported version of the runtime you intend to use, then update only
the runtime command, preserving the REA script arguments and other settings.
Rerun setup to review the registration and restart the client.
`rea mcp doctor --json` tests the production MCP adapter using the runtime
executable and environment of the terminal running doctor. It does not launch
the client's configured command or reproduce the client's environment. If doctor
passes but the client disconnects, inspect the client's startup error and registered
launcher; see [verification scope](client-verification.md#launch-environment-verification).
## Runtime settings and client deadlines
`update_analysis_settings` changes curated defaults inside the running REA
@@ -267,6 +301,15 @@ Setup and doctor follow Qwen Code's tilde and working-directory-relative home
overrides. Qwen Code also discovers personal skills in `~/.agents/skills`,
where setup installs the bundled workflow.
If an existing direct-script registration fails with `env: node: No such file or
directory`, refresh it through setup as described under [Launchers and PATH](#launchers-and-path).
For an immediate Qwen-specific workaround, when Node is installed in
`/opt/homebrew/bin`, add `"PATH": "/opt/homebrew/bin:${PATH}"` to that server's
existing `env` object and restart Qwen. Qwen expands `${PATH}` from its parent
environment; other clients can use different substitution syntax. Qwen's stdio
launcher inherits the parent `PATH` and applies server overrides; a reduced
parent environment does not establish that Qwen removed Homebrew from it.
## Antigravity
Antigravity 2.0 discovers personal skills in `~/.gemini/config/skills`.
+18
View File
@@ -11,6 +11,16 @@ and provider lanes. Each record below states its host, client version, model
fixture, and remaining coverage; a native-client run with a loopback model does
not establish live model-service acceptance.
## Launch environment verification
`rea mcp doctor --json` starts the production MCP adapter with the current runtime
executable and environment. A healthy result establishes that adapter's
initialization, discovery and target-free request flow; it does not verify a
client's configured launcher or its inherited environment. Test the generated
registration through the actual client, including a reduced `PATH` when checking
desktop or service startup. Record the client version and launch environment
separately from terminal doctor results. See [launcher troubleshooting](client-configuration.md#launchers-and-path).
## Published Pi code-mode renderer limit
A separate probe of the published `@earendil-works/pi-codemode` 1.1.0 and 1.2.0
@@ -53,6 +63,14 @@ skill setting; Hermes discovers it through the selected `HERMES_HOME`.
Set `REA_VERIFY_HERMES_STICKY_PROFILE=1` to exercise native Hermes selection of
a named sticky profile with `HERMES_HOME` still pointing to its root.
For Qwen on POSIX, set `REA_VERIFY_QWEN_REDUCED_PATH=1` and select its JavaScript
entry point with `REA_VERIFY_QWEN_COMMAND` to test a restricted client environment.
Setup retains the caller's `PATH`; the native Qwen `chat` or `call` run then uses
an isolated `PATH` containing only Bash for the shell-tool fixture. The lane
checks that `node`, `npm` and `npx` cannot be found and uses the generated REA
registration unchanged. Its receipt records `reducedPath`, `clientPath` and
`registrationCommand`; the option alone does not establish a passing native run.
The Qwen lane also supports native Windows (verified with Qwen Code 0.25.1 and
Node 24.19.0 for both `chat` and `call`). With Node 24.18.0, the same `call`
journey reached full Evidence readback but the client aborted during exit with
+41 -10
View File
@@ -2,12 +2,14 @@ import {
lstat,
mkdir,
readFile,
realpath,
readdir,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { dirname, join } from "node:path";
import { dirname, isAbsolute, join } from "node:path";
import { parse as parseToml } from "smol-toml";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { PRODUCT_IDENTITY } from "../dist/identity.js";
@@ -37,6 +39,17 @@ const CODEX_ORIGINAL = [
"",
].join("\n");
const isDirectRegistration = async (command, cli) =>
Array.isArray(command) &&
command.length === 3 &&
typeof command[0] === "string" &&
typeof command[1] === "string" &&
isAbsolute(command[0]) &&
isAbsolute(command[1]) &&
command[2] === "mcp" &&
(await realpath(command[0])) === (await realpath(process.execPath)) &&
(await realpath(command[1])) === (await realpath(cli));
const verifyMcpAdd = async ({ commands, cli, environment, npxLog }) => {
await commands.run(cli, ["mcp", "add"], environment);
const mcpRegistration = await readFile(npxLog, "utf8");
@@ -122,7 +135,7 @@ const verifyAbsentOpenCodeSetup = async ({
!result.appliedActions.includes("configured_opencode") ||
config.mcp?.rea?.type !== "local" ||
config.mcp.rea.enabled !== true ||
JSON.stringify(config.mcp.rea.command) !== JSON.stringify([cli, "mcp"])
!(await isDirectRegistration(config.mcp.rea.command, cli))
)
throw new Error(
`packaged setup did not configure explicitly selected OpenCode without a config file: ${JSON.stringify(result)}`,
@@ -172,8 +185,13 @@ const verifyAbsentClaudeCodeSetup = async ({
firstExecution.status !== 0 ||
first.status !== "ready" ||
!first.appliedActions.includes("configured_claude_code") ||
config.mcpServers?.rea?.command !== cli ||
JSON.stringify(config.mcpServers?.rea?.args) !== JSON.stringify(["mcp"]) ||
!(await isDirectRegistration(
[
config.mcpServers?.rea?.command,
...(config.mcpServers?.rea?.args ?? []),
],
cli,
)) ||
(await pathExists(markerPath)) ||
registration?.state !== "aligned" ||
registration.config_path !== configPath
@@ -270,8 +288,13 @@ export const assertClientConfig = async (cli, configPath, original) => {
const config = JSON.parse(await readFile(configPath, "utf8"));
if (
config.existing !== true ||
config.mcpServers?.rea?.command !== cli ||
JSON.stringify(config.mcpServers?.rea?.args) !== JSON.stringify(["mcp"])
!(await isDirectRegistration(
[
config.mcpServers?.rea?.command,
...(config.mcpServers?.rea?.args ?? []),
],
cli,
))
)
throw new Error("packaged client readback failed");
if (!(await readFile(`${configPath}.rea.backup`)).equals(original))
@@ -296,7 +319,7 @@ const assertOpenCodeConfig = async ({
(expectedRegistration &&
(mcp?.rea?.type !== "local" ||
mcp.rea.enabled !== true ||
JSON.stringify(mcp.rea.command) !== JSON.stringify([cli, "mcp"]))) ||
!(await isDirectRegistration(mcp.rea.command, cli)))) ||
(!expectedRegistration && mcp?.rea !== undefined)
)
throw new Error("packaged OpenCode JSONC registration readback failed");
@@ -309,11 +332,15 @@ const assertCodexSymlink = async ({
codexTarget,
}) => {
const codex = await readFile(codexTarget, "utf8");
const registration = parseToml(codex).mcp_servers?.rea;
if (
!(await lstat(codexConfig)).isSymbolicLink() ||
!(await lstat(cursorConfig)).isSymbolicLink() ||
!codex.includes("[mcp_servers.rea]") ||
!codex.includes(`command = "${cli}"`) ||
!(await isDirectRegistration(
[registration?.command, ...(registration?.args ?? [])],
cli,
)) ||
!codex.includes("startup_timeout_sec = 30") ||
!codex.startsWith(CODEX_ORIGINAL) ||
(await readFile(`${codexConfig}.rea.backup`, "utf8")) !== CODEX_ORIGINAL
@@ -446,11 +473,15 @@ const verifySetupFailureRecovery = async ({
environment,
);
const recovered = JSON.parse(recoveredExecution.stdout);
const registration = JSON.parse(await readFile(cursorConfig, "utf8"))
.mcpServers?.rea;
if (
recovered.status !== status ||
recoveredExecution.status !== (status === "ready" ? 0 : 1) ||
JSON.parse(await readFile(cursorConfig, "utf8")).mcpServers?.rea
?.command !== cli
!(await isDirectRegistration(
[registration?.command, ...(registration?.args ?? [])],
cli,
))
)
throw new Error("packaged setup did not recover");
};
+72 -14
View File
@@ -6,6 +6,7 @@ import {
readFile,
mkdtemp,
realpath,
symlink,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve, sep } from "node:path";
@@ -34,6 +35,11 @@ const command = process.env.REA_VERIFY_QWEN_COMMAND ?? "qwen";
// npm exposes only qwen.cmd on Windows. Resolve a selected JavaScript entry
// before the verifier switches to its disposable workspace.
const entryPoint = /\.(c|m)?js$/iu.test(command) ? resolve(command) : undefined;
const reducedPath = process.env.REA_VERIFY_QWEN_REDUCED_PATH === "1";
assert(
!reducedPath || (process.platform !== "win32" && entryPoint !== undefined),
"REA_VERIFY_QWEN_REDUCED_PATH=1 requires POSIX and a JavaScript REA_VERIFY_QWEN_COMMAND entry point",
);
const launch = (args) =>
entryPoint === undefined
? [command, args]
@@ -51,12 +57,15 @@ let evidenceSeen = false;
let skillSeen = false;
let finalSeen = false;
let skillRequested = false;
let searchRequested = false;
let searchCallId;
let analysisCallId;
let artifactReadByAgent = false;
let artifactPath;
let selectedSkillLoaded = false;
let artifactDigest;
let catalogSize = null;
let registrationCommand = null;
let clientPath = null;
const commands = await createVerifierCommands();
await withVerifierCleanup(async () => {
await withVerifierCleanup(async () => {
@@ -139,12 +148,47 @@ await withVerifierCleanup(async () => {
const configured = JSON.parse(
await readFile(join(profile, "settings.json"), "utf8"),
);
registrationCommand = [
configured.mcpServers.rea.command,
...configured.mcpServers.rea.args,
];
assert.deepEqual(configured.model, { name: "rea-client-fixture" });
assert.equal(configured.skills, undefined);
assert.deepEqual(configured.mcpServers.other, {
command: "unrelated-server",
disabled: true,
});
clientPath = baseEnvironment.PATH;
if (reducedPath) {
assert.equal(
configured.mcpServers.rea.env?.PATH,
undefined,
"Setup must not persist its ambient PATH into the registration",
);
clientPath = join(lab, "client-bin");
await mkdir(clientPath);
// Qwen's Evidence readback uses its shell tool, which launches bare bash.
// Keep that fixture dependency available without exposing node/npm/npx.
const bash = await commands.exec("bash", ["-c", "command -v bash"], {
env: baseEnvironment,
timeout: 5_000,
});
await symlink(
await realpath(bash.stdout.trim()),
join(clientPath, "bash"),
);
await commands.exec(
process.execPath,
[
"-e",
"const {spawnSync}=require('node:child_process');const assert=require('node:assert/strict');for(const command of ['node','npm','npx'])assert.equal(spawnSync(command,['--version'],{timeout:1000}).error?.code,'ENOENT',`${command} unexpectedly available on reduced PATH`);",
],
{
env: { ...baseEnvironment, PATH: clientPath },
timeout: 5_000,
},
);
}
const target = join(lab, "fixture α测试");
await mkdir(target, { recursive: true });
await writeFile(
@@ -184,15 +228,21 @@ await withVerifierCleanup(async () => {
skillSeen ||= JSON.stringify(body.messages ?? []).includes(
"reverse-engineer-anything",
);
const messageText = (body.messages ?? [])
.map((message) =>
const messages = (body.messages ?? []).map((message) => ({
role: message.role,
callId: message.tool_call_id,
text:
typeof message.content === "string"
? message.content
: (message.content ?? [])
.map((part) => part.text ?? "")
.join("\n"),
)
.join("\n");
}));
const messageText = messages.map((message) => message.text).join("\n");
const toolResponseText = (callId) =>
messages.find(
(message) => message.role === "tool" && message.callId === callId,
)?.text ?? "";
selectedSkillLoaded ||= messageText.includes(
join(sharedSkills, "reverse-engineer-anything"),
);
@@ -262,9 +312,7 @@ await withVerifierCleanup(async () => {
t.name.includes("analyze_javascript_application"),
);
const reviewedNames = [
...JSON.stringify(
(body.messages ?? []).filter((m) => m.role === "tool"),
).matchAll(
...toolResponseText(searchCallId).matchAll(
/(?:\\?"name\\?"\s*:\s*\\?")([^"\\]*analyze_javascript_application)(?:\\?")/g,
),
]
@@ -274,7 +322,7 @@ await withVerifierCleanup(async () => {
const invoke = tools.find((t) => t.name === "tool_call");
const skill = tools.find((t) => t.name === "skill");
const shell = tools.find((t) => t.name === "run_shell_command");
const persisted = messageText.match(
const persisted = toolResponseText(analysisCallId).match(
/Full output saved to: ([^\r\n]+)/u,
);
const call = (name, args) => ({
@@ -308,26 +356,32 @@ await withVerifierCleanup(async () => {
const script = `const fs=require('node:fs');const e=JSON.parse(fs.readFileSync(${JSON.stringify(artifactPath)},'utf8'));console.log(JSON.stringify({REA_CLIENT_FIXTURE:JSON.stringify(e.normalized_result).includes('REA_CLIENT_FIXTURE'),evidence_id:e.evidence_id,graph_nodes:e.normalized_result.graph.nodes.length}));`;
// Keep paths and JSON quotes out of shell syntax on Windows and POSIX.
const encodedScript = Buffer.from(script).toString("base64");
const nodeCommand =
process.platform === "win32"
? "node"
: `'${process.execPath.replaceAll("'", "'\"'\"'")}'`;
message = call(shell.name, {
command: `node -e "eval(Buffer.from('${encodedScript}','base64').toString('utf8'))"`,
command: `${nodeCommand} -e "eval(Buffer.from('${encodedScript}','base64').toString('utf8'))"`,
description:
"Read the complete saved REA Evidence and verify the fixture facts.",
});
} else if (selected)
} else if (selected) {
message = call(selected.name, {
input_path: target,
format: "directory",
});
else if (reviewedNames.length && invoke)
analysisCallId = message.tool_calls[0].id;
} else if (reviewedNames.length && invoke) {
message = call(invoke.name, {
name: reviewedNames[0],
arguments: { input_path: target, format: "directory" },
});
else if (search && !searchRequested) {
searchRequested = true;
analysisCallId = message.tool_calls[0].id;
} else if (search && searchCallId === undefined) {
message = call(search.name, {
query: "analyze_javascript_application",
});
searchCallId = message.tool_calls[0].id;
} else
throw new Error(
`REA analysis tool not exposed; tools: ${tools.map((t) => t.name).join(", ")}`,
@@ -342,6 +396,7 @@ await withVerifierCleanup(async () => {
: `Use REA to analyze the JavaScript directory ${target}. Report the result.`;
const env = {
...baseEnvironment,
PATH: clientPath,
OPENAI_API_KEY: "local-fixture-key",
OPENAI_BASE_URL: endpoint,
};
@@ -413,6 +468,9 @@ await withVerifierCleanup(async () => {
client: "Qwen Code",
clientVersion,
mode,
reducedPath,
...(reducedPath ? { clientPath } : {}),
registrationCommand,
passed,
artifacts: lab,
runtimeRoot,
+6 -3
View File
@@ -1,5 +1,6 @@
import { lstat, realpath } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
import { basename, dirname, join } from "node:path";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
interface ClientConfigPathStats {
readonly uid?: number;
@@ -70,9 +71,9 @@ const resolveOwnedConfigSymlink = async (
const canonicalRegularConfigPath = async (
requestedPath: string,
fileSystem: ClientConfigPathFileSystem,
): Promise<string> => {
): Promise<string | undefined> => {
if (fileSystem.realpath === undefined) return requestedPath;
let ancestor = resolve(requestedPath);
let ancestor = absoluteCallerPath(requestedPath);
const missing: string[] = [];
while (true) {
try {
@@ -80,6 +81,8 @@ const canonicalRegularConfigPath = async (
} catch (cause: unknown) {
if (!isAbsentPath(cause) || dirname(ancestor) === ancestor)
return requestedPath;
// An absent directory before '..' cannot establish the selected parent.
if (basename(ancestor) === "..") return undefined;
missing.unshift(basename(ancestor));
ancestor = dirname(ancestor);
}
@@ -4,6 +4,7 @@ import { basename, dirname, join, resolve, sep } from "node:path";
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
import { fileHandleMatchesBytes } from "../filesystem/fileHandleMatchesBytes.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { OwnedStagedOutput } from "../filesystem/OwnedStagedOutput.js";
import {
@@ -290,10 +291,10 @@ const isParentAliasRetarget = async (
);
}
if (requestedStats.isSymbolicLink()) return false;
const requestedParent = dirname(resolve(requestedPath));
const requestedParent = dirname(absoluteCallerPath(requestedPath));
try {
const canonicalParent = await realpath(requestedParent);
return resolve(canonicalParent) !== requestedParent;
return canonicalParent !== requestedParent;
} catch (cause: unknown) {
void cause;
return false;
+96 -24
View File
@@ -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,
};
};
+3 -4
View File
@@ -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. */
+44 -16
View File
@@ -495,6 +495,37 @@ describe("doctor runtime check", () => {
);
});
it.each([undefined, "", "not-a-version", "14garbage"])(
"retains unknown host support for macOS probe value %s",
async (version) => {
const result = await runDoctor(
undefined,
host({ macosVersion: () => Promise.resolve(version) }),
);
expect(result.checks).toContainEqual(
expect.objectContaining({
name: "host",
ok: false,
classification: "unknown",
}),
);
},
);
it.skipIf(process.platform !== "darwin")(
"reads the macOS version without PATH utilities",
async () => {
const system = systemDoctorHost({
environment: { PATH: "/nonexistent" },
});
try {
expect(await system.macosVersion()).toMatch(/^\d+(?:\.\d+)*$/u);
} finally {
await system.close?.();
}
},
);
describe("doctor", () => {
it("returns exact recovery for every failed diagnostic", async () => {
const result = await runDoctor(
@@ -518,22 +549,19 @@ describe("doctor", () => {
});
});
it.each(["11.7", undefined])(
"rejects unsupported macOS version %s",
async (version) => {
const result = await runDoctor(
undefined,
host({ macosVersion: () => Promise.resolve(version) }),
);
expect(result.checks).toContainEqual(
expect.objectContaining({
name: "host",
ok: false,
classification: "unsupported_host",
}),
);
},
);
it.each(["11.7"])("rejects unsupported macOS version %s", async (version) => {
const result = await runDoctor(
undefined,
host({ macosVersion: () => Promise.resolve(version) }),
);
expect(result.checks).toContainEqual(
expect.objectContaining({
name: "host",
ok: false,
classification: "unsupported_host",
}),
);
});
it("admits only the experimental Windows x64 host boundary", async () => {
const windows = await runDoctor(
+3 -1
View File
@@ -764,7 +764,9 @@ const readMacosVersion = async (
options: { readonly env: NodeJS.ProcessEnv },
): Promise<string | undefined> => {
try {
return (await run("sw_vers", ["-productVersion"], options)).stdout.trim();
return (
await run("/usr/bin/sw_vers", ["-productVersion"], options)
).stdout.trim();
} catch (cause: unknown) {
// best-effort cleanup: optional OS version probing; failure means unknown.
void cause;
+18 -4
View File
@@ -129,8 +129,25 @@ const hostCheck = async (host: DoctorHost): Promise<DoctorCheck> => {
host.platform === "darwin" ? await host.macosVersion() : undefined;
const linuxDistribution =
host.platform === "linux" ? await host.linuxDistribution() : undefined;
const macosMajor =
macosVersion !== undefined && /^\d+(?:\.\d+)*$/u.test(macosVersion)
? Number(macosVersion.split(".")[0])
: undefined;
if (host.platform === "darwin" && macosMajor === undefined)
return {
name: "host",
ok: false,
classification: "unknown",
detail:
macosVersion === undefined
? "The macOS version could not be determined."
: `The macOS version probe returned an unrecognized value: ${JSON.stringify(macosVersion)}.`,
remediation: "Check /usr/bin/sw_vers -productVersion and rerun doctor.",
};
const supported =
(host.platform === "darwin" && parseMajor(macosVersion) >= 12) ||
(host.platform === "darwin" &&
macosMajor !== undefined &&
macosMajor >= 12) ||
(host.platform === "linux" && supportsReaHost(linuxDistribution)) ||
(host.platform === "win32" && host.architecture === "x64");
const detail =
@@ -359,8 +376,5 @@ const unsupportedHopperRemediation = (configured: boolean): string =>
? "Check HOPPER_LAUNCHER_PATH and its publisher verification evidence. Run rea setup to install and retain an authenticated official Hopper package for offline use."
: "Run rea setup to install and retain an authenticated official Hopper package for offline use.";
const parseMajor = (version: string | undefined): number =>
Number.parseInt(version?.split(".")[0] ?? "0", 10);
const providerCheckName = (providerId: string, name: string): boolean =>
name === providerId || name.startsWith(`${providerId}-`);
+16 -6
View File
@@ -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"];
};
/**
@@ -2,6 +2,7 @@ import type { DoctorProviderInspection } from "./Doctor.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
const SELECTED_REGISTRATION_SETTINGS = [
"REA_ILSPY_CMD_PATH",
"REA_ANALYSIS_PROVIDER",
"REA_ASAR_MAX_HEADER_BYTES",
"REA_BRIDGE_MAX_REQUEST_BYTES",
@@ -147,6 +147,48 @@ describe("materializeRegistrationRunnerCache", () => {
});
describe("launchRegistrationRunnerCommand", () => {
it("retains bounded head and tail bytes and preserves UTF-8 across writes", async () => {
const result = await launchRegistrationRunnerCommand(
process.execPath,
[
"-e",
`
process.stdout.write(Buffer.from([0xe4]));
setTimeout(() => {
process.stdout.write(Buffer.from([0xb8, 0xad]));
process.stderr.write('x'.repeat(60000));
setTimeout(() => {
process.stderr.write('y'.repeat(60000));
process.stderr.write('FINAL: package permission denied');
process.exitCode = 1;
}, 30);
}, 30);
`,
],
{
env: process.env,
cwd: process.cwd(),
timeoutMs: 5000,
platform: process.platform,
},
);
expect(result.exitCode).toBe(1);
expect(result.stdout).toBe("中");
expect(Buffer.byteLength(result.stderr)).toBeLessThan(64 * 1024 + 50);
expect(result.stderr).toContain("[output omitted]");
expect(result.stderr).toContain("FINAL: package permission denied");
const prewarm = await materializeRegistrationRunnerCache(
["npx", "-y", specifier, "mcp"],
{},
async () => result,
);
expect(prewarm.status).toBe("failed");
if (prewarm.status === "failed") {
expect(prewarm.reason).toContain("exit 1");
expect(prewarm.reason).toContain("FINAL: package permission denied");
}
});
it("captures a successful command exit", async () => {
const result = await launchRegistrationRunnerCommand(
process.execPath,
+18 -22
View File
@@ -1,5 +1,6 @@
import { spawn, type ChildProcess } from "node:child_process";
import { tmpdir } from "node:os";
import { BoundedOutputExcerpt } from "../process/BoundedOutputExcerpt.js";
import { cleanupWindowsProcessTree } from "../process/ProcessOwnership.js";
import { inspectClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
@@ -123,8 +124,10 @@ export const launchRegistrationRunnerCommand: RegistrationRunnerLauncher = (
// group no longer receives the terminal's signal.
...(options.platform === "win32" ? {} : { detached: true }),
});
const stdout = boundedStream(child.stdout);
const stderr = boundedStream(child.stderr);
const stdout = new BoundedOutputExcerpt(PREWARM_CAPTURE_LIMIT);
const stderr = new BoundedOutputExcerpt(PREWARM_CAPTURE_LIMIT);
child.stdout?.on("data", (chunk: Buffer) => stdout.append(chunk));
child.stderr?.on("data", (chunk: Buffer) => stderr.append(chunk));
let timedOut = false;
let spawnError: string | undefined;
let interruptedSignal: string | undefined;
@@ -164,8 +167,8 @@ export const launchRegistrationRunnerCommand: RegistrationRunnerLauncher = (
resolve({
exitCode: code ?? signal,
signal: signal ?? null,
stdout: stdout(),
stderr: stderr(),
stdout: stdout.text,
stderr: stderr.text,
timedOut,
...(spawnError === undefined ? {} : { spawnError }),
...(interruptedSignal === undefined ? {} : { interruptedSignal }),
@@ -176,32 +179,25 @@ export const launchRegistrationRunnerCommand: RegistrationRunnerLauncher = (
/** Terminal interrupts that must also stop a detached package-runner tree. */
const FORWARD_SIGNALS: readonly NodeJS.Signals[] = ["SIGINT", "SIGTERM"];
const boundedStream = (
stream: NodeJS.ReadableStream | null,
): (() => string) => {
let captured = "";
stream?.on("data", (chunk: Buffer) => {
if (captured.length < PREWARM_CAPTURE_LIMIT)
captured += chunk.toString("utf8").slice(0, PREWARM_CAPTURE_LIMIT);
});
return () => captured;
};
const prewarmFailureReason = (launch: RegistrationRunnerLaunch): string => {
const observed =
const outcome =
launch.spawnError !== undefined
? launch.spawnError
? "spawn failed"
: launch.interruptedSignal !== undefined
? `the package runner was stopped by ${launch.interruptedSignal}`
: launch.timedOut
? `the package runner timed out after the bounded prewarm window`
: `exit ${launch.exitCode === null ? "unknown" : String(launch.exitCode)}${
launch.stderr.trim() === "" ? "" : `: ${launch.stderr.trim()}`
}`;
const normalized = observed
: `exit ${launch.exitCode === null ? "unknown" : String(launch.exitCode)}`;
const normalized = (launch.spawnError ?? launch.stderr)
.replace(/\u001b(?:\[[0-9;?]*[ -/]*[@-~]|\([0-9A-Za-z]|[@-Z\\^_])/gu, "")
.replace(/[\u0000-\u001f\u007f]+/gu, " ")
.replace(/\s+/gu, " ")
.trim();
return `The pinned package-runner entry could not be materialized before the first client start (${normalized.slice(0, PREWARM_REASON_LIMIT)}); the first client start will install it inside its startup deadline.`;
const detailLimit = PREWARM_REASON_LIMIT - outcome.length - 2;
const detail =
normalized.length > detailLimit
? `… ${normalized.slice(-(detailLimit - 2))}`
: normalized;
const observed = `${outcome}${detail === "" ? "" : `: ${detail}`}`;
return `The pinned package-runner entry could not be materialized before the first client start (${observed}); the first client start will install it inside its startup deadline.`;
};
+11 -7
View File
@@ -1714,7 +1714,7 @@ it("explains the macOS keg-only JDK case without probing an unselected installat
},
}),
);
expect(probed).toEqual(["java"]);
expect(probed).toEqual(["/usr/bin/java"]);
expect(inspection.checks.find(({ name }) => name === "java")).toMatchObject({
status: "failed",
code: "runtime_missing",
@@ -1813,8 +1813,8 @@ it.each([
scenario === "explicit home"
? ["/selected-jdk/bin/java"]
: scenario === "PATH succeeds"
? ["java"]
: ["java", keg],
? ["/usr/bin/java"]
: ["/usr/bin/java", keg],
);
expect(inspection.status).toBe(
scenario === "PATH succeeds" || scenario === "keg succeeds"
@@ -1825,19 +1825,21 @@ it.each([
expect(ghidraInstallationDiagnostics(inspection)).toMatchObject({
java_discovery_failures: expect.arrayContaining([
{
command: "java",
command: "/usr/bin/java",
code: "runtime_missing",
detail: expect.any(String),
},
]),
});
}
if (scenario === "both fail") expect(inspection.javaCommand).toBe("java");
if (scenario === "both fail")
expect(inspection.javaCommand).toBe("/usr/bin/java");
},
);
it.each([
["darwin", undefined, "/selected-bin/java", "java"],
["darwin", undefined, "/selected-bin/java", "/selected-bin/java"],
["darwin", undefined, "/linked/../jdk/java", "/linked/../jdk/java"],
["darwin", "/selected-jdk", undefined, "/selected-jdk/bin/java"],
["linux", undefined, undefined, "java"],
["win32", undefined, undefined, "java.exe"],
@@ -1849,7 +1851,9 @@ it.each([
const inspection = await inspectGhidraInstallation(
{
resourceOwner: inspectionResources.owner,
environment: { PATH: "/selected-bin" },
environment: {
PATH: `:relative-bin:${pathJava === undefined ? "/selected-bin" : pathJava.slice(0, -"/java".length)}`,
},
installDir: INSTALL,
platform,
architecture: "arm64",
+29 -15
View File
@@ -11,6 +11,7 @@ import { ok, type Result } from "../domain/result.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { readGhidraExecutableHeader } from "./GhidraExecutableHeader.js";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import {
readGhidraInstallationProperties,
type GhidraInstallationFileOptions,
@@ -524,24 +525,37 @@ const installationCoordinates = async (
),
)
.filter((candidate) => host.executable(candidate));
let javaCommand = (() => {
const selectedJava: {
command: string;
source: "configured" | "path" | "homebrew" | "default";
} = (() => {
if (options.javaHome !== undefined)
return path.join(
options.javaHome,
"bin",
platform === "win32" ? "java.exe" : "java",
);
return {
source: "configured",
command: path.join(
options.javaHome,
"bin",
platform === "win32" ? "java.exe" : "java",
),
};
if (platform !== "darwin")
return platform === "win32" ? "java.exe" : "java";
return {
source: "default",
command: platform === "win32" ? "java.exe" : "java",
};
// Check the caller's PATH, never a file named java in the working directory.
const javaOnPath = options.environment.PATH?.split(":").some(
(directory) =>
path.isAbsolute(directory) &&
host.executable(path.join(directory, "java")),
);
if (javaOnPath) return "java";
return homebrewJavaCandidates()[0] ?? "java";
const javaOnPath = options.environment.PATH?.split(":")
.filter((directory) => path.isAbsolute(directory))
.map((directory) => absoluteCallerPath(`${directory}/java`, { platform }))
.find((candidate) => host.executable(candidate));
if (javaOnPath !== undefined)
return { source: "path", command: javaOnPath };
const homebrew = homebrewJavaCandidates()[0];
return homebrew === undefined
? { source: "default", command: "/usr/bin/java" }
: { source: "homebrew", command: homebrew };
})();
let javaCommand = selectedJava.command;
// Java is a prerequisite of a selected installation. Without one, or with a
// relative JAVA_HOME, configuration validation refuses Ghidra before Java can
// matter, so callers such as setup and doctor must not execute a host JVM.
@@ -574,7 +588,7 @@ const installationCoordinates = async (
if (
platform === "darwin" &&
options.javaHome === undefined &&
javaCommand === "java" &&
(selectedJava.source === "path" || selectedJava.source === "default") &&
javaProbe !== undefined &&
!javaProbe.ok &&
!(javaProbe.error instanceof AnalysisError) &&
+6 -3
View File
@@ -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,
+46
View File
@@ -0,0 +1,46 @@
/** Byte-bounded head and tail diagnostics, decoded after stream chunks are joined. */
export class BoundedOutputExcerpt {
#head: Buffer = Buffer.alloc(0);
#tail: Buffer = Buffer.alloc(0);
#total = 0;
readonly #headLimit: number;
readonly #tailLimit: number;
constructor(readonly limit: number) {
this.#headLimit = Math.ceil(limit / 2);
this.#tailLimit = limit - this.#headLimit;
}
append(chunk: Buffer): void {
this.#total += chunk.byteLength;
const headCount = Math.min(
chunk.byteLength,
this.#headLimit - this.#head.byteLength,
);
if (headCount > 0)
this.#head = Buffer.concat([this.#head, chunk.subarray(0, headCount)]);
const rest = chunk.subarray(headCount);
if (rest.byteLength > 0 && this.#tailLimit > 0) {
const retained = rest.subarray(-this.#tailLimit);
this.#tail = Buffer.concat([
this.#tail.subarray(
Math.max(
0,
this.#tail.byteLength + retained.byteLength - this.#tailLimit,
),
),
retained,
]);
}
}
get truncated(): boolean {
return this.#total > this.limit;
}
get text(): string {
return this.truncated
? `${this.#head.toString("utf8")} … [output omitted] … ${this.#tail.toString("utf8")}`
: Buffer.concat([this.#head, this.#tail]).toString("utf8");
}
}
+10 -2
View File
@@ -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]);
+8 -40
View File
@@ -1,4 +1,5 @@
import { spawn } from "node:child_process";
import { BoundedOutputExcerpt } from "./BoundedOutputExcerpt.js";
export interface BoundedCommandOutput {
readonly exitCode: string | number | null;
@@ -21,31 +22,10 @@ export const execFileBoundedOutput = (
env: options.env,
stdio: ["ignore", "pipe", "pipe"],
});
const headLimit = Math.ceil(options.limit / 2);
const tailLimit = options.limit - headLimit;
const stdout = { head: Buffer.alloc(0), tail: Buffer.alloc(0), total: 0 };
const stderr = { head: Buffer.alloc(0), tail: Buffer.alloc(0), total: 0 };
let outputTruncated = false;
const capture =
(stream: typeof stdout) =>
(chunk: Buffer): void => {
stream.total += chunk.length;
const headCount = Math.min(
chunk.length,
Math.max(0, headLimit - stream.head.length),
);
if (headCount > 0)
stream.head = Buffer.concat([
stream.head,
chunk.subarray(0, headCount),
]);
const rest = chunk.subarray(headCount);
if (rest.length > 0 && tailLimit > 0)
stream.tail = Buffer.concat([stream.tail, rest]).subarray(-tailLimit);
if (stream.total > options.limit) outputTruncated = true;
};
child.stdout.on("data", capture(stdout));
child.stderr.on("data", capture(stderr));
const stdout = new BoundedOutputExcerpt(options.limit);
const stderr = new BoundedOutputExcerpt(options.limit);
child.stdout.on("data", (chunk: Buffer) => stdout.append(chunk));
child.stderr.on("data", (chunk: Buffer) => stderr.append(chunk));
let spawnError: NodeJS.ErrnoException | undefined;
child.on("error", (error: NodeJS.ErrnoException) => {
spawnError = error;
@@ -53,21 +33,9 @@ export const execFileBoundedOutput = (
child.on("close", (code, signal) => {
resolve({
exitCode: spawnError?.code ?? code ?? signal,
stdout: retainedOutput(stdout, options.limit),
stderr: retainedOutput(stderr, options.limit),
outputTruncated,
stdout: stdout.text,
stderr: stderr.text,
outputTruncated: stdout.truncated || stderr.truncated,
});
});
});
const retainedOutput = (
stream: {
readonly head: Buffer;
readonly tail: Buffer;
readonly total: number;
},
limit: number,
): string =>
stream.total > limit
? `${stream.head.toString("utf8")} … [output omitted] … ${stream.tail.toString("utf8")}`
: Buffer.concat([stream.head, stream.tail]).toString("utf8");
+23
View File
@@ -23,6 +23,29 @@ import { snapshotEnvironment } from "./process/snapshotEnvironment.js";
afterEach(() => vi.unstubAllEnvs());
it.each([undefined, "/caller/tools with spaces/ilspycmd"])(
"preserves the explicit ILSpy path %s without inventing an unset value",
async (selected) => {
const host = systemSetupHost(
createDoctorHostFixture({
...(selected === undefined ? {} : { configuredIlspyCmdPath: selected }),
ilspyCmdVersion: async () => "ilspycmd 9.1.0",
}),
selected === undefined ? {} : { REA_ILSPY_CMD_PATH: selected },
);
try {
const state = await host.initialSetupState();
expect(state.providerEnvironment.REA_ILSPY_CMD_PATH).toBe(selected);
if (selected !== undefined)
expect(state.doctor.checks).toContainEqual(
expect.objectContaining({ name: "ilspycmd", ok: true }),
);
} finally {
await host.close?.();
}
},
);
it("plans setup and uninstall against selected home and client overrides", async () => {
const root = await mkdtemp(join(tmpdir(), "rea-selected-setup-"));
const environment = {
@@ -275,10 +275,7 @@ cliTest(
peer: { command: ["/usr/bin/true"] },
rea: {
type: "local",
command:
process.platform === "win32"
? [process.execPath, launcher, "mcp"]
: [launcher, "mcp"],
command: [process.execPath, launcher, "mcp"],
},
});
},
@@ -1,10 +1,142 @@
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { expect } from "vitest";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest.skipIf(process.platform === "win32")(
"migrates an owned shebang registration and starts its generated MCP command without a runtime on PATH",
async ({ cli }) => {
const home = await createTestTempDirectory("rea-registration-interpreter-");
const profile = join(home, ".qwen");
const config = join(profile, "settings.json");
const entrypoint = resolve("scripts/rea.mjs");
const restrictedPath = join(home, "path-without-runtime");
await mkdir(restrictedPath);
// Linux process ownership requires procps; no JavaScript runtime is exposed.
if (process.platform === "linux")
await symlink("/bin/ps", join(restrictedPath, "ps"));
const retainedEnvironment = {
PATH: restrictedPath,
CALLER_SETTING: "keep exactly",
};
const unrelated = { command: "other-server", args: ["keep"] };
const original = JSON.stringify({
mcpServers: {
rea: {
command: entrypoint,
args: ["mcp"],
env: retainedEnvironment,
},
unrelated,
},
});
await mkdir(profile);
await writeFile(config, original);
const environment = {
HOME: home,
USERPROFILE: home,
QWEN_HOME: profile,
PATH: restrictedPath,
HOPPER_LAUNCHER_PATH: join(home, "missing-hopper"),
GHIDRA_INSTALL_DIR: join(home, "missing-ghidra"),
JAVA_HOME: join(home, "missing-java"),
};
const run = (arguments_: readonly string[]) =>
cli.run({
arguments: [...arguments_, "--json"],
cwd: home,
environment,
});
const doctor = await run([
"doctor",
"--client",
"qwen_code",
"--skill=false",
]);
expect(doctor.exitCode, doctor.stdout).toBe(1);
expect(doctor.json).toMatchObject({
identity: {
registrations: expect.arrayContaining([
expect.objectContaining({ client: "qwen_code", state: "stale" }),
]),
},
});
expect(await readFile(config, "utf8")).toBe(original);
const setupArguments = [
"setup",
"--client",
"qwen_code",
"--skill=false",
"--yes",
];
const setup = await run(setupArguments);
expect(setup.exitCode, setup.stdout).toBe(0);
const configured = await readFile(config, "utf8");
const document = z
.object({
mcpServers: z.object({
rea: z.object({
command: z.string(),
args: z.array(z.string()),
env: z.record(z.string(), z.string()),
}),
unrelated: z.unknown(),
}),
})
.parse(JSON.parse(configured));
const registration = document.mcpServers.rea;
expect(registration).toMatchObject({
command: process.execPath,
args: [entrypoint, "mcp"],
env: retainedEnvironment,
});
expect(document.mcpServers.unrelated).toEqual(unrelated);
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
const transport = new StdioClientTransport({
command: registration.command,
args: registration.args,
cwd: home,
env: { ...environment, ...registration.env },
stderr: "pipe",
});
const client = new Client({ name: "setup-interpreter", version: "1" });
try {
await client.connect(transport);
expect(
(await client.listTools()).tools.map(({ name }) => name),
).toContain("binary_session");
const session = await client.callTool({
name: "binary_session",
arguments: {},
});
expect(session.isError).not.toBe(true);
expect(session.structuredContent).toMatchObject({
result: { open: false },
});
} finally {
await client.close();
await transport.close();
}
const repeated = await run(setupArguments);
expect(repeated.exitCode, repeated.stdout).toBe(0);
expect(repeated.json).toMatchObject({
plannedActions: [],
appliedActions: [],
});
expect(await readFile(config, "utf8")).toBe(configured);
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
},
);
cliTest.skipIf(process.platform === "win32").for([
["rea", "rea-agents"],
["rea-agents", "rea"],
+2 -2
View File
@@ -282,8 +282,8 @@ cliTest(
other: { command: "other-server" },
rea: {
type: "stdio",
command: process.platform === "win32" ? process.execPath : cliPath,
args: process.platform === "win32" ? [cliPath, "mcp"] : ["mcp"],
command: process.execPath,
args: [cliPath, "mcp"],
},
},
autoEnableCodemode: false,
@@ -54,14 +54,8 @@ cliTest(
mcpServers: {
other: { command: "other-server" },
rea: {
command:
process.platform === "win32"
? process.execPath
: resolve("scripts/rea.mjs"),
args:
process.platform === "win32"
? [resolve("scripts/rea.mjs"), "mcp"]
: ["mcp"],
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
},
},
});
@@ -390,13 +390,7 @@ cliTest(
mcp: {
rea: clientRegistrationEntry(
"opencode",
// Windows launches JavaScript through Node; otherwise setup repairs
// the command before this test can exercise layered environment data.
[
...(process.platform === "win32" ? [process.execPath] : []),
resolve("scripts/rea.mjs"),
"mcp",
],
[process.execPath, resolve("scripts/rea.mjs"), "mcp"],
env,
),
},
@@ -19,6 +19,10 @@ describe("MCP doctor help boundary", () => {
expect(result.stdout).toContain("Usage: rea mcp doctor [options]");
expect(result.stdout).toContain("--format <toon|json|yaml|md|jsonl>");
expect(result.stdout).toContain("--full-output");
expect(result.stdout).toContain("current runtime");
expect(result.stdout).toContain(
"registered command or launch environment",
);
// Help returns before the diagnostic session starts, so no report
// envelope and no startup diagnostics are produced.
expect(result.stdout).not.toContain("healthy");
@@ -43,7 +43,7 @@ describe.each(["vscode", "copilot_cli", "opencode", "commandcode"])(
await mkdir(dirname(client.configPath), { recursive: true });
await writeFile(client.configPath, original);
const currentCommandPath = join(home, "rea");
const command = [currentCommandPath, "mcp"];
const command = [process.execPath, currentCommandPath, "mcp"];
expect(await inspectClientConfiguration(client, {}, command)).toEqual({
status: "update",
backupPath: `${client.configPath}.rea.backup`,
@@ -789,6 +789,12 @@ describe("client launcher traversal semantics", () => {
await symlink(join(root, "other", "child"), join(root, "link"));
const candidate = `${root}/link/../entry.mjs`;
expect(await readFile(candidate, "utf8")).toBe("// foreign dispatcher\n");
expect(
await readFile(normalizeEntrypointPath(candidate, "node")!, "utf8"),
).toBe("// current dispatcher\n");
expect(
await readFile(normalizeEntrypointPath(candidate, "bun")!, "utf8"),
).toBe("// foreign dispatcher\n");
expect(
classifyClientRegistrationCommand([candidate, "mcp"], current),
).toBe("unknown");
@@ -802,6 +808,9 @@ describe("client launcher traversal semantics", () => {
),
).toBe("owned");
const unresolvable = `${root}/missing/../entry.mjs`;
await expect(
readFile(normalizeEntrypointPath(unresolvable, "bun")!, "utf8"),
).rejects.toMatchObject({ code: "ENOENT" });
expect(
classifyClientRegistrationCommand([unresolvable, "mcp"], current),
).toBe("unknown");
@@ -2,9 +2,11 @@ import {
chmod,
copyFile,
lstat,
mkdir,
readFile,
realpath,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { constants as fsConstants } from "node:fs";
@@ -25,6 +27,43 @@ import {
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { ClientConfigurationPublication } from "../../../src/application/ClientConfigurationPublication.js";
it
.skipIf(process.platform === "win32")
.each(["direct", "parent", "lexical-parent"] as const)(
"keeps the selected file after a parent alias retarget: %s",
async (variant) => {
const parentTraversal = variant !== "direct";
const root = await createTestTempDirectory("rea-config-retarget-");
const first = join(root, "first");
const second = variant === "lexical-parent" ? root : join(root, "second");
for (const directory of [first, second]) {
await mkdir(join(directory, "child"), { recursive: true });
await writeFile(join(directory, "config.json"), "original");
}
const alias = join(root, "link");
await symlink(parentTraversal ? join(first, "child") : first, alias);
const requestedPath = `${alias}/${parentTraversal ? "../" : ""}config.json`;
const selected = join(first, "config.json");
const state = await lstat(selected);
await rm(alias);
await symlink(parentTraversal ? join(second, "child") : second, alias);
const publication = await ClientConfigurationPublication.write(
selected,
"updated",
"original",
undefined,
{ requestedPath, state },
);
await publication.verify(async () => {
await publication.assertCurrent();
expect(await readFile(selected, "utf8")).toBe("updated");
expect(await readFile(join(second, "config.json"), "utf8")).toBe(
"original",
);
});
},
);
const failedVerificationFileSystem = (
failure: "verification" | "recovery",
): UninstallFileSystem => {
@@ -23,7 +23,7 @@ it("threads the selected host and environment into system diagnostics", async ()
environment,
execFileOutput: async (command, arguments_, options) => {
calls.push({ command, arguments: arguments_, environment: options?.env });
if (command === "sw_vers")
if (command === "/usr/bin/sw_vers")
return {
stdout: "15.0\n",
stderr: "",
@@ -52,12 +52,12 @@ it("threads the selected host and environment into system diagnostics", async ()
expect(host.platform).toBe("linux");
expect(host.architecture).toBe("x64");
await host.installationPaths?.();
await host.macosVersion();
expect(await host.macosVersion()).toBe("15.0");
await host.executable(process.execPath);
expect(calls.map(({ command }) => command)).toEqual([
"which",
"sw_vers",
"/usr/bin/sw_vers",
"ldd",
]);
expect(
@@ -1,11 +1,13 @@
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { join, resolve } from "node:path";
import { expect, it } from "vitest";
import { assertClientConfig } from "../../../scripts/verify-package-setup.mjs";
import { terminalModesRestored } from "../../../scripts/lib/verify-setup-terminal.mjs";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const cli = resolve("scripts/rea.mjs");
it("requires positive canonical-input and echo terminal mode tokens", () => {
expect(terminalModesRestored("speed 38400 baud; icanon echo isig")).toBe(
true,
@@ -28,16 +30,16 @@ it("rejects a backup replaced with the post-setup client configuration", async (
const configured = JSON.stringify({
existing: true,
model: "caller",
mcpServers: { rea: { command: process.execPath, args: ["mcp"] } },
mcpServers: { rea: { command: process.execPath, args: [cli, "mcp"] } },
});
await writeFile(path, configured);
await writeFile(`${path}.rea.backup`, original);
await assertClientConfig(process.execPath, path, original);
await assertClientConfig(cli, path, original);
await writeFile(`${path}.rea.backup`, configured);
await expect(
assertClientConfig(process.execPath, path, original),
).rejects.toThrow("packaged client backup failed");
await expect(assertClientConfig(cli, path, original)).rejects.toThrow(
"packaged client backup failed",
);
expect(await readFile(path, "utf8")).toBe(configured);
});
@@ -47,7 +49,7 @@ it("compares backup bytes even when invalid UTF-8 decodes to the same text", asy
const original = Buffer.from('{"existing":true,"note":"\uFFFD"}\n');
const configured = JSON.stringify({
existing: true,
mcpServers: { rea: { command: process.execPath, args: ["mcp"] } },
mcpServers: { rea: { command: process.execPath, args: [cli, "mcp"] } },
});
const changed = Buffer.concat([
Buffer.from('{"existing":true,"note":"'),
@@ -57,7 +59,7 @@ it("compares backup bytes even when invalid UTF-8 decodes to the same text", asy
expect(changed.toString("utf8")).toBe(original.toString("utf8"));
await writeFile(path, configured);
await writeFile(`${path}.rea.backup`, changed);
await expect(
assertClientConfig(process.execPath, path, original),
).rejects.toThrow("packaged client backup failed");
await expect(assertClientConfig(cli, path, original)).rejects.toThrow(
"packaged client backup failed",
);
});
@@ -121,6 +121,42 @@ it.skipIf(process.platform === "win32")(
},
);
it.skipIf(process.platform === "win32").each([false, true])(
"preserves filesystem parent traversal when the configuration exists=%s",
async (exists) => {
const root = await createTestTempDirectory("rea-config-parent-traversal-");
const selectedDirectory = join(root, "selected");
await mkdir(join(selectedDirectory, "child"), { recursive: true });
await symlink(join(selectedDirectory, "child"), join(root, "link"), "dir");
const selected = join(selectedDirectory, "config.json");
const unrelated = join(root, "config.json");
const original = '{"theme":"keep"}\n';
await writeFile(unrelated, original);
if (exists) await writeFile(selected, original);
const requested = `${root}/link/../config.json`;
expect(await resolveClientConfigTransactionPath(requested)).toBe(selected);
expect(
await configureClientConfiguration({
name: "cursor",
format: "json",
configPath: requested,
}),
).toMatchObject({ status: "configured" });
expect(JSON.parse(await readFile(selected, "utf8"))).toMatchObject({
mcpServers: { rea: { command: "npx" } },
});
expect(await readFile(unrelated, "utf8")).toBe(original);
await expect(lstat(`${unrelated}.rea.backup`)).rejects.toMatchObject({
code: "ENOENT",
});
if (exists)
expect(await readFile(`${selected}.rea.backup`, "utf8")).toBe(original);
expect(
await resolveClientConfigTransactionPath(`${root}/absent/../config.json`),
).toBeUndefined();
},
);
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",
@@ -1,6 +1,7 @@
import {
access,
mkdir,
readFile,
realpath,
rm,
symlink,
@@ -10,7 +11,12 @@ import { dirname, join } from "node:path";
import { expect, it } from "vitest";
import { setupRegistrationCommand } from "../../../src/application/SetupHost.js";
import { classifyClientRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js";
import {
classifyClientRegistrationCommand,
registrationRuntimePath,
} from "../../../src/application/ClientRegistrationIdentity.js";
import { readClientRegistrationInspections } from "../../../src/application/ClientRegistrationStatus.js";
import { javaScriptRuntime } from "../../../src/domain/runtimeVersion.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
it.skipIf(process.platform === "win32")(
@@ -39,14 +45,14 @@ it.skipIf(process.platform === "win32")(
const command = setupRegistrationCommand("linux", undefined, unstable, {
PATH: join(latest, "bin"),
});
expect(command).toEqual([alias, "mcp"]);
expect(command).toEqual([process.execPath, alias, "mcp"]);
expect(classifyClientRegistrationCommand(command, unstable)).toBe("owned");
await rm(latest);
await symlink(next, latest, "dir");
await rm(old, { recursive: true });
await expect(access(unstable)).rejects.toMatchObject({ code: "ENOENT" });
await access(command[0]!);
expect(await realpath(command[0]!)).toBe(join(next, scriptWithin("next")));
await access(command[1]!);
expect(await realpath(command[1]!)).toBe(join(next, scriptWithin("next")));
expect(
classifyClientRegistrationCommand(
command,
@@ -56,6 +62,168 @@ it.skipIf(process.platform === "win32")(
},
);
it.skipIf(process.platform === "win32").each(["node", "bun"] as const)(
"preserves a same-file %s runtime alias across a manager version switch",
async (runtime) => {
const root = await createTestTempDirectory("rea-stable-runtime-alias-");
const old = join(root, "old");
const next = join(root, "next");
for (const [directory, contents] of [
[old, "old runtime"],
[next, "next runtime"],
] as const) {
await mkdir(directory);
await writeFile(join(directory, runtime), contents, { mode: 0o700 });
}
const latest = join(root, "latest");
await symlink(old, latest, "dir");
const executable = join(old, runtime);
const alias = join(latest, runtime);
const selected = registrationRuntimePath(
executable,
runtime,
{ PATH: latest },
"linux",
);
expect(selected).toBe(alias);
expect(await realpath(selected)).toBe(executable);
await rm(latest);
await symlink(next, latest, "dir");
await rm(old, { recursive: true });
await expect(access(executable)).rejects.toMatchObject({ code: "ENOENT" });
await access(selected);
expect(await realpath(selected)).toBe(join(next, runtime));
expect(
registrationRuntimePath(
join(next, runtime),
runtime,
{ PATH: latest },
"linux",
),
).toBe(selected);
},
);
it.skipIf(process.platform === "win32")(
"does not replace the selected runtime with a foreign executable or manager shim",
async () => {
const root = await createTestTempDirectory("rea-runtime-alias-identity-");
const foreign = join(root, "foreign");
const missing = join(root, "missing");
const valid = join(root, "valid");
const runtime = javaScriptRuntime(process.versions).id;
const script = join(root, "rea.mjs");
await writeFile(script, "// selected REA script\n");
for (const directory of [foreign, missing, valid]) await mkdir(directory);
await writeFile(join(foreign, runtime), '#!/bin/sh\nexec node "$@"\n', {
mode: 0o700,
});
await symlink(join(root, "absent"), join(missing, runtime));
await symlink(process.execPath, join(valid, runtime));
expect(
registrationRuntimePath(
process.execPath,
runtime,
{ PATH: `${foreign}:${missing}:relative` },
"linux",
),
).toBe(process.execPath);
expect(
registrationRuntimePath(
process.execPath,
runtime,
{ PATH: `${foreign}:${valid}` },
"linux",
),
).toBe(join(valid, runtime));
expect(
setupRegistrationCommand("linux", undefined, script, { PATH: valid }),
).toEqual([join(valid, runtime), script, "mcp"]);
expect(
registrationRuntimePath(
process.execPath,
runtime,
{ PATH: valid },
"win32",
),
).toBe(process.execPath);
},
);
it.skipIf(process.platform === "win32")(
"checks the native PATH alias instead of collapsing a symlink parent segment",
async () => {
const root = await createTestTempDirectory("rea-runtime-path-parent-");
const runtime = javaScriptRuntime(process.versions).id;
await mkdir(join(root, "selected"));
await mkdir(join(root, "other", "child"), { recursive: true });
await mkdir(join(root, "other", "selected"));
await symlink(join(root, "other", "child"), join(root, "link"));
await symlink(process.execPath, join(root, "selected", runtime));
await writeFile(
join(root, "other", "selected", runtime),
"foreign runtime\n",
{ mode: 0o700 },
);
expect(
registrationRuntimePath(
process.execPath,
runtime,
{
PATH: `${root}/link/../selected`,
},
process.platform,
),
).toBe(process.execPath);
},
);
it.skipIf(process.platform === "win32")(
"retains unknown runtime identity after its executable disappears despite a known REA script",
async () => {
const home = await createTestTempDirectory("rea-removed-runtime-");
const executable = join(home, "node");
const script = join(home, "rea.mjs");
const config = join(home, ".cursor", "mcp.json");
await symlink(process.execPath, executable);
await writeFile(script, "// selected REA script\n");
await mkdir(join(home, ".cursor"));
const command = [executable, script, "mcp"];
const original = JSON.stringify({
mcpServers: {
rea: { command: executable, args: [script, "mcp"] },
},
});
await writeFile(config, original);
const inspect = () =>
readClientRegistrationInspections(home, script, {
environment: {},
selectedClientIds: ["cursor"],
});
expect(await inspect()).toContainEqual(
expect.objectContaining({
commandIdentity: "owned",
status: expect.objectContaining({ client: "cursor", state: "aligned" }),
}),
);
await rm(executable);
expect(classifyClientRegistrationCommand(command, script)).toBe("unknown");
expect(await inspect()).toContainEqual(
expect.objectContaining({
commandIdentity: "unknown",
status: expect.objectContaining({
client: "cursor",
state: "unknown",
command,
remediation: expect.any(String),
}),
}),
);
expect(await readFile(config, "utf8")).toBe(original);
},
);
it.skipIf(process.platform === "win32")(
"keeps selected entrypoint when PATH aliases are foreign, unresolved, unexecutable or relative",
async () => {
@@ -77,12 +245,12 @@ it.skipIf(process.platform === "win32")(
setupRegistrationCommand("linux", undefined, entrypoint, {
PATH: `${foreign}:${missing}:relative`,
}),
).toEqual([entrypoint, "mcp"]);
).toEqual([process.execPath, entrypoint, "mcp"]);
expect(
setupRegistrationCommand("linux", undefined, selected, {
PATH: unexecutable,
}),
).toEqual([selected, "mcp"]);
).toEqual([process.execPath, selected, "mcp"]);
expect(
setupRegistrationCommand("win32", undefined, entrypoint, {
PATH: foreign,
+3
View File
@@ -112,6 +112,9 @@ describe("production MCP doctor", () => {
ok: false,
error: {
healthy: false,
scope: expect.stringContaining(
"does not verify a client's registered command",
),
checks: [{ name: "transport", ok: false }],
},
meta: {