Files
rea/src/process/OwnedCommand.ts
T
2bf3663f65 feat(browser): inspect historical web network captures (#992)
* feat(browser): inspect retained HAR and mitmproxy capture evidence

Add independent offline format adapters, inline CLI/MCP evidence, bounded
owned decoder processes, and real producer verification.

Refs #882

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): preserve capture boundaries during cancellation and decoding

Honor cancellation during owned-command cleanup, retain opaque HAR
extension content, and clean verifier resources on every failure path.

Refs #882

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): exclude sensitive capture fields across representations

Normalize known native transport URLs for both bytes and strings, including backups. Exclude explicitly sensitive text fields with null so redaction markers cannot reintroduce declared literals. Cover both provider identities in catalog conformance.

Refs #882

Assisted-by: Yukari (gensokyo-skills:yukari-boundary-analysis)
Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)

* fix(browser): exclude sensitive capture identities and bound diagnostics

* fix(browser): validate excluded HAR subtrees before projection

* fix(browser): distinguish capture input limits from decoder failures

* fix(browser): preserve decoder failure categories

* fix(browser): validate excluded native credential values

* test(browser): report capture verifier assertions directly

* fix(browser): classify native representation failures precisely

* fix(browser): exclude URL userinfo after header whitespace

* fix(browser): preserve capture evidence and failure boundaries

* fix(browser): exclude sensitive literals from capture limitations

* test(browser): check limitation values without JSON syntax

* fix(browser): preserve acquisition integrity and exclude selection diagnostics

* fix(browser): preserve capture acquisition and declaration boundaries

* fix(mcp): project sensitive capture argument validation

* fix: preserve capture bytes and prelaunch cancellation

* fix(browser): exclude sensitive HAR record ancestry

* fix(browser): preserve transport URL and parameter exclusions

* fix(browser): preserve numeric redaction and canonical capture provenance

* fix(browser): cover derived capture values and ownership preparation deadlines

* fix(browser): exclude explicitly sensitive artifact digests

* chore: regenerate generated metadata

* fix(browser): reject lossy HAR member normalization

* test(browser): assert the advertised unsupported-profile outcome

* fix(browser): preserve JSON visitor nesting failure locations

* fix(browser): exclude explicitly sensitive native source ranges

---------

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-08 07:53:41 +08:00

199 lines
5.9 KiB
TypeScript

