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:
To3akaRin
2026-10-23 02:30:06 +08:00
committed by GitHub
parent 1684c73d4f
commit b4bc6cf3ee
5 changed files with 177 additions and 10 deletions
+4
View File
@@ -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
+62 -1
View File
@@ -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"] },
+9 -8
View File
@@ -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" },
+40
View File
@@ -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();