mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
3b698645a0
Validate completed reports before cancellation projection, retain valid Evidence and captured command diagnostics, and convert raw cleanup partials at their application owner. Record partial Evidence consistently across MCP handlers. Keep page inspection separate from unfinished bundle analysis. Refs #4261
213 lines
7.4 KiB
TypeScript
213 lines
7.4 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";
|
|
import { completeProviderEvidence } from "./ProviderOperationExecution.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);
|
|
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 completeProviderEvidence(
|
|
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 }],
|
|
},
|
|
),
|
|
),
|
|
OPERATION,
|
|
options,
|
|
);
|
|
}
|
|
}
|