Files
rea/src/cli/firmwareCommands.ts
T
morluto cbc0dd1e7c fix(contracts)!: require absolute local paths for remaining caller-supplied file inputs (#1004)
* fix(contracts)!: require absolute local paths for remaining caller-supplied file inputs

Follow-up to the session snapshot/export tightening: the same invisible server-cwd resolution applied to open_binary target paths (parseBinaryTarget(path, process.cwd())), import_evidence_bundle bundle reads, firmware input files (resolve(raw)), browser executable launch paths (resolve-then-spawn), Electron executable/application/root paths (realpath on raw input), and the expected server path check.

Each field gains the shared isAbsoluteLocalPath refinement plus a catalog-visible description, since JSON Schema cannot express the rule. Examples, scenario fixtures, and contract tests updated to absolute paths; new schema cases cover rejection/acceptance/omission per field.

Explicitly unchanged (verified rooted or out of scope): inspect_plist paths (resolved against the target bundle dir), firmware output_directory (already provider-enforced), filesystem observation paths (resolved against the caller-supplied working_directory), keyed-archive member paths (archive-relative), JS evidence member paths (evidence-relative under root_alias), provider-internal execution schemas, CLI-only doctor flows, and CLI options (own resolve against the operator cwd).

* fix(cli): resolve operator-relative paths before shared absolute-path validation

The contract tightening rejects relative paths, but three CLI commands forward operator-relative input into the shared schemas: firmware commands pass raw args to FirmwareAnalysisService, and the Electron/browser scenario commands validate parsed scenario JSON directly. CLI paths are relative to the operator shell, so resolve them against the working directory first: resolve() in firmwareCommands, and a shared resolveCliJsonPaths JSON key-path helper (cliJsonInput) applied in electronCommands and cliBrowserScenarioCommands. MCP behavior is unchanged — MCP callers must still send absolute paths.

Covers the regression with a relative-path firmware CLI acceptance test (asserts provider-stage capability_unavailable, not a validation error) and unit tests for the helper.
2026-10-08 00:52:37 +08:00

103 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "incur";
import { resolve } from "node:path";
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 { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { FirmwareOperation } from "../domain/firmware/firmwareAnalysis.js";
import type { Logger } from "../logger.js";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.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 execute = (
name: string,
operation: FirmwareOperation,
input: unknown,
) =>
withCommandCancellation((signal) =>
logCliCommand(logger, name, async () => {
const result = await service.execute(operation, input, { signal });
return result.ok ? result.value : projectAnalysisError(result.error);
}),
);
const path = z.string().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: resolve(args.path) },
),
});
cli.command(CLI_COMMANDS.extractFirmware, {
description:
"Extract selected firmware bytes and return verified files and provenance",
args: z.object({
path,
outputDirectory: z
.string()
.describe("Absolute absent output directory; parent must exist"),
}),
options: z.object({
offset: z
.number()
.int()
.nonnegative()
.optional()
.describe("Selected byte offset; requires length"),
length: z
.number()
.int()
.positive()
.optional()
.describe("Selected byte length; requires offset"),
maxDepth: z
.number()
.int()
.optional()
.describe("Extraction depth, 1–10; default 3"),
maxOutputBytes: z
.number()
.int()
.optional()
.describe("Staging byte budget; default 256 MiB, maximum 512 MiB"),
maxOutputFiles: z
.number()
.int()
.optional()
.describe("Staging entry budget; default 10000"),
}),
run: ({ args, options }) =>
execute(CLI_COMMANDS.extractFirmware, "extract_firmware", {
path: resolve(args.path),
output_directory: resolve(args.outputDirectory),
...(options.offset === undefined && options.length === undefined
? {}
: { range: { offset: options.offset, length: options.length } }),
...(options.maxDepth === undefined
? {}
: { max_depth: options.maxDepth }),
...(options.maxOutputBytes === undefined
? {}
: { max_output_bytes: options.maxOutputBytes }),
...(options.maxOutputFiles === undefined
? {}
: { max_output_files: options.maxOutputFiles }),
}),
});
};