Files
reverse-engineering-anything/src/application/WebNetworkCaptureService.ts
T
morluto b955b825fc fix(artifacts): retain read cleanup ownership through CLI and MCP shutdown
Keep failed stable-read handles and WABT runtime roots under retryable resource scopes. Preserve cancellation causes and primary errors, close the same service instances bound to MCP tools, and consolidate CLI operation/cleanup projection.

Validation: fast typecheck and lint, cached runtime build, docs checks, SDK schema validation, focused service and real-file cleanup tests, CLI cleanup regressions, and MCP lifecycle/shutdown workflows. Provider process fixtures simulate engine output; real WABT and Linux EVM execution are not established by this pass.
2026-10-13 16:08:41 +08:00

204 lines
7.1 KiB
TypeScript

import { redactExplicitFailure } from "../domain/explicitSensitiveFailure.js";
import { excludeCaptureCoordinates } from "../domain/webNetworkCaptureCoordinates.js";
import { redactExplicitText } from "../domain/explicitSensitiveValues.js";
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "./AnalysisProvider.js";
import type { WebNetworkCapturePort } from "./WebNetworkCapturePort.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { createAnalysisProfile } from "../domain/analysisProfile.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { jsonObjectSchema } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import {
inspectWebNetworkCaptureInputSchema,
captureSensitiveValues,
webNetworkCaptureSchema,
} from "../domain/webNetworkCapture.js";
const OPERATION = "inspect_web_network_capture";
/** Shared CLI/MCP historical inspection preserves caller selection and producer record identity. */
export class WebNetworkCaptureService {
constructor(readonly port: WebNetworkCapturePort) {}
/** Release resources retained by the selected capture adapter. */
close(): Promise<void> {
return this.port.close?.() ?? Promise.resolve();
}
/** Return selected complete records inline, without inventing live browser attribution. */
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(OPERATION));
const failure = (error: AnalysisError): Result<never, AnalysisError> =>
err(redactExplicitFailure(error, captureSensitiveValues(rawInput)));
const parsed = inspectWebNetworkCaptureInputSchema.safeParse(rawInput);
if (!parsed.success)
return failure(
analysisInputErrorFromIssues(OPERATION, parsed.error.issues, rawInput, {
cause: parsed.error,
}),
);
const input = parsed.data;
if (!isAbsolute(input.capture_path))
return failure(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["capture_path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
if (
input.record_ordinals !== undefined &&
new Set(input.record_ordinals).size !== input.record_ordinals.length
)
return failure(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["record_ordinals"],
reason: "invalid_format",
message: "Each requested producer ordinal must occur once.",
},
]),
);
const loaded = await this.port.inspect(input, options);
if (!loaded.ok) return failure(loaded.error);
if (options?.signal?.aborted)
return failure(new AnalysisCancelledError(OPERATION));
const report = webNetworkCaptureSchema.safeParse(loaded.value);
if (!report.success)
return failure(
new AnalysisOutputError(
OPERATION,
"Capture adapter returned malformed historical evidence.",
),
);
const value = report.data;
const artifactSha256 = value.artifact.sha256;
if (artifactSha256 === null)
return failure(
new AnalysisOutputError(
OPERATION,
"Capture adapter omitted its observed artifact digest before explicit projection.",
),
);
if (
value.artifact.path !== input.capture_path ||
value.format !== input.format ||
value.total_records !== value.records.length ||
value.records.some((record, index) => record.ordinal !== index)
)
return failure(
new AnalysisOutputError(
OPERATION,
"Capture adapter changed the selected artifact, format or original record sequence.",
),
);
const safeValue = excludeCaptureCoordinates(value, input.sensitive_values);
const redactText = (text: string): string =>
redactExplicitText(text, input.sensitive_values);
const selected =
input.record_ordinals ?? value.records.map(({ ordinal }) => ordinal);
const records = [];
for (const ordinal of selected) {
const record = safeValue.records[ordinal];
if (record === undefined)
return failure(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["record_ordinals"],
reason: "invalid_format",
message: `Producer ordinal ${ordinal} is outside the retained capture (${value.total_records} records).`,
},
]),
);
records.push({
...record,
limitations: record.limitations.map(redactText),
});
}
const safePath = input.sensitive_values.some((literal) =>
input.capture_path.includes(literal),
)
? ""
: input.capture_path;
const safeSha256 = input.sensitive_values.some((literal) =>
artifactSha256.includes(literal),
)
? null
: artifactSha256;
const subjectUnavailableReason = redactText(
"The explicitly sensitive artifact digest is excluded; its cryptographic identity is unknown in this Evidence.",
);
const limitations = [
...safeValue.limitations,
...(safePath === ""
? [
"The explicitly sensitive artifact path is excluded; its size remains available.",
]
: []),
].map(redactText);
const parameters = jsonObjectSchema.parse({
capture_path: safePath,
...(input.sensitive_values.some((literal) =>
input.format.includes(literal),
)
? {}
: { format: input.format }),
record_ordinals: selected,
sensitive_value_count: input.sensitive_values.length,
});
return ok(
createEvidence(
safeSha256 === null
? undefined
: {
path: safePath,
sha256: safeSha256,
format: "file",
},
value.decoder,
{
operation: OPERATION,
parameters,
result: {
...safeValue,
artifact: { ...value.artifact, path: safePath, sha256: safeSha256 },
records,
limitations:
safeSha256 === null
? [...limitations, subjectUnavailableReason]
: limitations,
},
...(safeValue.format !== "qlog"
? {}
: {
analysisProfile: createAnalysisProfile(value.decoder, {
qlog_profile: safeValue.qlog.profile,
clock: safeValue.qlog.clock,
scope: safeValue.qlog.scope,
}),
}),
confidence: "observed",
authority: "historical-reference",
limitations,
...(safeSha256 === null ? { subjectUnavailableReason } : {}),
locations:
safePath === "" ? [] : [{ kind: "artifact-path", path: safePath }],
},
),
);
}
}