fix(setup): register explicit MCP runtimes and preserve launcher identity

This commit is contained in:
morluto
2026-10-26 02:31:41 +08:00
parent b8ce3c74cd
commit d69a54d592
19 changed files with 637 additions and 100 deletions
+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,