fix(capture): retain safe caller path selections (#9626)

* fix(cli)!: retain repeated WASM input paths

BREAKING CHANGE: caller_input_paths values may now be ordered arrays of strings as well as strings. Clients must accept both forms.

* fix(capture): retain safe caller path selections

* fix(evidence): retain auxiliary selections in direct summaries

* test(wasm): separate SDK schema and acquired-input coverage
This commit is contained in:
morluto authored and GitHub committed 2026-11-09 20:12:45 +08:00
1 parent dda2d40295
commit bf271ba3fb
10 files changed
+326 -130

No files matched your search

+70 -58
View File
@@ -10,7 +10,11 @@ import {
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { createAnalysisProfile } from "../domain/analysisProfile.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import {
createEvidence,
withEvidenceCallerInputPath,
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";
@@ -39,6 +43,7 @@ export class WebNetworkCaptureService {
async inspect(
rawInput: unknown,
options?: ExecutionOptions,
callerPaths?: { readonly capture: string; readonly descriptor?: string },
): Promise<Result<Evidence, AnalysisError>> {
if (options?.signal?.aborted)
return err(
@@ -87,7 +92,11 @@ export class WebNetworkCaptureService {
);
let partialFailure: AnalysisError | undefined;
if (partial.success) {
const projected = this.#projectCapture(input, partial.data);
const projected = this.#projectCapture(
input,
partial.data,
callerPaths,
);
if (projected.ok) loaded.error.partialObservation = projected.value;
else partialFailure = projected.error;
} else if (isCapturePartialCandidate(loaded.error.partialObservation)) {
@@ -128,7 +137,7 @@ export class WebNetworkCaptureService {
"Capture adapter returned malformed historical evidence.",
),
);
const projected = this.#projectCapture(input, report.data);
const projected = this.#projectCapture(input, report.data, callerPaths);
if (!projected.ok) return failure(projected.error);
return completeProviderEvidence(projected.value, options?.signal);
}
@@ -136,6 +145,7 @@ export class WebNetworkCaptureService {
#projectCapture(
input: InspectWebNetworkCaptureInput,
value: WebNetworkCapture,
callerPaths?: { readonly capture: string; readonly descriptor?: string },
): Result<Evidence, AnalysisError> {
const artifactSha256 = value.artifact.sha256;
if (artifactSha256 === null)
@@ -160,6 +170,8 @@ export class WebNetworkCaptureService {
const safeValue = excludeCaptureCoordinates(value, input.sensitive_values);
const redactText = (text: string): string =>
redactExplicitText(text, input.sensitive_values);
const isSensitive = (text: string): boolean =>
input.sensitive_values.some((literal) => text.includes(literal));
const selected =
input.record_ordinals ?? value.records.map(({ ordinal }) => ordinal);
const records = [];
@@ -189,16 +201,8 @@ export class WebNetworkCaptureService {
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 safePath = isSensitive(input.capture_path) ? "" : input.capture_path;
const safeSha256 = isSensitive(artifactSha256) ? null : artifactSha256;
const subjectUnavailableReason = redactText(
"The explicitly sensitive artifact digest is excluded; its cryptographic identity is unknown in this Evidence.",
);
@@ -212,11 +216,7 @@ export class WebNetworkCaptureService {
].map(redactText);
const parameters = jsonObjectSchema.parse({
capture_path: safePath,
...(input.sensitive_values.some((literal) =>
input.format.includes(literal),
)
? {}
: { format: input.format }),
...(isSensitive(input.format) ? {} : { format: input.format }),
record_ordinals: selected,
...(input.protobuf === undefined
? {}
@@ -247,48 +247,60 @@ export class WebNetworkCaptureService {
? {}
: { capture_timeout_ms: input.capture_timeout_ms }),
});
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,
const evidence = createEvidence(
safeSha256 === null
? undefined
: {
path: safePath,
sha256: safeSha256,
format: "file",
},
...(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 }],
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 }],
},
);
if (callerPaths === undefined) return ok(evidence);
const safeCallerPath = (path: string | undefined) =>
path === undefined || isSensitive(path) ? undefined : path;
const callerPath = safeCallerPath(callerPaths.capture);
const descriptor = safeCallerPath(callerPaths.descriptor);
return ok(
withEvidenceCallerInputPath(
evidence,
callerPath,
descriptor === undefined
? undefined
: { descriptor_set_path: descriptor },
),
);
}