455 lines
15 KiB
JavaScript
455 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 } from "node:os";
|
|
import { isAbsolute, join, relative } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import Ajv2020 from "ajv/dist/2020.js";
|
|
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",
|
|
"This verifier requires a disposable POSIX account; Windows is unverified",
|
|
);
|
|
const account = process.env.REA_VERIFY_COMMANDCODE_ACCOUNT_HOME;
|
|
assert(
|
|
account && isAbsolute(account) && account === homedir(),
|
|
"Run as a disposable account whose actual OS home matches REA_VERIFY_COMMANDCODE_ACCOUNT_HOME; do not override HOME",
|
|
);
|
|
assert.equal(
|
|
await readFile(join(account, ".rea-client-verification"), "utf8"),
|
|
"Disposable REA client verification account\n",
|
|
);
|
|
const repo = fileURLToPath(new URL("..", import.meta.url));
|
|
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
|
|
const command = process.env.REA_VERIFY_COMMANDCODE_COMMAND ?? "cmd";
|
|
const mode = process.argv[2] ?? "call";
|
|
assert(
|
|
["chat", "call"].includes(mode),
|
|
"Usage: verify-commandcode-client.mjs [chat|call]",
|
|
);
|
|
const verifier = createVerifierRun();
|
|
const lab = await mkdtemp(join(account, "rea-commandcode-client-"));
|
|
const workspace = join(lab, "workspace");
|
|
const target = join(workspace, "fixture α测试");
|
|
const profile = join(account, ".commandcode");
|
|
const configPath = join(profile, "mcp.json");
|
|
const skillDirectory = join(
|
|
account,
|
|
".agents",
|
|
"skills",
|
|
"reverse-engineer-anything",
|
|
);
|
|
const expectedNames = TOOL_CONTRACTS.map(
|
|
(tool) => "mcp__rea__" + tool.name,
|
|
).sort();
|
|
let clientVersion = null;
|
|
let expectedSkill;
|
|
let inventoryChecked = false;
|
|
let catalogSize = null;
|
|
let skillLoaded = false;
|
|
let schemasChecked = 0;
|
|
let catalogArtifact;
|
|
let catalogDigest;
|
|
let catalogProjection;
|
|
let artifactProjectionSeen = false;
|
|
let evidenceSeen = false;
|
|
let evidenceBytes;
|
|
let finalSeen = false;
|
|
let stage = 0;
|
|
let passed = false;
|
|
let fixture;
|
|
const commands = await createVerifierCommands();
|
|
await withVerifierCleanup(async () => {
|
|
await withVerifierCleanup(async () => {
|
|
await Promise.all([
|
|
mkdir(target, { recursive: true }),
|
|
mkdir(profile, { recursive: true }),
|
|
mkdir(join(lab, "tmp")),
|
|
]);
|
|
await commands.exec("git", ["init", "--quiet", workspace], {
|
|
timeout: 30_000,
|
|
stdin: "pipe",
|
|
});
|
|
await writeFile(join(target, "app.js"), CLIENT_JAVASCRIPT_FIXTURE_SOURCE);
|
|
const environment = {
|
|
PATH: process.env.PATH,
|
|
NODE_OPTIONS: process.env.NODE_OPTIONS,
|
|
USERPROFILE: account,
|
|
TMPDIR: join(lab, "tmp"),
|
|
CMD_LOCAL_ONLY: "1",
|
|
// Native print mode checks for an account key even with a keyless BYOK model.
|
|
// This synthetic value is never a real credential; local-only refuses hosted API calls.
|
|
COMMAND_CODE_API_KEY: "local-fixture-only",
|
|
COMMANDCODE_SKIP_UPDATES: "1",
|
|
COMMANDCODE_DISABLE_CRON: "1",
|
|
OTEL_SDK_DISABLED: "true",
|
|
REA_PROCESS_RUN_ID: verifier.run_id,
|
|
};
|
|
const schemaValidator = new Ajv2020({
|
|
strict: false,
|
|
validateFormats: false,
|
|
});
|
|
const textValues = (value) =>
|
|
typeof value === "string"
|
|
? [value]
|
|
: value !== null && typeof value === "object"
|
|
? Object.values(value).flatMap(textValues)
|
|
: [];
|
|
const call = (name, args) => ({
|
|
role: "assistant",
|
|
content: null,
|
|
tool_calls: [
|
|
{
|
|
id: `call_commandcode_${++stage}`,
|
|
type: "function",
|
|
function: { name, arguments: JSON.stringify(args) },
|
|
},
|
|
],
|
|
});
|
|
const validateCatalog = async (text) => {
|
|
const entries = [
|
|
...text.matchAll(
|
|
/^### (mcp__rea__[A-Za-z0-9_]+)\n[\s\S]*?\nParameters:\n```json\n([\s\S]*?)\n```/gm,
|
|
),
|
|
];
|
|
assert.deepEqual(
|
|
entries.map((entry) => entry[1]).sort(),
|
|
expectedNames,
|
|
"Native exact-name search must return every REA input schema",
|
|
);
|
|
for (const entry of entries) {
|
|
const schema = JSON.parse(entry[2]);
|
|
assert(
|
|
schemaValidator.validateSchema(schema),
|
|
`${entry[1]}: invalid native schema: ${JSON.stringify(schemaValidator.errors)}`,
|
|
);
|
|
}
|
|
schemasChecked = entries.length;
|
|
catalogDigest = createHash("sha256").update(text).digest("hex");
|
|
await writeFile(join(lab, "native-catalog.md"), text);
|
|
};
|
|
fixture = await createOpenAiModelFixture({
|
|
directory: lab,
|
|
prefix: "commandcode",
|
|
onRequest: async ({ body, tools }) => {
|
|
assert(tools.some((tool) => tool.name === "search_tools"));
|
|
const strings = textValues(body.messages);
|
|
if (!inventoryChecked) {
|
|
const names = [
|
|
...new Set(
|
|
strings.flatMap(
|
|
(text) => text.match(/mcp__rea__[A-Za-z0-9_]+/g) ?? [],
|
|
),
|
|
),
|
|
].sort();
|
|
catalogSize = names.length;
|
|
assert.deepEqual(
|
|
names,
|
|
expectedNames,
|
|
"Default native deferred-tool prompt must advertise the complete REA inventory",
|
|
);
|
|
assert.equal(
|
|
tools.filter((tool) => tool.name.startsWith("mcp__rea__")).length,
|
|
0,
|
|
"Keep native lazy schema delivery enabled",
|
|
);
|
|
assert(
|
|
tools
|
|
.find((tool) => tool.name === "activate_skill")
|
|
?.parameters.properties.name.enum.includes(
|
|
"reverse-engineer-anything",
|
|
),
|
|
);
|
|
inventoryChecked = true;
|
|
}
|
|
if (mode === "chat") {
|
|
assert.equal(
|
|
body.messages.filter((message) => message.role === "tool").length,
|
|
0,
|
|
);
|
|
finalSeen = true;
|
|
return { role: "assistant", content: "REA_CLIENT_COMPATIBILITY_OK" };
|
|
}
|
|
if (stage === 0)
|
|
return call("activate_skill", { name: "reverse-engineer-anything" });
|
|
assert(
|
|
strings.some((text) => text.includes(expectedSkill)),
|
|
"The model must receive the complete activated skill body",
|
|
);
|
|
skillLoaded = true;
|
|