Files
reverse-engineering-anything/src/cli/applicationCommands.ts
T
Kaoru0822andClaude Opus 5.5 85013da0ce fix(cli): describe one-shot timeouts for an invocation whose session has ended (#8535)
* fix(cli): describe one-shot timeouts for an invocation whose session has ended

A one-shot CLI analysis that reached a provider deadline told its caller
that the Ghidra session remained usable and reported a runtime-editable
limit applying to the next operation, although the command had already
released its provider session and runtime.

The shared error projection now takes an explicit caller lifetime.
Providers keep guidance that holds only for a continuing caller (session
reuse, same-session runtime updates) apart from the request outcome, and
the presentation adds it only for a continuing caller. One-shot owners
(runDirectAnalysis, runProviderAnalysis, finishAnalysisCommand and the
JavaScript application command) project for the invocation: details.limit
reports scope rea-invocation, applies_at next_invocation and the launch
environment seed, and the remediation leads with that setting for a new
invocation. MCP output keeps its runtime-update and same-session guidance.

Covers the Ghidra decompile timeout, decompiler failure, regex stack
exhaustion and startup timeout, the shared regex deadline, the Kuna
decompile timeout and the Hopper startup timeout.

Fixes #8311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cli): project composed child failures for the caller lifetime

Cleanup composition embedded its primary and cleanup failures as
continuing-caller projections, and baked the primary's message and the
cleanup's remediation into the composed error. A one-shot command whose
Ghidra startup timed out while shutdown also failed therefore still
reported a rea-runtime limit and update_analysis_settings in
details.diagnostics.primary_error.

childFailureDiagnostics now snapshots each embedded child at composition
for both lifetimes when its projection depends on the caller, so cleanup
history keeps its composition-time state and the final projection selects
the caller's form. composedCallerGuidance re-attributes message and
remediation with their continuing-only parts kept separate. Error copies
and redaction keep the invocation diagnostics. The Hopper startup timeout
now carries its continuing guidance on the error like the other producers.

Invocation limits keep agent_editable and rea_restart_required, with
values that hold for a new invocation, so only scope and applies_at
change and environment is added.

In existing-project mode a decompile timeout or a cancelled session now
says the next session copies and opens REA_GHIDRA_PROJECT_PATH again
rather than repeating import and analysis.

Tests compare delivered guidance with the owning formatters and constants
instead of freezing prose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 08:57:32 -08:00

274 lines
9.5 KiB
TypeScript

import { Cli, z } from "incur";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { parseCliJsonInput } from "../cliJsonInput.js";
import { logCliCommand } from "../cliLogging.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { Logger } from "pino";
import { registerSourceToBundleCommand } from "./sourceToBundleCommand.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import type { EvidenceLookup } from "../application/EvidenceInputResolver.js";
import type { Evidence } from "../domain/evidence.js";
import type { Result } from "../domain/result.js";
import {
evidenceBundleOption,
runEvidenceInputCommand,
} from "./evidenceInputCommand.js";
import { withCommandCancellation } from "./commandCancellation.js";
type CliInstance = ReturnType<typeof Cli.create>;
/** Register CLI equivalents of provider-neutral application graph workflows. */
export const registerApplicationCommands = (
cli: ReturnType<typeof Cli.create>,
logger: Logger,
): void => {
registerEvidenceJsonCommand({
cli,
logger,
name: CLI_COMMANDS.traceApplicationFeature,
operation: "trace_application_feature",
description:
"Trace a typed seed through authenticated application Evidence JSON",
workflow: async (input, lookup, signal) => {
const { traceApplicationFeatureEvidence } =
await import("../application/javascript/JavaScriptApplicationWorkflowService.js");
const result = traceApplicationFeatureEvidence(input, lookup, { signal });
return result.ok ? { ok: true, value: result.value.evidence } : result;
},
});
registerEvidenceJsonCommand({
cli,
logger,
name: CLI_COMMANDS.traceJavaScriptSemantics,
operation: "trace_javascript_semantics",
description:
"Trace bounded static semantic relations through authenticated application Evidence",
workflow: async (input, lookup, signal) => {
const { traceJavaScriptSemanticsEvidence } =
await import("../application/javascript/JavaScriptSemanticTraceService.js");
const result = traceJavaScriptSemanticsEvidence(input, lookup, {
signal,
});
return result.ok ? { ok: true, value: result.value.evidence } : result;
},
});
registerEvidenceJsonCommand({
cli,
logger,
name: CLI_COMMANDS.compareApplicationVersions,
operation: "compare_application_versions",
description:
"Compare two authenticated JavaScript Application Graph versions",
workflow: async (input, lookup, signal) => {
const { compareApplicationVersionsEvidence } =
await import("../application/javascript/JavaScriptApplicationWorkflowService.js");
const result = compareApplicationVersionsEvidence(input, lookup, {
signal,
});
return result.ok ? { ok: true, value: result.value.evidence } : result;
},
});
registerSourceToBundleCommand(cli, logger);
registerEvidenceJsonCommand({
cli,
logger,
name: CLI_COMMANDS.compareJavaScriptExportShapes,
operation: "compare_javascript_export_shapes",
description:
"Compare exact static JavaScript export return shapes without execution",
workflow: async (input, lookup, signal) => {
const { compareJavaScriptExportShapesEvidence } =
await import("../application/javascript/JavaScriptApplicationWorkflowService.js");
const result = compareJavaScriptExportShapesEvidence(input, lookup, {
signal,
});
return result.ok ? { ok: true, value: result.value.evidence } : result;
},
});
registerObligationLedgerCommand(cli, logger);
registerCoverageCommand(cli, logger);
registerEvidenceJsonCommand({
cli,
logger,
name: CLI_COMMANDS.projectAndroidApplicationGraph,
operation: "project_android_application_graph",
description:
"Project authenticated APK inventory Evidence into an Android application graph",
workflow: async (input, lookup, signal) => {
const { projectAndroidApplicationEvidence } =
await import("../application/android/AndroidApplicationService.js");
const result = projectAndroidApplicationEvidence(input, lookup, {
signal,
});
return result.ok ? { ok: true, value: result.value.evidence } : result;
},
});
registerEvidenceJsonCommand({
cli,
logger,
name: CLI_COMMANDS.projectAppleApplicationGraph,
operation: "project_apple_application_graph",
description:
"Project authenticated IPA or macOS app inventory Evidence into an Apple application graph",
workflow: async (input, lookup, signal) => {
const { projectAppleApplicationEvidence } =
await import("../application/apple/AppleApplicationService.js");
const result = projectAppleApplicationEvidence(input, lookup, { signal });
return result.ok ? { ok: true, value: result.value.evidence } : result;
},
});
registerEvidenceJsonCommand({
cli,
logger,
name: CLI_COMMANDS.projectHarmonyApplicationGraph,
operation: "project_harmony_application_graph",
description:
"Project authenticated HarmonyOS HAP, HSP, or App Pack inventory Evidence into an application graph",
workflow: async (input, lookup, signal) => {
const { projectHarmonyApplicationEvidence } =
await import("../application/harmony/HarmonyApplicationService.js");
const result = projectHarmonyApplicationEvidence(input, lookup, {
signal,
});
return result.ok ? { ok: true, value: result.value.evidence } : result;
},
});
};
const registerObligationLedgerCommand = (
cli: CliInstance,
logger: Logger,
): void =>
registerJsonCommand({
cli,
logger,
name: CLI_COMMANDS.buildReconstructionObligationLedger,
description:
"Generate a deterministic Evidence-backed reconstruction obligation ledger page",
inputSchema: z.unknown(),
workflow: async (input) => {
const { buildReconstructionObligationLedgerEvidence } =
await import("../application/ReconstructionObligationLedgerService.js");
return buildReconstructionObligationLedgerEvidence(input);
},
});
const registerCoverageCommand = (cli: CliInstance, logger: Logger): void =>
registerJsonCommand({
cli,
logger,
name: CLI_COMMANDS.evaluateReconstructionCoverage,
description: "Evaluate inline fail-closed reconstruction coverage",
inputSchema: z.unknown(),
workflow: async (input) => {
const { evaluateReconstructionCoverage } =
await import("../application/ReconstructionCoverageService.js");
return evaluateReconstructionCoverage(input);
},
});
interface JsonCommandOptions<Schema extends z.ZodType> {
readonly cli: CliInstance;
readonly logger: Logger;
readonly name: string;
readonly description: string;
readonly inputSchema: Schema;
readonly workflow: (
input: z.output<Schema>,
) => Promise<
| { readonly ok: true; readonly value: JsonValue }
| { readonly ok: false; readonly error: AnalysisError }
>;
}
const registerJsonCommand = <Schema extends z.ZodType>({
cli,
logger,
name,
description,
inputSchema,
workflow,
}: JsonCommandOptions<Schema>): void => {
cli.command(name, {
description,
args: z.object({
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
}),
run: ({ args }) =>
logCliCommand(logger, name, async () => {
const input = await parseCliJsonInput(args.inputJson, name);
if (!input.ok) return input.error;
const parsed = inputSchema.safeParse(input.value);
if (!parsed.success)
return {
error: "Application workflow failed",
...projectAnalysisError(
analysisInputErrorFromIssues(
name,
parsed.error.issues,
input.value,
),
),
};
const result = await workflow(parsed.data);
return result.ok
? result.value
: {
error: "Application workflow failed",
...projectAnalysisError(result.error, "invocation"),
};
}),
});
};
/** Evidence consumers share one explicit portable source and cancellation lifetime. */
const registerEvidenceJsonCommand = ({
cli,
logger,
name,
operation,
description,
workflow,
}: {
readonly cli: CliInstance;
readonly logger: Logger;
readonly name: string;
readonly operation: string;
readonly description: string;
readonly workflow: (
input: unknown,
lookup: EvidenceLookup | undefined,
signal: AbortSignal,
) => Promise<Result<Evidence, AnalysisError>>;
}): void => {
cli.command(name, {
description,
args: z.object({
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
}),
options: z.object({ evidenceBundle: evidenceBundleOption }),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, name, async () => {
const input = await parseCliJsonInput(args.inputJson, name, signal);
if (!input.ok) return input.error;
const result = await runEvidenceInputCommand(
operation,
options.evidenceBundle,
signal,
(lookup) => workflow(input.value, lookup, signal),
);
return result.ok
? result.value
: {
error: "Application workflow failed",
...result.error,
};
}),
),
});
};