Files
rea/tests/acceptance/tools/protobufCapture.test.ts
T

632 lines
21 KiB
TypeScript

import { z } from "zod";
import { readFile, unlink, writeFile, mkdir, truncate } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { cliTest } from "../../support/cli/cliFixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
protobufCaptureDescriptor,
protobufHar,
protobufFrame,
protobufCaptureBody,
} from "../../fixtures/protobufCapture.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import {
webNetworkCaptureSchema,
inspectWebNetworkCaptureInputSchema,
} from "../../../src/domain/webNetworkCapture.js";
import { readEvidenceBundle } from "../../../src/application/EvidenceBundleFiles.js";
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
type CaptureRequest = z.input<typeof inspectWebNetworkCaptureInputSchema>;
// Acceptance files have isolated workers; the file owns one server and each case owns its inputs.
const captureTest = cliTest.extend<{
$worker: { mcp: Awaited<ReturnType<typeof connectLocalToolsMcp>> };
}>({
mcp: [
// oxlint-disable-next-line no-empty-pattern -- Vitest parses fixture dependencies from object destructuring.
async ({}, use) => {
let close: (() => Promise<void>) | undefined;
try {
const mcp = await connectLocalToolsMcp({}, (cleanup) => {
close = cleanup;
});
await use(mcp);
} finally {
await close?.();
}
},
{ scope: "worker" },
],
});
const retainedCaptureDetailsSchema = z.object({
partial_observation: z.strictObject({
kind: z.literal("retained-evidence"),
evidence_id: z.string(),
}),
});
async function prepareMcpCapture(
call: Awaited<ReturnType<typeof connectLocalToolsMcp>>["call"],
) {
const root = await createTestTempDirectory("rea-protobuf-mcp-");
const capture = join(root, "capture.har");
const descriptor = join(root, "capture.pb");
await writeFile(capture, protobufHar());
await writeFile(descriptor, protobufCaptureDescriptor());
const args: CaptureRequest = {
capture_path: capture,
format: "har",
record_ordinals: [0],
protobuf: {
schema: {
descriptor_set_path: descriptor,
message_type: "capture.Message",
},
},
};
return { call, args, root, capture, descriptor };
}
captureTest(
"excludes Protobuf values in text framing and trailer credentials through MCP",
async ({ mcp }) => {
const { call, root } = await prepareMcpCapture(mcp.call);
const textPath = join(root, "text.har");
await writeFile(
textPath,
protobufHar(
Buffer.from(protobufCaptureBody().toString("base64")),
"application/grpc-web-text",
),
);
const textExcluded = await call("inspect_web_network_capture", {
capture_path: textPath,
format: "har",
record_ordinals: [0],
sensitive_values: ["hello"],
});
expect(textExcluded.isError, JSON.stringify(textExcluded)).not.toBe(true);
const excludedText = webNetworkCaptureSchema.parse(
parseEvidence(textExcluded.structuredContent).normalized_result,
);
expect(excludedText.records[0]?.binary_fields[0]).toMatchObject({
state: "redacted",
content_base64: null,
sha256: null,
});
expect(excludedText.records[0]?.reported).toMatchObject({
response: { content: { text: null } },
});
expect(excludedText.records[0]?.protobuf).toBeUndefined();
const credentialPath = join(root, "trailer.har");
await writeFile(
credentialPath,
protobufHar(
Buffer.concat([
protobufFrame(Buffer.from("0a0568656c6c6f", "hex")),
protobufFrame(
Buffer.from(
"grpc-status: 0\r\nauthorization: Bearer fixture-only-credential\r\n",
),
128,
),
]),
),
);
const credential = await call("inspect_web_network_capture", {
capture_path: credentialPath,
format: "har",
record_ordinals: [0],
});
expect(credential.isError, JSON.stringify(credential)).not.toBe(true);
const excludedCredential = webNetworkCaptureSchema.parse(
parseEvidence(credential.structuredContent).normalized_result,
);
expect(excludedCredential.records[0]?.binary_fields[0]).toMatchObject({
state: "redacted",
content_base64: null,
});
expect(excludedCredential.records[0]?.redactions).toContainEqual({
pointer: "/response/content/text",
reason: "transport-credential",
});
expect(excludedCredential.records[0]?.protobuf).toBeUndefined();
expect(JSON.stringify(credential)).not.toContain("fixture-only-credential");
},
);
captureTest(
"retains partial capture Evidence after schema and capacity failures through MCP",
async ({ mcp }) => {
const { call, args, root, descriptor } = await prepareMcpCapture(mcp.call);
const missing = await call("inspect_web_network_capture", {
...args,
sensitive_values: ["unrelated fixture exclusion"],
protobuf: {
schema: {
descriptor_set_path: descriptor,
message_type: "capture.Missing",
},
},
});
const missingError = parseMcpToolError(missing).error;
expect(missingError.code).toBe("invalid_request");
const retainedSource = retainedCaptureDetailsSchema.parse(
missingError.details,
).partial_observation;
const retainedSummary = await call("inspect_analysis_view", {
source: retainedSource,
view: { kind: "summary" },
});
expect(retainedSummary.isError, JSON.stringify(retainedSummary)).not.toBe(
true,
);
expect(
analysisViewResultSchema.parse(
parseEvidence(retainedSummary.structuredContent).normalized_result,
),
).toMatchObject({
summary: {
protobuf_coverage: { status: "partial", inspected_records: 0 },
},
});
const missingView = await call("inspect_analysis_view", {
source: retainedSource,
view: { kind: "protobuf", collection: "fields", offset: 0, limit: 1 },
});
expect(missingView.isError, JSON.stringify(missingView)).not.toBe(true);
expect(
analysisViewResultSchema.parse(
parseEvidence(missingView.structuredContent).normalized_result,
),
).toMatchObject({ source_status: "partial", items: [] });
const capacityPath = join(root, "capacity.har");
const repeated = Buffer.alloc(200000);
for (let index = 0; index < repeated.length; index += 2) {
repeated[index] = 8;
repeated[index + 1] = 1;
}
await writeFile(capacityPath, protobufHar(protobufFrame(repeated)));
const capacity = await call("inspect_web_network_capture", {
capture_path: capacityPath,
format: "har",
record_ordinals: [0],
sensitive_values: ["unrelated fixture exclusion"],
});
const capacityError = parseMcpToolError(capacity).error;
expect(capacityError.code).toBe("resource_constraint");
expect(capacityError.retryable).toBe(false);
const capacitySource = retainedCaptureDetailsSchema.parse(
capacityError.details,
).partial_observation;
const capacitySummary = await call("inspect_analysis_view", {
source: capacitySource,
view: { kind: "summary" },
});
expect(capacitySummary.isError, JSON.stringify(capacitySummary)).not.toBe(
true,
);
expect(
analysisViewResultSchema.parse(
parseEvidence(capacitySummary.structuredContent).normalized_result,
),
).toMatchObject({
summary: {
protobuf_coverage: { status: "partial", inspected_records: 0 },
},
});
expect(JSON.stringify(capacity)).not.toContain("content_base64");
},
);
captureTest(
"preflights trailer credentials before parsing and retaining later records through MCP",
async ({ mcp }) => {
const { call, root, descriptor } = await prepareMcpCapture(mcp.call);
const repeated = Buffer.alloc(200000);
for (let index = 0; index < repeated.length; index += 2) {
repeated[index] = 8;
repeated[index + 1] = 1;
}
const large = protobufFrame(repeated);
const credential = protobufFrame(
Buffer.from(
"grpc-status: 0\r\nauthorization: fixture-only-trailer-credential\r\n",
),
128,
);
const small = protobufFrame(Buffer.from([8, 1]));
for (const [name, body, mimeType, framing] of [
[
"large",
Buffer.concat([large, credential]),
"application/grpc-web+proto",
"auto",
],
[
"malformed",
Buffer.concat([
small,
protobufFrame(
Buffer.from(
"malformed\r\nauthorization: fixture-only-trailer-credential\r\n",
),
128,
),
]),
"application/grpc-web+proto",
"auto",
],
[
"text-override",
Buffer.from(Buffer.concat([small, credential]).toString("base64")),
"application/grpc-web-text",
"raw",
],
] as const) {
const path = join(root, `credential-${name}.har`);
await writeFile(path, protobufHar(body, mimeType));
const response = await call("inspect_web_network_capture", {
capture_path: path,
format: "har",
record_ordinals: [0],
protobuf: { framing },
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const capture = webNetworkCaptureSchema.parse(
parseEvidence(response.structuredContent).normalized_result,
);
expect(capture.records[0]?.binary_fields[0]).toMatchObject({
state: "redacted",
content_base64: null,
sha256: null,
});
expect(capture.records[0]?.protobuf).toBeUndefined();
expect(capture.protobuf_coverage?.status).toBe("partial");
}
const path = join(root, "credential-later-record.har");
await writeFile(
path,
protobufHar(large, "application/grpc-web+proto", [
Buffer.concat([small, credential]),
]),
);
for (const schema of [
undefined,
{ descriptor_set_path: descriptor, message_type: "capture.Missing" },
]) {
const response = await call("inspect_web_network_capture", {
capture_path: path,
format: "har",
record_ordinals: [0, 1],
sensitive_values: ["unrelated fixture exclusion"],
...(schema === undefined ? {} : { protobuf: { schema } }),
});
const error = parseMcpToolError(response).error;
expect(error.code).toBe(
schema === undefined ? "resource_constraint" : "invalid_request",
);
const source = retainedCaptureDetailsSchema.parse(
error.details,
).partial_observation;
const retained = await call("inspect_analysis_view", {
source,
view: { kind: "capture", detail: "complete", offset: 0, limit: 2 },
});
expect(retained.isError, JSON.stringify(retained)).not.toBe(true);
const view = analysisViewResultSchema.parse(
parseEvidence(retained.structuredContent).normalized_result,
);
expect(view).toMatchObject({
items: [
expect.anything(),
{
binary_fields: [
{ state: "redacted", content_base64: null, sha256: null },
],
},
],
});
}
},
);
captureTest(
"reports descriptor read failures with retained capture Evidence through MCP",
async ({ mcp }) => {
const { call, args, root } = await prepareMcpCapture(mcp.call);
const directory = join(root, "descriptor-directory");
const oversized = join(root, "oversized.pb");
await mkdir(directory);
await writeFile(oversized, "");
await truncate(oversized, 32 * 1024 * 1024 + 1);
for (const [path, reason] of [
[join(root, "missing.pb"), "invalid_value"],
[directory, "invalid_format"],
[oversized, "out_of_range"],
] as const) {
const response = await call("inspect_web_network_capture", {
...args,
sensitive_values: ["unrelated fixture exclusion"],
protobuf: {
schema: {
descriptor_set_path: path,
message_type: "capture.Message",
},
},
});
const error = parseMcpToolError(response).error;
expect(error.code).toBe("invalid_request");
expect(error.details).toMatchObject({
issues: [
{ path: ["protobuf", "schema", "descriptor_set_path"], reason },
],
});
expect(
retainedCaptureDetailsSchema.parse(error.details).partial_observation
.kind,
).toBe("retained-evidence");
}
},
);
captureTest(
"decodes selected capture bytes with a supplied descriptor and pages exported facts after both sources disappear",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-protobuf-cli-");
const capture = join(root, "capture.har");
const descriptor = join(root, "capture.pb");
const bundle = join(root, "evidence.json");
await writeFile(capture, protobufHar());
await writeFile(descriptor, protobufCaptureDescriptor());
const original = await cli.run({
arguments: ["inspect-web-network-capture", capture, "har", "--json"],
});
expect(original.exitCode, original.stderr).toBe(0);
expect(
webNetworkCaptureSchema.parse(
parseEvidence(original.json).normalized_result,
).records[0],
).not.toHaveProperty("protobuf");
const decoded = await cli.run({
arguments: [
"inspect-web-network-capture",
capture,
"har",
"--record",
"0",
"--protobuf-schema",
descriptor,
"--protobuf-message",
"capture.Message",
"--detail",
"summary",
"--evidence_output",
bundle,
"--json",
],
});
expect(decoded.exitCode, decoded.stdout + decoded.stderr).toBe(0);
const loaded = await readEvidenceBundle(bundle);
if (!loaded.ok) throw loaded.error;
const parent = loaded.value.records.find(
(record) => record.operation === "inspect_web_network_capture",
);
if (parent === undefined)
throw new Error("Complete capture Evidence must be retained");
const report = webNetworkCaptureSchema.parse(parent.normalized_result);
expect(report.records[0]?.protobuf?.[0]).toMatchObject({
status: "complete",
schema: { message_type: "capture.Message" },
});
expect(report.records[0]?.protobuf?.[0]?.fields[0]).toMatchObject({
schema_name: "sentence",
schema_value: "hello",
field_id: "offset:5",
});
await unlink(capture);
await unlink(descriptor);
const request = join(root, "view.json");
await writeFile(
request,
JSON.stringify({
source: { kind: "inline", evidence: parent },
view: {
kind: "protobuf",
collection: "fields",
field_ids: ["offset:5"],
fields: ["schema_name", "schema_value"],
offset: 0,
limit: 1,
},
}),
);
const viewed = await cli.run({
arguments: ["inspect-analysis-view", request, "--json"],
});
expect(viewed.exitCode, viewed.stdout + viewed.stderr).toBe(0);
const view = analysisViewResultSchema.parse(
parseEvidence(viewed.json).normalized_result,
);
expect(view).toMatchObject({
kind: "protobuf",
parent_evidence_id: parent.evidence_id,
source_status: "complete",
coverage: { total: 1, examined: 1, exhausted: true },
items: [
{
record_ordinal: 0,
body_pointer: "/response/content/text",
field_id: "offset:5",
schema_name: "sentence",
schema_value: "hello",
},
],
});
expect(JSON.stringify(view)).not.toContain("content_base64");
expect((await readFile(bundle)).length).toBeGreaterThan(0);
},
);
captureTest(
"uses production MCP schemas, retained pages and CLI-equivalent decoding without reopening sources",
async ({ cli, mcp }) => {
const { call, args, capture, descriptor } = await prepareMcpCapture(
mcp.call,
);
const complete = await call("inspect_web_network_capture", args);
expect(complete.isError, JSON.stringify(complete)).not.toBe(true);
const parent = parseEvidence(complete.structuredContent);
const cliResult = await cli.run({
arguments: [
"inspect-web-network-capture",
capture,
"har",
"--record",
"0",
"--protobuf-schema",
descriptor,
"--protobuf-message",
"capture.Message",
"--json",
],
});
expect(cliResult.exitCode, cliResult.stdout + cliResult.stderr).toBe(0);
expect(parseEvidence(cliResult.json).evidence_id).toBe(parent.evidence_id);
const redacted = await call("inspect_web_network_capture", {
...args,
sensitive_values: ["hello"],
});
expect(redacted.isError, JSON.stringify(redacted)).not.toBe(true);
const excluded = webNetworkCaptureSchema.parse(
parseEvidence(redacted.structuredContent).normalized_result,
);
expect(excluded.records[0]?.protobuf).toBeUndefined();
expect(excluded.protobuf_coverage).toMatchObject({
status: "partial",
selected_records: 1,
inspected_records: 1,
});
expect(JSON.stringify(redacted)).not.toContain("hello");
const summary = await call("inspect_web_network_capture", {
...args,
detail: "summary",
});
expect(summary.isError, JSON.stringify(summary)).not.toBe(true);
const result = analysisViewResultSchema.parse(
parseEvidence(summary.structuredContent).normalized_result,
);
expect(result).toMatchObject({
kind: "summary",
parent_evidence_id: parent.evidence_id,
});
const invalid = await call("inspect_web_network_capture", {
...args,
record_ordinals: undefined,
});
expect(invalid.isError).toBe(true);
expect(parseMcpToolError(invalid).error.code).toBe("invalid_request");
await unlink(capture);
await unlink(descriptor);
const viewed = await call("inspect_analysis_view", {
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
view: {
kind: "protobuf",
collection: "fields",
field_numbers: [2],
fields: ["schema_name", "schema_value"],
offset: 0,
limit: 1,
},
});
expect(viewed.isError, JSON.stringify(viewed)).not.toBe(true);
expect(
analysisViewResultSchema.parse(
parseEvidence(viewed.structuredContent).normalized_result,
),
).toMatchObject({
kind: "protobuf",
items: [{ schema_name: "values", schema_value: '["1","2"]' }],
coverage: { total: 1, exhausted: true },
});
},
);
const trailerCases = [
["valid", "grpc-status", "0", "complete"],
["uppercase", "Grpc-Status", "0", "partial"],
["nul", "grpc-status", "0\u0000bad", "partial"],
["lf", "grpc-status", "0\nbad", "partial"],
["cr", "grpc-status", "0\rbad", "partial"],
["vt", "grpc-status", "0\u000bbad", "partial"],
["del", "grpc-status", "0\u007fbad", "partial"],
] as const;
captureTest.for(
(["grpc-web", "grpc-web-text"] as const).flatMap((framing) =>
trailerCases.map(([name, headerName, value, status]) => ({
framing,
name,
headerName,
value,
status,
})),
),
)(
"preserves $framing trailer case $name through CLI and MCP without changing capture bytes",
async ({ framing, name, headerName, value, status }, { cli, mcp }) => {
const root = await createTestTempDirectory("rea-grpc-web-trailers-");
const path = join(root, `${framing}-${name}.har`);
const trailer = Buffer.from(`${headerName}: ${value}\r\n`);
const body = Buffer.concat([
protobufFrame(Buffer.from([8, 1])),
protobufFrame(trailer, 128),
]);
const original = protobufHar(
framing === "grpc-web-text" ? Buffer.from(body.toString("base64")) : body,
`application/${framing}`,
);
await writeFile(path, original);
const response = await cli.run({
arguments: [
"inspect-web-network-capture",
path,
"har",
"--record",
"0",
"--protobuf-framing",
"auto",
"--json",
],
});
expect(response.exitCode, response.stdout + response.stderr).toBe(0);
const evidence = parseEvidence(response.json);
const report = webNetworkCaptureSchema.parse(evidence.normalized_result);
expect(report.protobuf_coverage?.status, name).toBe(status);
expect(report.records[0]?.protobuf?.[0]).toMatchObject({
status,
framing,
examined_bytes: body.length,
frames: [
{ kind: "data", status: "complete", examined_bytes: 2 },
{ kind: "trailer", status, examined_bytes: trailer.length },
],
fields: [{ number: 1, wire_type: "varint", value: "1" }],
});
const reply = await mcp.call("inspect_web_network_capture", {
capture_path: path,
format: "har",
record_ordinals: [0],
protobuf: { framing: "auto" },
});
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
expect(reply.structuredContent).toEqual(response.json);
expect(await readFile(path, "utf8")).toBe(original);
},
);