mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
* refactor(errors): read cleanup completion from dependencies instead of pushing it to dependents AnalysisError.cleanupIncomplete becomes a getter over the error's own unresolved state and its bound dependencies. The WeakRef dependents graph and resolveCleanup's push propagation are removed, and subclasses mark their initial state through markCleanupIncomplete instead of overriding a public mutable field. * fix(errors): settle bound dependencies before binding more A composite whose dependencies had released but whose status was never read stayed incomplete when a later dependency was bound. * refactor(errors): let errors read a lease group's release instead of resolving them (#10759) AnalysisCleanupLeaseGroup no longer calls resolveCleanup() on its error. The error holds the group as a release claim and reads cleanupReleased when asked, so completion follows the lease owner without a push. * chore(verify): count resolveCleanup and bindCleanupDependencies calls so they only shrink (#10763) * refactor(errors): let errors read a lease group's release instead of resolving them AnalysisCleanupLeaseGroup no longer calls resolveCleanup() on its error. The error holds the group as a release claim and reads cleanupReleased when asked, so completion follows the lease owner without a push. * chore(verify): count resolveCleanup and bindCleanupDependencies calls so they only shrink * refactor(process): let worker cleanup errors read their root epoch's release OwnedWorkerRuntime's root epoch is now an AnalysisCleanupReleaseClaim. Errors bound to it read its release when asked instead of the epoch pushing resolveCleanup() through weak references, and a runtime without a retained root binds an already-released owner. Removes the OwnedWorkerCleanupFailure structural type and four resolveCleanup() pushes; the counted baseline shrinks accordingly. * refactor(pcap): let pcap cleanup errors read command and snapshot release (#10792) Retained pcap commands, retained snapshot failures and snapshot completion descriptors now own their release and errors read it: commands and snapshot failures implement AnalysisCleanupReleaseClaim, and a failed completion-descriptor close is retried through an AnalysisCleanupLease bound to its error. Removes four resolveCleanup() pushes; the counted baseline shrinks accordingly. * refactor(process): let Darwin helper cleanup errors read their owner's release Darwin helper cache and run-token reader root cleanup now record an AnalysisCleanupRelease that every failure reports; a successful retry confirms it. Replaces the first-failure anchor that later failures depended on and the resolveCleanup() push. Removes two resolveCleanup() and two bindCleanupDependencies() calls; both counted baselines shrink. * refactor(ghidra): let Ghidra cleanup errors read the client's and checkpoint's release GhidraClient and GhidraCheckpoint bind each cleanup failure to an AnalysisCleanupRelease and confirm it when a later close or retry releases everything, instead of keeping a set of failures and pushing resolveCleanup() to each. Removes two resolveCleanup() calls; the counted baseline shrinks. * refactor(process): let debugger session cleanup errors read a confirmed stop OwnedDebuggerSessionRegistry binds each retained gdb or rizin cleanup failure to the session's AnalysisCleanupRelease and confirms it when a stop is confirmed, keeping the latest failure for retry reporting, instead of pushing resolveCleanup() to a set of failures. Removes one resolveCleanup() call; the counted baseline shrinks. * refactor(process): drop the debugger registry's unused closeOperation option Returning the latest retained failure removed the last reader of closeOperation; remove the option and the values gdb and rizin supplied. * refactor(server): let environment shutdown errors read a successful close serveEnvironment binds each shutdown failure to an AnalysisCleanupRelease that a later successful close confirms, instead of pushing resolveCleanup() to a set of failures. The state-file retry test now also requires the earlier report to read the release. Removes one resolveCleanup() call; the counted baseline shrinks. * refactor(desktop): release retained screen-judge cleanup through its lease Each retained judge or screenshot cleanup owner is wrapped in an AnalysisCleanupLease bound to its failure; close() releases the lease, which records the release the failure reads, instead of closing the owner and pushing resolveCleanup(). Removes one resolveCleanup() call; the counted baseline shrinks. * refactor(ida): let IDA close failures read the session's later close IdaSessionClient's close failure binds an owner that is both its resource owner and its release claim, read from the client's closed state, instead of keeping weak references and pushing resolveCleanup() after a successful retry. Removes one resolveCleanup() call; the counted baseline shrinks. * refactor(javascript): let source-map codec cleanup errors read a later close (#10814) * refactor(javascript): let source-map codec cleanup errors read a later close OwnedCodec binds each close failure to an AnalysisCleanupRelease that a later successful close confirms, replacing the first-failure anchor that later failures depended on and the resolveCleanup() push. Removes one resolveCleanup() and one bindCleanupDependencies() call; both counted baselines shrink. * refactor(cutter): let pending Cutter command errors read the completion receipt (#10815) * refactor(cutter): let pending Cutter command errors read the completion receipt Each pending Cutter command keeps an AnalysisCleanupRelease bound to its timeout failure; the producer's terminal completion receipt confirms it instead of pushing resolveCleanup(). Removes one resolveCleanup() call; the counted baseline shrinks. * refactor(application): let provider candidate cleanup errors read the candidate's close (#10817) * refactor(application): let provider candidate cleanup errors read the candidate's close AnalysisProviderRegistry keeps one AnalysisCleanupRelease per candidate whose resource close is unconfirmed; failures bind to it and a later confirmed close confirms it, instead of pushing resolveCleanup() to a set of failures. Removes one resolveCleanup() call; the counted baseline shrinks. * fix(errors): bind a cleanup release claim to an error at most once AnalysisError kept release claims in an array, so a provider whose retried close returned the same error bound the same claim again on every retry and each cleanupIncomplete read scanned the duplicates. Keep the claims in a set. * refactor(application): let reverse-tool cleanup errors read a verified stop (#10819) * refactor(application): let reverse-tool cleanup errors read a verified stop Each retained provider process group in ReverseEngineeringService keeps an AnalysisCleanupRelease; its command and retry failures bind to it and a later verified stop confirms it, instead of collecting failures and pushing resolveCleanup(). Removes one resolveCleanup() call; the counted baseline shrinks. * refactor(sqlite): let SQLite worker and root cleanup errors read their release (#10821) Each retained SQLite snapshot cleanup records an AnalysisCleanupRelease for its worker and for its runtime root; the resource failures bind to them and a verified stop or root removal confirms them, instead of pushing resolveCleanup(). Composite failures keep reading those resource failures. Removes two resolveCleanup() calls; the counted baseline shrinks. * refactor(android): let JADX cleanup errors read their retained cleanup's release Each retained JADX cleanup records an AnalysisCleanupRelease for the admission failure's descriptor owners and one for the whole retained cleanup. The admission failure and every error the cleanup reports bind to them, and a confirmed close confirms them, instead of pushing resolveCleanup() through WeakRefs to reported errors. Removes two resolveCleanup() calls; the counted baseline shrinks. * refactor(application): let session close failures read the client's later close A retained session cleanup records an AnalysisCleanupRelease; unowned cleanup failures the client's close reported bind to it and the successful close confirms it, instead of the session collecting them and pushing resolveCleanup(). Failures that already follow their own owner keep reading it. Adds a regression for an unowned close failure. Removes one resolveCleanup() call; the counted baseline shrinks. * refactor(desktop): let build-computer and agentenv cleanup errors read their owner's release Build-computer teardown, retained task readers, remote reader folders and agentenv solve sandboxes each record an AnalysisCleanupRelease that their bound failures read; a verified teardown or stop confirms it instead of pushing resolveCleanup() through WeakRef sets. Long-run job failures read whether every job they name has confirmed its stop, so a failure naming two jobs no longer reads complete when only one stops (regression added). WeakRef sets kept only where late facts (completion receipts, reader views) are still retained as diagnostics. Removes six resolveCleanup() calls; the counted baseline shrinks. * refactor(kuna): let source-map and Kuna cache cleanup errors read their owner's release (#10856) * refactor(browser): let late source-map fetch errors read their response's release A pending source-map fetch and a response cancellation each record an AnalysisCleanupRelease that their cancellation or deadline failure reads; releasing the late response or the reader lock confirms it instead of pushing resolveCleanup(). Removes two resolveCleanup() calls; the counted baseline shrinks. * refactor(kuna): let project-cache cleanup errors read each owner's release Each retained Kuna resource owner (process, staging, hash descriptor, retired file) records a release that the errors reporting it read; an error completes once every owner it reports confirms, and retirement that moves staging to a retained file forwards the staging owner's release to the file's. Replaces the WeakRef report list, its pruning and the resolveCleanup() push. Removes one resolveCleanup() call; the counted baseline shrinks. * refactor(process): forbid resolveCleanup() pushes; native markers read their supervisor's release (#10857) * refactor(providers): let an attributed client failure read its source's completed cleanup Re-attributing a failure whose source cleanup already completed now binds a claim reading that source's release instead of pushing resolveCleanup() onto the new error; a pending source is still bound as a dependency. Resources stay as history, as before. Removes one resolveCleanup() call; the counted baseline shrinks. * refactor(process): let native command markers read their supervisor's release ProviderProcessSupervisor records an AnalysisCleanupRelease that a stop proving the process ended confirms. Native command and LLDB resolver cleanup markers bind to their supervisor's release, so a verified stop by either owner completes them, instead of each owner pushing resolveCleanup() onto the markers it tracks. Removes the last two resolveCleanup() calls outside the error base; the counted baseline is empty. * chore(verify): forbid resolveCleanup() pushes outside the error base Every production owner now confirms a release its errors read, so the counted baseline is empty. The guard becomes an outright ban with no baseline entry. * test(native): complete native command and LLDB markers through a direct supervisor stop * refactor(errors): complete an error's cleanup from its owner's record and every dependency (#10892) * refactor(errors): complete an error's cleanup from its owner's record and every dependency * refactor(errors): complete test errors through an owner release and delete resolveCleanup() (#10898) * refactor(android): compose JADX and Apktool cleanup failures through one shared owner (#10900) * refactor(android): compose JADX and Apktool cleanup failures through one shared owner * refactor(application): compose service cleanup failures through the shared owner (#10902)
323 lines
12 KiB
TypeScript
323 lines
12 KiB
TypeScript
import { redactExplicitFailure } from "../domain/explicitSensitiveFailure.js";
|
|
import { excludeCaptureCoordinates } from "../domain/webNetworkCaptureCoordinates.js";
|
|
import { redactExplicitText } from "../domain/explicitSensitiveValues.js";
|
|
import type { ExecutionOptions } from "./AnalysisProvider.js";
|
|
import type { WebNetworkCapturePort } from "./WebNetworkCapturePort.js";
|
|
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import {
|
|
AnalysisInputError,
|
|
AnalysisOutputError,
|
|
cancelledBy,
|
|
} from "../domain/analysisErrorCore.js";
|
|
import { createAnalysisProfile } from "../domain/analysisProfile.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";
|
|
import {
|
|
inspectWebNetworkCaptureInputSchema,
|
|
captureSensitiveValues,
|
|
webNetworkCaptureSchema,
|
|
type InspectWebNetworkCaptureInput,
|
|
type WebNetworkCapture,
|
|
} from "../domain/webNetworkCapture.js";
|
|
import { completeProviderEvidence } from "./ProviderOperationExecution.js";
|
|
import { analysisErrorWithCleanupDependencies } from "../domain/analysisErrorCleanup.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,
|
|
callerPaths?: { readonly capture: string; readonly descriptor?: string },
|
|
): Promise<Result<Evidence, AnalysisError>> {
|
|
if (options?.signal?.aborted)
|
|
return err(cancelledBy(OPERATION, options.signal));
|
|
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 (
|
|
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.",
|
|
},
|
|
]),
|
|
);
|
|
if (input.protobuf !== undefined && input.record_ordinals === undefined)
|
|
return failure(
|
|
new AnalysisInputError(OPERATION, undefined, [
|
|
{
|
|
path: ["record_ordinals"],
|
|
reason: "invalid_format",
|
|
message:
|
|
"Protobuf options require explicit producer record ordinals.",
|
|
},
|
|
]),
|
|
);
|
|
const loaded = await this.port.inspect(input, options);
|
|
if (!loaded.ok) {
|
|
const partial = webNetworkCaptureSchema.safeParse(
|
|
loaded.error.partialObservation,
|
|
);
|
|
let partialFailure: AnalysisError | undefined;
|
|
if (partial.success) {
|
|
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)) {
|
|
partialFailure = new AnalysisOutputError(
|
|
OPERATION,
|
|
"Capture adapter retained malformed historical evidence.",
|
|
{ cause: partial.error },
|
|
);
|
|
}
|
|
if (partialFailure !== undefined) {
|
|
const rejected = loaded.error.partialObservation;
|
|
for (
|
|
let owner: AnalysisError | undefined = loaded.error;
|
|
owner !== undefined;
|
|
owner = owner.cleanupPrimary
|
|
) {
|
|
if (owner.partialObservation === rejected)
|
|
owner.partialObservation = undefined;
|
|
}
|
|
partialFailure.retainSecondaryFailure(loaded.error);
|
|
return failure(
|
|
loaded.error.cleanupIncomplete
|
|
? analysisErrorWithCleanupDependencies(
|
|
partialFailure,
|
|
loaded.error,
|
|
OPERATION,
|
|
)
|
|
: partialFailure,
|
|
);
|
|
}
|
|
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 projected = this.#projectCapture(input, report.data, callerPaths);
|
|
if (!projected.ok) return failure(projected.error);
|
|
return completeProviderEvidence(projected.value, options?.signal);
|
|
}
|
|
|
|
#projectCapture(
|
|
input: InspectWebNetworkCaptureInput,
|
|
value: WebNetworkCapture,
|
|
callerPaths?: { readonly capture: string; readonly descriptor?: string },
|
|
): Result<Evidence, AnalysisError> {
|
|
const artifactSha256 = value.artifact.sha256;
|
|
if (artifactSha256 === null)
|
|
return err(
|
|
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 err(
|
|
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 isSensitive = (text: string): boolean =>
|
|
input.sensitive_values.some((literal) => text.includes(literal));
|
|
const selected =
|
|
input.record_ordinals ?? value.records.map(({ ordinal }) => ordinal);
|
|
const records = [];
|
|
for (const [index, ordinal] of selected.entries()) {
|
|
const record = safeValue.records[ordinal];
|
|
// A selection past the capture's records is a caller range fault.
|
|
if (record === undefined)
|
|
return err(
|
|
new AnalysisInputError(OPERATION, undefined, [
|
|
value.total_records === 0
|
|
? {
|
|
path: ["record_ordinals", index],
|
|
reason: "invalid_value",
|
|
message: "The selected capture contains no records.",
|
|
}
|
|
: {
|
|
path: ["record_ordinals", index],
|
|
reason: "out_of_range",
|
|
minimum: 0,
|
|
maximum: value.total_records - 1,
|
|
message: `Producer ordinal ${ordinal} is outside the retained capture (${value.total_records} records).`,
|
|
},
|
|
]),
|
|
);
|
|
records.push({
|
|
...record,
|
|
limitations: record.limitations.map(redactText),
|
|
});
|
|
}
|
|
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.",
|
|
);
|
|
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,
|
|
...(isSensitive(input.format) ? {} : { format: input.format }),
|
|
record_ordinals: selected,
|
|
...(input.protobuf === undefined
|
|
? {}
|
|
: {
|
|
protobuf: {
|
|
framing: input.protobuf.framing,
|
|
...(input.protobuf.body_pointers === undefined
|
|
? {}
|
|
: {
|
|
body_pointers: input.protobuf.body_pointers.map(redactText),
|
|
}),
|
|
...(input.protobuf.schema === undefined
|
|
? {}
|
|
: {
|
|
schema: {
|
|
descriptor_set_path: redactText(
|
|
input.protobuf.schema.descriptor_set_path,
|
|
),
|
|
message_type: redactText(
|
|
input.protobuf.schema.message_type,
|
|
),
|
|
},
|
|
}),
|
|
},
|
|
}),
|
|
sensitive_value_count: input.sensitive_values.length,
|
|
...(input.max_input_bytes === undefined
|
|
? {}
|
|
: { max_input_bytes: input.max_input_bytes }),
|
|
...(input.max_reply_bytes === undefined
|
|
? {}
|
|
: { max_reply_bytes: input.max_reply_bytes }),
|
|
...(input.max_nesting_depth === undefined
|
|
? {}
|
|
: { max_nesting_depth: input.max_nesting_depth }),
|
|
...(input.capture_timeout_ms === undefined
|
|
? {}
|
|
: { capture_timeout_ms: input.capture_timeout_ms }),
|
|
});
|
|
const evidence = 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 }],
|
|
},
|
|
);
|
|
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 },
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
const isCapturePartialCandidate = (value: unknown): boolean => {
|
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
return false;
|
|
const candidate = value as Record<string, unknown>;
|
|
return (
|
|
"artifact" in candidate &&
|
|
["format", "records", "decoder"].some((field) => field in candidate)
|
|
);
|
|
};
|