Reuse the format owner's full validation for exact authenticated immutable captures across retained queries. Preserve ordinary mutable-input validation and per-request subject, coverage and budget checks. Refs #3133. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
252 lines
8.2 KiB
JavaScript
252 lines
8.2 KiB
JavaScript
/** Public stdio MCP capture paging; keep original recordings and transcripts outside Git. */
|
|
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { mkdir, readFile, writeFile, mkdtemp } from "node:fs/promises";
|
|
import { createWriteStream } from "node:fs";
|
|
import { finished } from "node:stream/promises";
|
|
import { join, resolve } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { parseArgs } from "node:util";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { createRetainedQueryCaller } from "./retained-query-calls.mjs";
|
|
import { finalizeRetainedQueryReceipt } from "./retained-query-finalization.mjs";
|
|
|
|
const { values } = parseArgs({
|
|
options: Object.fromEntries(
|
|
["runtime", "capture", "output", "bundle"].map((name) => [
|
|
name,
|
|
{ type: "string" },
|
|
]),
|
|
),
|
|
});
|
|
for (const name of ["runtime", "capture", "output"])
|
|
assert.ok(
|
|
values[name] && values[name] === resolve(values[name]),
|
|
`${name} must be absolute`,
|
|
);
|
|
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
const output = values.output;
|
|
await mkdir(output, { mode: 0o700 });
|
|
const recording = await readFile(values.capture);
|
|
const raw = JSON.parse(recording);
|
|
const events = raw.traces.flatMap((trace, t) =>
|
|
trace.events.map((reported, e) => ({
|
|
reported,
|
|
pointer: `/traces/${t}/events/${e}`,
|
|
})),
|
|
);
|
|
assert.ok(events.length >= 6);
|
|
const home = await mkdtemp(join(tmpdir(), "rea-capture-validation-"));
|
|
const client = new Client({
|
|
name: "retained-capture-validation",
|
|
version: "1",
|
|
});
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [join(values.runtime, "scripts/rea.mjs"), "mcp"],
|
|
env: {
|
|
HOME: home,
|
|
PATH: process.env.PATH,
|
|
NODE_OPTIONS: "--max-old-space-size=2048",
|
|
REA_LOG_LEVEL: "silent",
|
|
},
|
|
stderr: "pipe",
|
|
});
|
|
const serverLog = createWriteStream(join(output, "server.log"));
|
|
const serverLogFinished = finished(serverLog);
|
|
serverLogFinished.catch(() => {});
|
|
transport.stderr?.pipe(serverLog);
|
|
const report = {
|
|
capture_sha256: hash(recording),
|
|
capture_bytes: recording.length,
|
|
events: events.length,
|
|
mode: values.bundle ? "imported" : "producer",
|
|
calls: [],
|
|
};
|
|
const transcript = [];
|
|
const call = createRetainedQueryCaller(client, (receipt, event) => {
|
|
report.calls.push(receipt);
|
|
transcript.push(event);
|
|
});
|
|
const result = async (name, args, phase) => {
|
|
const response = await call(name, args, phase);
|
|
assert.notEqual(response.isError, true, JSON.stringify(response));
|
|
assert.ok(response.structuredContent);
|
|
return response.structuredContent;
|
|
};
|
|
const started = performance.now();
|
|
try {
|
|
await client.connect(transport);
|
|
const discovery = performance.now();
|
|
const catalog = await client.listTools();
|
|
assert.equal(
|
|
catalog.nextCursor,
|
|
undefined,
|
|
"This driver requires a complete catalog page",
|
|
);
|
|
report.catalog = {
|
|
elapsed_ms: performance.now() - discovery,
|
|
sha256: hash(JSON.stringify(catalog)),
|
|
bytes: Buffer.byteLength(JSON.stringify(catalog)),
|
|
};
|
|
await writeFile(join(output, "catalog.json"), JSON.stringify(catalog));
|
|
// Compile the exact live named contract for each call, avoiding client-wide compilation.
|
|
const definitions = new Map(catalog.tools.map((tool) => [tool.name, tool]));
|
|
const originalCallTool = client.callTool.bind(client);
|
|
client.callTool = (input, options) =>
|
|
originalCallTool(input, {
|
|
...options,
|
|
toolDefinition: definitions.get(input.name),
|
|
});
|
|
let original;
|
|
if (values.bundle) {
|
|
const bundle = JSON.parse(await readFile(values.bundle, "utf8"));
|
|
original = bundle.records.find(
|
|
(record) => record.operation === "inspect_web_network_capture",
|
|
);
|
|
assert.ok(original);
|
|
await result("import_evidence_bundle", { path: values.bundle }, "import");
|
|
} else {
|
|
const evidence = await result(
|
|
"inspect_web_network_capture",
|
|
{
|
|
capture_path: values.capture,
|
|
format: "qlog",
|
|
detail: "summary",
|
|
},
|
|
"produce-summary",
|
|
);
|
|
report.summary_id = evidence.evidence_id;
|
|
const path = join(output, "original.json");
|
|
await result("export_evidence_bundle", { path }, "export-original");
|
|
const bundle = JSON.parse(await readFile(path, "utf8"));
|
|
original = bundle.records.find(
|
|
(record) => record.operation === "inspect_web_network_capture",
|
|
);
|
|
assert.ok(original);
|
|
}
|
|
assert.equal(
|
|
original.normalized_result.artifact.sha256,
|
|
report.capture_sha256,
|
|
);
|
|
assert.equal(original.normalized_result.total_records, events.length);
|
|
const source = {
|
|
kind: "retained-evidence",
|
|
evidence_id: original.evidence_id,
|
|
};
|
|
report.source_id = original.evidence_id;
|
|
const answers = [];
|
|
for (let pass = 0; pass < 3; pass += 1) {
|
|
const summary = await result(
|
|
"inspect_analysis_view",
|
|
{ source, view: { kind: "summary" } },
|
|
`summary-${pass}`,
|
|
);
|
|
assert.equal(
|
|
summary.normalized_result.summary.total_records,
|
|
events.length,
|
|
);
|
|
answers.push(summary);
|
|
for (const offset of [0, 2, 4]) {
|
|
const evidence = await result(
|
|
"inspect_analysis_view",
|
|
{
|
|
source,
|
|
view: { kind: "capture", detail: "complete", offset, limit: 2 },
|
|
},
|
|
`page-${pass}-${offset}`,
|
|
);
|
|
const view = evidence.normalized_result;
|
|
assert.equal(view.parent_evidence_id, original.evidence_id);
|
|
assert.equal(view.coverage.total, events.length);
|
|
assert.equal(view.coverage.next_offset, offset + 2);
|
|
assert.equal(
|
|
view.items.length,
|
|
2,
|
|
"Every selected record must be returned",
|
|
);
|
|
for (const [i, row] of view.items.entries()) {
|
|
assert.equal(row.ordinal, offset + i);
|
|
assert.equal(row.location.pointer, events[offset + i].pointer);
|
|
const expected = structuredClone(events[offset + i].reported);
|
|
// QUIC reset tokens are transport authentication material, not reported facts.
|
|
const resetToken = Object.hasOwn(
|
|
expected.data ?? {},
|
|
"stateless_reset_token",
|
|
);
|
|
if (resetToken) expected.data.stateless_reset_token = null;
|
|
assert.deepEqual(
|
|
row.redactions,
|
|
resetToken
|
|
? [
|
|
{
|
|
pointer: "/data/stateless_reset_token",
|
|
reason: "transport-credential",
|
|
},
|
|
]
|
|
: [],
|
|
);
|
|
assert.deepEqual(row.reported, expected);
|
|
}
|
|
answers.push(evidence);
|
|
}
|
|
const traces = await result(
|
|
"inspect_analysis_view",
|
|
{
|
|
source,
|
|
view: { kind: "qlog", facet: "traces", offset: 0, limit: 50 },
|
|
},
|
|
`traces-${pass}`,
|
|
);
|
|
assert.equal(traces.normalized_result.page.items.length, raw.traces.length);
|
|
for (const [i, row] of traces.normalized_result.page.items.entries()) {
|
|
assert.equal(row.trace_index, i);
|
|
assert.equal(row.event_count, raw.traces[i].events.length);
|
|
}
|
|
answers.push(traces);
|
|
}
|
|
const roundtrip = join(output, "roundtrip.json");
|
|
await result(
|
|
"export_evidence_bundle",
|
|
{ path: roundtrip },
|
|
"export-roundtrip",
|
|
);
|
|
const exported = JSON.parse(await readFile(roundtrip, "utf8"));
|
|
assert.deepEqual(
|
|
exported.records.find(
|
|
(record) => record.evidence_id === original.evidence_id,
|
|
),
|
|
original,
|
|
);
|
|
assert.equal(hash(await readFile(values.capture)), report.capture_sha256);
|
|
report.answers_sha256 = hash(JSON.stringify(answers));
|
|
report.original_record_sha256 = hash(JSON.stringify(original));
|
|
report.status = "passed";
|
|
} catch (cause) {
|
|
report.status = "failed";
|
|
report.failure = String(cause?.stack ?? cause);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
report.elapsed_before_cleanup_ms = performance.now() - started;
|
|
try {
|
|
await writeFile(
|
|
join(output, "transcript.json"),
|
|
JSON.stringify(transcript),
|
|
);
|
|
} catch (cause) {
|
|
report.status = "failed";
|
|
report.transcript_failure = String(cause);
|
|
}
|
|
const finalized = await finalizeRetainedQueryReceipt({
|
|
report,
|
|
client,
|
|
transport,
|
|
account: home,
|
|
serverLog,
|
|
serverLogFinished,
|
|
output,
|
|
});
|
|
if (report.status !== "passed" || !finalized) process.exitCode = 1;
|
|
}
|