import type {
OwnedProviderProcessSpawnOptions,
ProviderProcessSnapshot,
SpawnedOwnedProviderProcess,
} from "./ProviderProcess.js";
import {
ProviderProcessSupervisor,
spawnOwnedProviderProcess,
} from "./ProviderProcess.js";
import { cleanupOwnedProcessGroup } from "./ProcessOwnership.js";
import {
ProviderStartupDeadline,
waitForAbortableDelay,
} from "./ProviderDeadline.js";
/** A short-lived command failed its lifecycle or complete-output contract. */
export class OwnedCommandFailure extends Error {
constructor(
readonly reason: "cancelled" | "timeout" | "output-limit" | "process",
message: string,
readonly snapshot: ProviderProcessSnapshot | null = null,
readonly cleanupFailure: string | null = null,
options?: ErrorOptions,
readonly resources: readonly string[] = [],
) {
super(message, options);
}
}
/** Execute a single owned process with independent cleanup and bounded diagnostic retention. */
export const runOwnedCommand = async (
spawn: OwnedProviderProcessSpawnOptions,
limits: { readonly timeoutMs: number; readonly diagnosticBytes: number },
options: {
readonly signal?: AbortSignal;
readonly launcher?: (
input: OwnedProviderProcessSpawnOptions,
) => Promise<SpawnedOwnedProviderProcess>;
} = {},
): Promise<ProviderProcessSnapshot> => {
const callerSignal = options.signal ?? spawn.signal;
if (callerSignal?.aborted)
throw new OwnedCommandFailure(
"cancelled",
"Command cancelled before launch.",
);
const deadline = new ProviderStartupDeadline(limits.timeoutMs, callerSignal);
try {
return await collectOwnedCommand(
spawn,
limits,
options.launcher ?? spawnOwnedProviderProcess,
deadline,
callerSignal,
);
} finally {
deadline.dispose();
}
};
const collectOwnedCommand = async (
spawn: OwnedProviderProcessSpawnOptions,
limits: { readonly diagnosticBytes: number },
launcher: (
input: OwnedProviderProcessSpawnOptions,
) => Promise<SpawnedOwnedProviderProcess>,
deadline: ProviderStartupDeadline,
callerSignal?: AbortSignal,
): Promise<ProviderProcessSnapshot> => {
const signal = deadline.signal;
let launched: SpawnedOwnedProviderProcess;
try {
launched = await launcher({
...spawn,
signal,
});
} catch (cause: unknown) {
const interruption = deadline.interruption;
if (interruption !== undefined)
throw new OwnedCommandFailure(
interruption,
interruption === "cancelled"
? "Command cancelled during ownership preparation."
: "Command deadline elapsed during ownership preparation.",
null,
null,
{ cause },
);
throw cause;
}
let exceeded = false;
let processFailure: string | undefined;
const supervisor = new ProviderProcessSupervisor(
{
...launched,
ownsProcessLifetime: true,
cleanup:
launched.cleanup ??
(() => cleanupOwnedProcessGroup(launched.ownership)),
},
{
maxDiagnosticBytes: limits.diagnosticBytes,
onDiagnostic: (event) => {
if (event.type === "output" && event.truncated === true)
exceeded = true;
if (event.type === "error") processFailure = event.message;
},
},
);
let failure: OwnedCommandFailure | undefined;
try {
while (!(await supervisor.waitForOutputClose(10))) {
const interruption = deadline.interruption;
if (interruption !== undefined)
throw new OwnedCommandFailure(
interruption,
interruption === "cancelled"
? "Command cancelled."
: "Command deadline elapsed.",
);
if (exceeded)
throw new OwnedCommandFailure(
"output-limit",
"Command diagnostic output exceeded its complete-output budget.",
);
if (processFailure !== undefined)
throw new OwnedCommandFailure("process", processFailure);
await waitForAbortableDelay(25, signal);
}
const snapshot = supervisor.snapshot();
const interruption = deadline.interruption;
if (interruption !== undefined)
throw new OwnedCommandFailure(
interruption,
interruption === "cancelled"
? "Command cancelled."
: "Command deadline elapsed.",
snapshot,
);
if (exceeded)
throw new OwnedCommandFailure(
"output-limit",
"Command diagnostic output exceeded its complete-output budget.",
snapshot,
);
if (
processFailure !== undefined ||
snapshot.exitCode !== 0 ||
snapshot.signal !== null
)
throw new OwnedCommandFailure(
"process",
processFailure ??
"Command failed; inspect its exit status and diagnostics.",
snapshot,
);
} catch (cause: unknown) {
failure =
cause instanceof OwnedCommandFailure
? cause
: new OwnedCommandFailure(
"process",
"Command collection failed.",
null,
null,
{ cause },
);
}
const stopped = await supervisor.stop();
const snapshot = supervisor.snapshot();
supervisor.dispose();
if (stopped.status === "incomplete")
throw new OwnedCommandFailure(
failure?.reason ?? "process",
failure?.message ?? "Command cleanup failed.",
snapshot,
stopped.reason,
failure === undefined ? undefined : { cause: failure },
[
spawn.runId,
...(spawn.cwd === undefined ? [] : [spawn.cwd]),
`pid:${launched.ownership.leaderPid}`,
`process-group:${launched.ownership.processGroupId}`,
],
);
if (failure !== undefined)
throw new OwnedCommandFailure(
failure.reason,
failure.message,
snapshot,
failure.cleanupFailure,
{ cause: failure },
failure.resources,
);
if (callerSignal?.aborted)
throw new OwnedCommandFailure("cancelled", "Command cancelled.", snapshot);
return snapshot;
};