mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(captures): scope HAR HTTP interpretation to entries (#4351)
* fix(captures): scope HAR HTTP interpretation to entries * test(snapshot): preserve Evidence-scoped native workflow unknowns * test(process): assert owned snapshot cancellation outcomes * test(process): preserve snapshot race causes and owned cleanup * test(cli): scope runtime cleanup provider injection CLI boundary tests share a module graph. The cleanup fixture's module mock cannot replace a provider factory already captured by the real command registration, leaving its close count at zero after another test imports it. Inject a typed provider factory into the registration instance, defaulting to the existing production factory. Both commands still create their own provider at execution time. Remove the redundant runtime context wrapper and the global mock state. Keep the real child CLI parity case and verify that neither command completion nor JSON output precedes awaited cleanup; retain the close-count, cleanup error and completed Evidence assertions. The unchanged test passes alone but fails with the command module preloaded. Both modes pass after the correction. Seven related files pass 28 tests; check and docs:check also pass on Node24.18.0/npm11.16.0 macOS arm64. * test(artifacts): use valid ASAR bytes for signature control * test(mcp): isolate snapshot filesystem fault injection * test(process): distinguish unacquired resources in failure progress * chore: restore main's copies of files the merge hook reformatted The pre-commit oxlint --fix touched two files this PR does not change; keep them identical to main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -270,6 +270,10 @@ not register or wrap the Fiddler MCP server.
|
||||
sensitive-value exclusions. Redacted byte fields retain neither original
|
||||
bytes nor their digest. Unknown extension names and token-looking query
|
||||
parameters do not establish sensitivity.
|
||||
HAR authentication fields, transport URLs and base64 content are interpreted
|
||||
only inside `log.entries`. Similarly named unknown fields in the document
|
||||
container remain opaque evidence; explicit sensitive-value declarations still
|
||||
apply to both scopes.
|
||||
|
||||
Mark sensitive literal string/UTF-8 byte values with MCP `sensitive_values`, or
|
||||
repeat CLI `--sensitive-value`. These declarations are never persisted in
|
||||
|
||||
@@ -1,9 +1,70 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { expect, it } from "vitest";
|
||||
import { historicalHar } from "../../../tests/fixtures/historicalHar.js";
|
||||
import {
|
||||
historicalHar,
|
||||
harWithContainerLookalikes,
|
||||
} from "../../../tests/fixtures/historicalHar.js";
|
||||
import { decodeCaptureText } from "./CaptureJson.js";
|
||||
import { decodeHarCapture } from "./HarCapture.js";
|
||||
|
||||
it("interprets HTTP fields only within HAR entries, preserving document lookalikes", () => {
|
||||
const fixture = harWithContainerLookalikes();
|
||||
const decoded = decodeHarCapture(JSON.stringify(fixture), []);
|
||||
expect(decoded.container.reported).toMatchObject({
|
||||
request: fixture.request,
|
||||
response: fixture.response,
|
||||
});
|
||||
expect(decoded.container.redactions).toEqual([]);
|
||||
expect(decoded.records[0]).toMatchObject({
|
||||
reported: {
|
||||
request: {
|
||||
url: "https://example.test/opaque",
|
||||
headers: [{ name: "Authorization", value: null }],
|
||||
cookies: [{ name: "session", value: null }],
|
||||
},
|
||||
response: {
|
||||
redirectURL: "https://example.test/redirect",
|
||||
headers: [{ name: "Set-Cookie", value: null }],
|
||||
},
|
||||
},
|
||||
binary_fields: [
|
||||
{ pointer: "/response/content/text", content_base64: "QUJD", bytes: 3 },
|
||||
],
|
||||
});
|
||||
const marked = decodeHarCapture(JSON.stringify(fixture), [
|
||||
"ordinary-root-evidence",
|
||||
]);
|
||||
expect(marked.container.reported).toMatchObject({
|
||||
request: {
|
||||
headers: [{ value: null }],
|
||||
cookies: [{ value: null }],
|
||||
},
|
||||
response: { headers: [{ value: null }], content: { text: null } },
|
||||
});
|
||||
expect(marked.container.redactions).toHaveLength(4);
|
||||
expect(
|
||||
marked.container.redactions.every(
|
||||
({ reason }) => reason === "explicit-sensitive-value",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not decode container lookalikes when excluding explicitly sensitive body bytes", () => {
|
||||
const fixture = harWithContainerLookalikes();
|
||||
fixture.response.content.text = "QUJD";
|
||||
const decoded = decodeHarCapture(JSON.stringify(fixture), ["ABC"]);
|
||||
expect(decoded.container.reported).toMatchObject({
|
||||
response: { content: { text: "QUJD" } },
|
||||
});
|
||||
expect(decoded.container.redactions).toEqual([]);
|
||||
expect(decoded.records[0]).toMatchObject({
|
||||
reported: { response: { content: { text: null } } },
|
||||
binary_fields: [
|
||||
{ state: "redacted", content_base64: null, bytes: null, sha256: null },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ values: [] },
|
||||
{ values: ["response"] },
|
||||
|
||||
@@ -32,7 +32,7 @@ export const decodeHarCapture = (
|
||||
sensitiveValues: readonly string[],
|
||||
): DecodedCapture => {
|
||||
const raw = parseCaptureJson(text, "HAR");
|
||||
const validation = projectHar(raw, [], false);
|
||||
const validation = projectHar(raw, [], "validation");
|
||||
const adaptedPlaywrightEntries = new Set(
|
||||
createHarValidator()(validation.reported),
|
||||
);
|
||||
@@ -55,11 +55,11 @@ export const decodeHarCapture = (
|
||||
const container = projectHar(
|
||||
{ ...raw, log: { ...raw.log, entries: null } },
|
||||
sensitiveValues,
|
||||
true,
|
||||
"container",
|
||||
);
|
||||
const records = entries.map((entry, ordinal): WebNetworkCaptureRecord => {
|
||||
// Validate each original record even when a structural ancestor excludes it.
|
||||
const projected = projectHar(entry, sensitiveValues, true);
|
||||
const projected = projectHar(entry, sensitiveValues, "entry");
|
||||
const projection: Projection = recordsExcluded
|
||||
? {
|
||||
reported: null,
|
||||
@@ -102,12 +102,13 @@ export const decodeHarCapture = (
|
||||
};
|
||||
};
|
||||
|
||||
/** Evidence projection when `redact` is true; the upstream schema validation copy otherwise. */
|
||||
/** Relative HTTP paths belong only to entries, never similarly named container extensions. */
|
||||
const projectHar = (
|
||||
value: unknown,
|
||||
sensitiveValues: readonly string[],
|
||||
redact: boolean,
|
||||
scope: "validation" | "container" | "entry",
|
||||
): Projection => {
|
||||
const redact = scope !== "validation";
|
||||
const redactor = new CaptureRedaction(sensitiveValues);
|
||||
const numeric_literals: Projection["numeric_literals"] = [];
|
||||
const binary_fields: Projection["binary_fields"] = [];
|
||||
@@ -136,7 +137,7 @@ const projectHar = (
|
||||
pointer,
|
||||
);
|
||||
if (
|
||||
redact &&
|
||||
scope === "entry" &&
|
||||
parent !== undefined &&
|
||||
redactor.harCredential(pointer, parent)
|
||||
)
|
||||
@@ -144,7 +145,7 @@ const projectHar = (
|
||||
if (item instanceof LosslessNumber) return projectNumber(item, pointer);
|
||||
if (typeof item === "string") {
|
||||
if (
|
||||
redact &&
|
||||
scope === "entry" &&
|
||||
pointer === "/response/content/text" &&
|
||||
parent?.encoding === "base64"
|
||||
) {
|
||||
@@ -191,7 +192,7 @@ const projectHar = (
|
||||
? redactor.text(
|
||||
item,
|
||||
pointer,
|
||||
redactor.harTransportUrl(pointer, parent),
|
||||
scope === "entry" && redactor.harTransportUrl(pointer, parent),
|
||||
)
|
||||
: item;
|
||||
}
|
||||
|
||||
@@ -42,7 +42,68 @@ const cli = async (
|
||||
throw cause;
|
||||
}
|
||||
};
|
||||
import { historicalHar } from "../../fixtures/historicalHar.js";
|
||||
import {
|
||||
historicalHar,
|
||||
harWithContainerLookalikes,
|
||||
} from "../../fixtures/historicalHar.js";
|
||||
|
||||
it("preserves opaque HAR container fields through CLI and MCP", async () => {
|
||||
const root = await createTestTempDirectory("rea-har-container-fields-");
|
||||
const path = join(root, "capture.har");
|
||||
const fixture = harWithContainerLookalikes();
|
||||
const original = JSON.stringify(fixture);
|
||||
await writeFile(path, original);
|
||||
const { call } = await connectLocalToolsMcp();
|
||||
for (const marked of [false, true]) {
|
||||
const sensitive = marked ? ["ordinary-root-evidence"] : [];
|
||||
const fromCli = await cli(
|
||||
path,
|
||||
"har",
|
||||
marked ? ["--sensitive-value", sensitive[0]!] : [],
|
||||
);
|
||||
expect(fromCli.ok, fromCli.stdout).toBe(true);
|
||||
const cliEvidence = parseEvidence(JSON.parse(fromCli.stdout));
|
||||
const response = await call("inspect_web_network_capture", {
|
||||
capture_path: path,
|
||||
format: "har",
|
||||
sensitive_values: sensitive,
|
||||
});
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
const evidence = parseEvidence(response.structuredContent);
|
||||
expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
|
||||
expect(evidence.normalized_result).toEqual(cliEvidence.normalized_result);
|
||||
const capture = webNetworkCaptureSchema.parse(evidence.normalized_result);
|
||||
expect(capture.container.reported).toMatchObject({
|
||||
request: {
|
||||
url: fixture.request.url,
|
||||
headers: [{ value: marked ? null : "ordinary-root-evidence" }],
|
||||
cookies: [{ value: marked ? null : "ordinary-root-evidence" }],
|
||||
},
|
||||
response: {
|
||||
redirectURL: fixture.response.redirectURL,
|
||||
headers: [{ value: marked ? null : "ordinary-root-evidence" }],
|
||||
content: { text: marked ? null : "ordinary-root-evidence" },
|
||||
},
|
||||
});
|
||||
expect(capture.records[0]).toMatchObject({
|
||||
reported: {
|
||||
request: {
|
||||
url: "https://example.test/opaque",
|
||||
headers: [{ value: null }],
|
||||
cookies: [{ value: null }],
|
||||
},
|
||||
response: {
|
||||
redirectURL: "https://example.test/redirect",
|
||||
headers: [{ value: null }],
|
||||
},
|
||||
},
|
||||
binary_fields: [
|
||||
{ pointer: "/response/content/text", content_base64: "QUJD", bytes: 3 },
|
||||
],
|
||||
});
|
||||
}
|
||||
expect(await readFile(path, "utf8")).toBe(original);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ encoded: "1e3", literal: "1000" },
|
||||
|
||||
Vendored
+40
@@ -35,6 +35,46 @@ export const historicalHar = () => ({
|
||||
},
|
||||
});
|
||||
|
||||
/** Unknown document fields may resemble entry fields without owning HTTP semantics. */
|
||||
export const harWithContainerLookalikes = () => {
|
||||
const fixture = historicalHar();
|
||||
const entry = fixture.log.entries[0]!;
|
||||
const request = {
|
||||
...entry.request,
|
||||
url: "https://user:password@example.test/opaque",
|
||||
headers: [{ name: "Authorization", value: "ordinary-root-evidence" }],
|
||||
cookies: [{ name: "session", value: "ordinary-root-evidence" }],
|
||||
};
|
||||
const response = {
|
||||
...entry.response,
|
||||
redirectURL: "https://user:password@example.test/redirect",
|
||||
headers: [{ name: "Set-Cookie", value: "ordinary-root-evidence" }],
|
||||
content: {
|
||||
...entry.response.content,
|
||||
encoding: "base64",
|
||||
text: "ordinary-root-evidence",
|
||||
},
|
||||
};
|
||||
return {
|
||||
...fixture,
|
||||
request,
|
||||
response,
|
||||
log: {
|
||||
...fixture.log,
|
||||
entries: [
|
||||
{
|
||||
...entry,
|
||||
request,
|
||||
response: {
|
||||
...response,
|
||||
content: { ...response.content, text: "QUJD" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/** Request paths exercising only the public API surface format rules. */
|
||||
export const apiSurfaceHar = () => {
|
||||
const fixture = historicalHar();
|
||||
|
||||
Reference in New Issue
Block a user