Separate extraction/inventory and caller-selectable publication budgets in the shared firmware workflow. Preserve phase, selected timeout, verified progress, caller cancellation identity and committed observations through cleanup. Enforce monotonic expiry even before timer callbacks run. Validated with 101 targeted tests, full static checks, documentation checks and real OpenWrt CLI/MCP workflows with digest and cleanup verification. Existing main CI failures are documented separately. Fixes #4176.
92 lines
3.5 KiB
TypeScript
92 lines
3.5 KiB
TypeScript
import { z } from "incur";
|
|
import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js";
|
|
import { createFirmwareAnalysisProvider } from "../composition/firmware.js";
|
|
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
|
import { logCliCommand } from "../cliLogging.js";
|
|
import { firmwareInputSchemas } from "../domain/firmware/firmwareAnalysis.js";
|
|
import { localPathStringSchema } from "../domain/localPath.js";
|
|
import type { FirmwareOperation } from "../domain/firmware/firmwareAnalysis.js";
|
|
import type { Logger } from "pino";
|
|
import type { CliInstance } from "./types.js";
|
|
import { withCommandCancellation } from "./commandCancellation.js";
|
|
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
|
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
|
|
|
/** One-shot firmware CLI commands backed by the same workflows as MCP. */
|
|
export const registerFirmwareCommands = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
environment: Readonly<Record<string, string | undefined>>,
|
|
): void => {
|
|
const service = new FirmwareAnalysisService(
|
|
createFirmwareAnalysisProvider(environment),
|
|
);
|
|
const extractInput = firmwareInputSchemas.extract_firmware.shape;
|
|
const range = extractInput.range.unwrap().shape;
|
|
const execute = (
|
|
name: string,
|
|
operation: FirmwareOperation,
|
|
input: unknown,
|
|
) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, name, () =>
|
|
finishAnalysisCommand(
|
|
() => service.execute(operation, input, { signal }),
|
|
service,
|
|
operation === "inspect_firmware_regions" ? "binwalk" : "unblob",
|
|
operation,
|
|
signal,
|
|
),
|
|
),
|
|
);
|
|
const path = localPathStringSchema.describe(
|
|
"Local firmware path; never executed",
|
|
);
|
|
cli.command(CLI_COMMANDS.inspectFirmwareRegions, {
|
|
description:
|
|
"Inspect firmware signature candidates without extracting them",
|
|
args: z.object({ path }),
|
|
run: ({ args }) =>
|
|
execute(
|
|
CLI_COMMANDS.inspectFirmwareRegions,
|
|
"inspect_firmware_regions",
|
|
// CLI paths are operator-relative; resolve before shared validation.
|
|
{ path: absoluteCallerPath(args.path) },
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.extractFirmware, {
|
|
description:
|
|
"Extract selected firmware bytes and return verified files and provenance",
|
|
args: z.object({
|
|
path,
|
|
outputDirectory: localPathStringSchema.describe(
|
|
"Absent output directory; parent must exist",
|
|
),
|
|
}),
|
|
options: z.object({
|
|
offset: range.offset
|
|
.optional()
|
|
.describe("Selected byte offset; requires length"),
|
|
length: range.length
|
|
.optional()
|
|
.describe("Selected byte length; requires offset"),
|
|
maxDepth: extractInput.max_depth,
|
|
maxOutputBytes: extractInput.max_output_bytes,
|
|
maxOutputFiles: extractInput.max_output_files,
|
|
publicationTimeoutMs: extractInput.publication_timeout_ms,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
execute(CLI_COMMANDS.extractFirmware, "extract_firmware", {
|
|
path: absoluteCallerPath(args.path),
|
|
output_directory: absoluteCallerPath(args.outputDirectory),
|
|
...(options.offset === undefined && options.length === undefined
|
|
? {}
|
|
: { range: { offset: options.offset, length: options.length } }),
|
|
max_depth: options.maxDepth,
|
|
max_output_bytes: options.maxOutputBytes,
|
|
max_output_files: options.maxOutputFiles,
|
|
publication_timeout_ms: options.publicationTimeoutMs,
|
|
}),
|
|
});
|
|
};
|