Files
rea/scripts/verify-omp-client.mjs
T

467 lines
15 KiB
JavaScript

import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { join, relative } from "node:path";
import { fileURLToPath } from "node:url";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { createVerifierCommands } from "./lib/verify-commands.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
import { saveVerifierCommandOutput } from "./lib/verifier-command-output.mjs";
import {
CLIENT_JAVASCRIPT_FIXTURE_SOURCE,
assertClientJavaScriptFixtureEvidence,
} from "./verify/clients/javascript-fixture.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs";
assert(
process.platform !== "win32",
"Native OMP verifier requires POSIX; Windows is unverified",
);
const repo = fileURLToPath(new URL("..", import.meta.url));
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
const command = process.env.REA_VERIFY_OMP_COMMAND ?? "omp";
const mode = process.argv[2] ?? "call";
assert(
["chat", "call"].includes(mode),
"Usage: verify-omp-client.mjs [chat|call]",
);
const verifier = createVerifierRun();
const lab = await mkdtemp(join(tmpdir(), "rea-omp-client-"));
const account = join(lab, "account");
const agent = join(lab, "selected agent");
const workspace = join(lab, "workspace");
const configPath = join(agent, "mcp.json");
const skillDirectory = join(
account,
".agents",
"skills",
"reverse-engineer-anything",
);
const expectedDevices = TOOL_CONTRACTS.map(
(tool) => "mcp__rea_" + tool.name,
).sort();
let clientVersion = null;
let expectedSkillBody;
let skillLoaded = false;
let contractRead = false;
let evidenceSeen = false;
let artifactDigest;
let finalSeen = false;
let catalogMounted = false;
let catalogSize = null;
let invalidProfileRejected = false;
let passed = false;
let fixture;
const commands = await createVerifierCommands();
await withVerifierCleanup(async () => {
await withVerifierCleanup(async () => {
await Promise.all([
mkdir(account),
mkdir(agent),
mkdir(workspace),
...["data", "state", "cache"].map((name) =>
mkdir(join(lab, name, "omp"), { recursive: true }),
),
]);
await commands.exec("git", ["init", "--quiet", workspace], {
timeout: 30_000,
stdin: "pipe",
});
// OMP's global config root uses native homedir independently of the agent override.
// A documented relative PI_CONFIG_DIR redirects it without changing HOME.
const environment = {
PATH: process.env.PATH,
NODE_OPTIONS: process.env.NODE_OPTIONS,
USERPROFILE: account,
PI_CODING_AGENT_DIR: agent,
PI_CONFIG_DIR: relative(homedir(), join(lab, "native-config")),
OMP_PROFILE: "",
XDG_DATA_HOME: join(lab, "data"),
XDG_STATE_HOME: join(lab, "state"),
XDG_CACHE_HOME: join(lab, "cache"),
REA_PROCESS_RUN_ID: verifier.run_id,
};
const target = join(lab, "fixture α测试");
await mkdir(target);
await writeFile(join(target, "app.js"), CLIENT_JAVASCRIPT_FIXTURE_SOURCE);
const analysisDevice = "xd://mcp__rea_analyze_javascript_application";
const containsText = (value, text) => {
if (typeof value === "string") return value.includes(text);
return (
value !== null &&
typeof value === "object" &&
Object.values(value).some((item) => containsText(item, text))
);
};
const call = (index, name, args) => ({
role: "assistant",
content: null,
tool_calls: [
{
id: `call_omp_${index}`,
type: "function",
function: { name, arguments: JSON.stringify(args) },
},
],
});
fixture = await createOpenAiModelFixture({
directory: lab,
prefix: "omp",
onRequest: async ({ body, tools, requestIndex }) => {
assert(tools.some((tool) => tool.name === "read"));
assert(tools.some((tool) => tool.name === "write"));
const instructions = body.messages.filter(
(message) => message.role === "system",
);
for (const device of expectedDevices)
assert(
containsText(instructions, device),
`OMP must advertise mounted device ${device}`,
);
if (mode === "chat") {
finalSeen = true;
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
}
const replies = body.messages.filter(
(message) => message.role === "tool",
);
if (requestIndex === 1)
return call(requestIndex, "read", {
i: "Load REA skill",
path: "skill://reverse-engineer-anything:raw",
});
assert(
expectedSkillBody !== undefined &&
containsText(replies, expectedSkillBody),
"OMP must return the complete installed skill body",
);
skillLoaded = true;
if (requestIndex === 2)
return call(requestIndex, "read", {
i: "Inspect REA native tool contract",
path: analysisDevice,
});
assert(
containsText(replies, "# mcp__rea_analyze_javascript_application"),
);
assert(
containsText(replies, "input_path") &&
containsText(replies, '"directory"'),
);
contractRead = true;
if (requestIndex === 3)
return call(requestIndex, "write", {
path: analysisDevice,
content: JSON.stringify({
input_path: target,
format: "directory",
}),
});
const evidenceReply = replies.find(
(reply) =>
typeof reply.content === "string" &&
reply.content.startsWith('{"evidence_id":'),
);
assert(
evidenceReply,
"OMP must deliver the full analysis result to the next model turn",
);
const { evidence } = await assertClientJavaScriptFixtureEvidence(
JSON.parse(evidenceReply.content),
target,
);
const bytes = JSON.stringify(evidence);
artifactDigest = createHash("sha256").update(bytes).digest("hex");
await writeFile(join(lab, "omp-evidence.json"), bytes);
evidenceSeen = true;
finalSeen = true;
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
},
});
const original = JSON.stringify(
{
mcpServers: { other: { command: "unrelated-server", enabled: false } },
disabledServers: ["rea", "other"],
},
null,
2,
);
await writeFile(configPath, original);
await writeFile(
join(agent, "config.yml"),
JSON.stringify({
skills: { customDirectories: [join(account, ".agents", "skills")] },
retry: { fallbackChains: { default: [] } },
}),
);
await writeFile(
join(agent, "models.yml"),
JSON.stringify({
providers: {
"rea-fixture": {
baseUrl: fixture.baseUrl,
apiKey: "local-fixture-key",
api: "openai-completions",
models: [
{
id: "rea-client-fixture",
name: "REA local fixture",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 1000000,
maxTokens: 4096,
},
],
},
},
}),
);
const rea = async (args, env = environment) =>
commands.exec(
process.execPath,
[join(runtimeRoot, "scripts/rea.mjs"), ...args, "--json"],
{ env, cwd: workspace, timeout: 60_000, stdin: "pipe" },
);
clientVersion = (
await commands.exec(command, ["--version"], {
env: environment,
timeout: 30_000,
stdin: "pipe",
})
).stdout.trim();
const invalidEnvironment = { ...environment, OMP_PROFILE: "Bad Name" };
const nativeRejected = await commands
.exec(command, ["--print", "--no-session", "--no-title", "Hello"], {
env: invalidEnvironment,
cwd: workspace,
timeout: 30_000,
stdin: "pipe",
})
.then(
() => undefined,
(cause) => {
if (typeof cause?.code !== "number" || cause.code === 0) throw cause;
return cause;
},
);
assert(
nativeRejected?.code === 1 &&
nativeRejected.snapshot.stderr.text.includes(
'Invalid OMP profile "Bad Name"',
),
);
await writeFile(
join(lab, "invalid-profile-native.stderr"),
nativeRejected.snapshot.stderr.text,
);
const setupRejected = await rea(
["setup", "--client", "omp", "--dry-run"],
invalidEnvironment,
).then(
() => undefined,
(cause) => {
if (typeof cause?.code !== "number" || cause.code === 0) throw cause;
return cause;
},
);
assert(typeof setupRejected?.code === "number" && setupRejected.code !== 0);
const rejected = JSON.parse(setupRejected.snapshot.stdout.text);
assert.equal(rejected.status, "needs_human");
assert.deepEqual(rejected.plannedActions, []);
assert.deepEqual(rejected.appliedActions, []);
assert(rejected.remediation.includes('Invalid OMP profile "Bad Name"'));
assert.equal(await readFile(configPath, "utf8"), original);
await writeFile(
join(lab, "invalid-profile-setup.json"),
setupRejected.snapshot.stdout.text,
);
invalidProfileRejected = true;
for (const args of [
["setup", "--client", "omp", "--dry-run"],
["setup", "--client", "omp", "--yes"],
["doctor", "--client", "omp", "--skill"],
]) {
const result = await rea(args);
await writeFile(
join(
lab,
`${args[0]}${args.includes("--dry-run") ? "-plan" : ""}.json`,
),
result.stdout,
);
const parsed = JSON.parse(result.stdout);
if (args[0] === "doctor") assert.equal(parsed.healthy, true);
else {
assert.equal(
parsed.status,
args.includes("--dry-run") ? "planned" : "ready",
);
if (args.includes("--dry-run")) {
assert(
parsed.plannedActions.some(
(action) => action.target === configPath,
),
);
assert(
parsed.plannedActions.some(
(action) => action.target === skillDirectory,
),
);
assert(
parsed.plannedActions.every(
(action) =>
action.target === configPath ||
action.target === skillDirectory ||
action.target.startsWith(skillDirectory + "/"),
),
"Refuse setup writes outside the owned config/skill",
);
}
}
}
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
const configured = await readFile(configPath, "utf8");
const registration = JSON.parse(configured);
assert.equal(registration.mcpServers.other.command, "unrelated-server");
assert.deepEqual(registration.disabledServers, ["other"]);
const repeated = JSON.parse(
(await rea(["setup", "--client", "omp", "--yes"])).stdout,
);
assert.deepEqual(repeated.appliedActions, []);
assert.equal(await readFile(configPath, "utf8"), configured);
assert.equal(await readFile(`${configPath}.rea.backup`, "utf8"), original);
expectedSkillBody = await readFile(
join(skillDirectory, "SKILL.md"),
"utf8",
);
const response = await saveVerifierCommandOutput(
() =>
commands.exec(
command,
[
"--mode",
"json",
"--print",
"--no-session",
"--no-title",
"--no-lsp",
"--no-pty",
"--approval-mode",
"yolo",
"--model",
"rea-fixture/rea-client-fixture",
mode === "chat"
? "Say hello briefly."
: `Use the REA skill to analyze ${target}.`,
],
{
env: environment,
cwd: workspace,
timeout: 120_000,
maxBuffer: 10 * 1024 * 1024,
stdin: "closed-pipe",
},
),
{
stdout: join(lab, `${mode}.stdout`),
stderr: join(lab, `${mode}.stderr`),
},
);
assert.equal(fixture.failure, undefined);
const events = response.stdout
.trim()
.split("\n")
.map((line) => JSON.parse(line));
const mounted = events
.filter(
(event) =>
event.type === "notice" &&
event.message?.startsWith("xd://: mounted "),
)
.flatMap((event) =>
event.message.slice("xd://: mounted ".length).split(", "),
)
.filter((name) => name.startsWith("mcp__rea_"));
catalogSize = mounted.length;
assert.deepEqual(
mounted.sort(),
expectedDevices,
"Native OMP must mount the complete REA catalog",
);
catalogMounted = true;
assert(
!events.some(
(event) => event.type === "tool_execution_end" && event.isError,
),
);
assert(
events.some(
(event) =>
event.type === "message_end" &&
event.message?.role === "assistant" &&
event.message.stopReason === "stop" &&
containsText(event.message.content, "REA_CLIENT_COMPATIBILITY_OK"),
),
"OMP must finish with the expected native assistant message",
);
assert(
events.some((event) => event.type === "agent_end" && event.isTerminal),
);
assert(finalSeen);
if (mode === "call") assert(skillLoaded && contractRead && evidenceSeen);
}, [
() => commands.close(),
async () => {
await fixture?.close();
},
]);
passed = true;
}, [
async () => {
const lineage = await completeVerifierRun(verifier);
const receipt = {
client: "OMP",
clientVersion,
mode,
passed,
artifacts: lab,
runtimeRoot,
host: {
platform: process.platform,
architecture: process.arch,
node: process.versions.node,
},
modelFixture:
"loopback OpenAI completions/SSE, caller-declared one-million-token model and synthetic usage; no live model claim",
discovery:
"explicit isolated default agent/global config/XDG roots and custom skill directory; named profiles, default OS-home skill discovery and Windows unverified",
interface:
"default native xd:// device metadata with read/write dispatch; complete JSON schemas are not forwarded as model functions",
catalogMounted,
catalogSize,
invalidProfileRejected,
skillLoaded,
contractRead,
evidenceSeen,
artifactDigest,
finalSeen,
requests: fixture?.requests,
probes: fixture?.probes,
fixtureFailure: fixture?.failure,
verifier: lineage,
};
await withVerifierCleanup(
() =>
writeFile(join(lab, "receipt.json"), JSON.stringify(receipt, null, 2)),
[
() => {
console.log(JSON.stringify(receipt));
},
],
);
},
]);