mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
fix(setup): register explicit MCP runtimes and preserve launcher identity
This commit is contained in:
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user