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.
This commit is contained in:
parent
eb8f6491a8
commit
cbc0dd1e7c
|
|
@ -675,9 +675,9 @@
|
|||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "8816080083d86cb4c05db8d1ef452938abef2e6fdeead566252592ffbb55d828",
|
||||
"tools_sha256": "c77a3b4740fc2e78172e1d6757b1915d2b638185d8e87d2f1885bb41ce538813",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "9b4e896d2585645f1ed1f9f3eef270cf798f08035c70091178b7bd6119b22bf5",
|
||||
"combined_sha256": "e652c2ec4cde078a23735789c1d01cb1b9d316c9af7708e86f2c5c31b37b0e51",
|
||||
"providers_sha256": "aea15f517c822d80918ccaba57a4fa06557f484cf27b8acbaed4e1a660fac896"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"manifest_id": "ecm_82b2f9406099c2d152fcfa020c3acb23deb22d82405d42a610553e2bd0da94cc",
|
||||
"manifest_id": "ecm_aa4a1c78be657e901c87cc215a703938cfe54d7a54411cbfd64872de3ce78b93",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "77c5acdd9315573e4a1a0843e4ca643626674d2872729fd5db491261d386f110"
|
||||
"sha256": "57f28ace603b9eb5d6d9ea64d68f563ea8a4833e6a53dd7b4b40ce5fb3ac56e1"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
|
|||
metadata:
|
||||
version: "27"
|
||||
tool_count: 133
|
||||
catalog_digest: "9b4e896d2585645f1ed1f9f3eef270cf798f08035c70091178b7bd6119b22bf5"
|
||||
catalog_digest: "e652c2ec4cde078a23735789c1d01cb1b9d316c9af7708e86f2c5c31b37b0e51"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { browserScenarioSchema } from "../domain/browserScenario.js";
|
|||
|
||||
it("uses exact accessible-name matching without a caller confirmation field", async () => {
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: { mode: "launch", executable_path: "chrome" },
|
||||
browser: { mode: "launch", executable_path: "/opt/chromium/chrome" },
|
||||
start_url: { url: "https://app.example.test/" },
|
||||
actions: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { projectInputIssues } from "../domain/inputIssueProjection.js";
|
|||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import { parseCliJsonInput, resolveCliJsonPaths } from "../cliJsonInput.js";
|
||||
import {
|
||||
electronPageInspectionOptions,
|
||||
javascriptApplicationOptions,
|
||||
|
|
@ -57,7 +57,11 @@ const registerElectronActiveCommand = (
|
|||
);
|
||||
if (!input.ok) return input.error;
|
||||
const parsed = electronActiveObservationInputSchema.safeParse(
|
||||
input.value,
|
||||
resolveCliJsonPaths(input.value, [
|
||||
["executable_path"],
|
||||
["application_path"],
|
||||
["application_root"],
|
||||
]),
|
||||
);
|
||||
if (!parsed.success)
|
||||
return inputError(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
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";
|
||||
|
|
@ -38,7 +39,8 @@ export const registerFirmwareCommands = (
|
|||
execute(
|
||||
CLI_COMMANDS.inspectFirmwareRegions,
|
||||
"inspect_firmware_regions",
|
||||
args,
|
||||
// CLI paths are operator-relative; resolve before shared validation.
|
||||
{ path: resolve(args.path) },
|
||||
),
|
||||
});
|
||||
cli.command(CLI_COMMANDS.extractFirmware, {
|
||||
|
|
@ -81,8 +83,8 @@ export const registerFirmwareCommands = (
|
|||
}),
|
||||
run: ({ args, options }) =>
|
||||
execute(CLI_COMMANDS.extractFirmware, "extract_firmware", {
|
||||
path: args.path,
|
||||
output_directory: args.outputDirectory,
|
||||
path: resolve(args.path),
|
||||
output_directory: resolve(args.outputDirectory),
|
||||
...(options.offset === undefined && options.length === undefined
|
||||
? {}
|
||||
: { range: { offset: options.offset, length: options.length } }),
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { z } from "zod";
|
|||
import { captureBrowserScenario } from "./application/BrowserScenarioCaptureService.js";
|
||||
import { createBrowserScenarioProvider } from "./composition/browserScenario.js";
|
||||
import { CLI_COMMANDS } from "./cliCommandNames.js";
|
||||
import { parseCliJsonInput } from "./cliJsonInput.js";
|
||||
import { parseCliJsonInput, resolveCliJsonPaths } from "./cliJsonInput.js";
|
||||
import { logCliCommand } from "./cliLogging.js";
|
||||
import { AnalysisInputError } from "./domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
|
||||
|
|
@ -32,7 +32,9 @@ export const registerBrowserScenarioCommands = (
|
|||
logCliCommand(logger, CLI_COMMANDS.captureBrowserScenario, async () => {
|
||||
const input = await parseCliJsonInput(args.inputJson, OPERATION);
|
||||
if (!input.ok) return input.error;
|
||||
const scenario = browserScenarioSchema.safeParse(input.value);
|
||||
const scenario = browserScenarioSchema.safeParse(
|
||||
resolveCliJsonPaths(input.value, [["browser", "executable_path"]]),
|
||||
);
|
||||
if (!scenario.success)
|
||||
return cliError(
|
||||
new AnalysisInputError(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import { AnalysisInputError } from "./domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "./domain/analysisErrorProjection.js";
|
||||
|
|
@ -33,6 +34,40 @@ export const parseCliJsonInput = async (
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve named string fields of CLI-supplied JSON against the operator
|
||||
* working directory. Each entry is a key path from the document root; only
|
||||
* fields holding strings are resolved, everything else passes through
|
||||
* untouched. Shared MCP contracts reject relative paths, so CLI workflows
|
||||
* resolve operator-relative values before validation.
|
||||
*/
|
||||
export const resolveCliJsonPaths = (
|
||||
value: unknown,
|
||||
paths: ReadonlyArray<ReadonlyArray<string>>,
|
||||
): unknown => {
|
||||
let resolved = value;
|
||||
for (const keys of paths) resolved = resolveJsonPath(resolved, keys);
|
||||
return resolved;
|
||||
};
|
||||
|
||||
const resolveJsonPath = (
|
||||
value: unknown,
|
||||
keys: ReadonlyArray<string>,
|
||||
): unknown => {
|
||||
const [head, ...tail] = keys;
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value))
|
||||
return value;
|
||||
const entries = Object.entries(value).map(
|
||||
([key, entry]: readonly [string, unknown]): readonly [string, unknown] => {
|
||||
if (key !== head) return [key, entry];
|
||||
if (tail.length === 0)
|
||||
return [key, typeof entry === "string" ? resolve(entry) : entry];
|
||||
return [key, resolveJsonPath(entry, tail)];
|
||||
},
|
||||
);
|
||||
return Object.fromEntries(entries);
|
||||
};
|
||||
|
||||
const cannotBeAnExistingFile = (cause: unknown): boolean =>
|
||||
typeof cause === "object" &&
|
||||
cause !== null &&
|
||||
|
|
|
|||
|
|
@ -15,9 +15,21 @@ const snapshotPathSchema = z
|
|||
"Absolute local filesystem path for the analysis snapshot; relative paths are rejected.",
|
||||
);
|
||||
|
||||
/** Caller-supplied analysis target path; must name the target absolutely. */
|
||||
const binaryTargetPathSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"path must be an absolute local filesystem path (for example /tmp/fixture.bin or C:\\analysis\\fixture.bin)",
|
||||
})
|
||||
.describe(
|
||||
"Absolute local filesystem path for the analysis target; relative paths are rejected.",
|
||||
);
|
||||
|
||||
/** Input contract for opening a target with an optional staged snapshot. */
|
||||
export const openBinaryInputSchema = z.object({
|
||||
path: z.string().min(1),
|
||||
path: binaryTargetPathSchema,
|
||||
format: executableFormatHintSchema
|
||||
.optional()
|
||||
.describe(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { browserScenarioBrowserSchema } from "../domain/browserScenarioValues.js";
|
||||
import { firmwareInputSchemas } from "../domain/firmware/firmwareAnalysis.js";
|
||||
import { electronActiveObservationInputSchema } from "../domain/javascript/electronActiveObservation.js";
|
||||
import { managedArtifactInputSchema } from "./managed/managedToolContracts.js";
|
||||
import { exportEvidenceBundleInputSchema } from "./sessionToolContracts.js";
|
||||
import {
|
||||
closeBinaryInputSchema,
|
||||
openBinaryInputSchema,
|
||||
} from "./sessionLifecycleInputs.js";
|
||||
import { importEvidenceBundleInputSchema } from "./sessionToolSchemas.js";
|
||||
import { binarySessionInputSchema } from "./sessionStatusContract.js";
|
||||
|
||||
const openBinaryAbsolute = (snapshot_path: string) =>
|
||||
openBinaryInputSchema.safeParse({ path: "/tmp/fixture", snapshot_path });
|
||||
|
|
@ -119,3 +124,151 @@ describe.runIf(process.platform === "win32")(
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("open binary target path requires an absolute local path", () => {
|
||||
it("rejects a relative target path", () => {
|
||||
expect(
|
||||
openBinaryInputSchema.safeParse({ path: "fixtures/app.bin" }).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute target path", () => {
|
||||
expect(
|
||||
openBinaryInputSchema.safeParse({ path: "/tmp/fixture.bin" }).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("evidence bundle import path requires an absolute local path", () => {
|
||||
it("rejects a relative bundle path", () => {
|
||||
expect(
|
||||
importEvidenceBundleInputSchema.safeParse({ path: "evidence.json" })
|
||||
.success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute bundle path", () => {
|
||||
expect(
|
||||
importEvidenceBundleInputSchema.safeParse({ path: "/tmp/evidence.json" })
|
||||
.success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("firmware input path requires an absolute local path", () => {
|
||||
it("rejects a relative firmware path when inspecting regions", () => {
|
||||
expect(
|
||||
firmwareInputSchemas.inspect_firmware_regions.safeParse({
|
||||
path: "firmware.bin",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a relative firmware path when extracting", () => {
|
||||
expect(
|
||||
firmwareInputSchemas.extract_firmware.safeParse({
|
||||
path: "firmware.bin",
|
||||
output_directory: "/tmp/firmware-output",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute firmware path", () => {
|
||||
expect(
|
||||
firmwareInputSchemas.inspect_firmware_regions.safeParse({
|
||||
path: "/tmp/firmware.bin",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("browser executable path requires an absolute local path", () => {
|
||||
it("rejects a bare executable name", () => {
|
||||
expect(
|
||||
browserScenarioBrowserSchema.safeParse({
|
||||
mode: "launch",
|
||||
executable_path: "chrome",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute executable path", () => {
|
||||
expect(
|
||||
browserScenarioBrowserSchema.safeParse({
|
||||
mode: "launch",
|
||||
executable_path: "/opt/chromium/chrome",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("electron observation paths require absolute local paths", () => {
|
||||
const valid = {
|
||||
executable_path: "/Applications/Electron.app/Contents/MacOS/Electron",
|
||||
application_path: "/tmp/electron-app/main.js",
|
||||
};
|
||||
|
||||
it("rejects a relative executable path", () => {
|
||||
expect(
|
||||
electronActiveObservationInputSchema.safeParse({
|
||||
...valid,
|
||||
executable_path: "Electron",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a relative application path", () => {
|
||||
expect(
|
||||
electronActiveObservationInputSchema.safeParse({
|
||||
...valid,
|
||||
application_path: "main.js",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects a relative application root", () => {
|
||||
expect(
|
||||
electronActiveObservationInputSchema.safeParse({
|
||||
...valid,
|
||||
application_root: "app",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts absolute observation paths", () => {
|
||||
expect(
|
||||
electronActiveObservationInputSchema.safeParse({
|
||||
...valid,
|
||||
application_root: "/tmp/electron-app",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("still accepts omitting the application root", () => {
|
||||
expect(electronActiveObservationInputSchema.safeParse(valid).success).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("expected server path requires an absolute local path", () => {
|
||||
it("rejects a relative server path", () => {
|
||||
expect(
|
||||
binarySessionInputSchema.safeParse({
|
||||
expected_server_path: "dist/main.js",
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts an absolute server path", () => {
|
||||
expect(
|
||||
binarySessionInputSchema.safeParse({
|
||||
expected_server_path: "/opt/rea/dist/main.js",
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("still accepts omitting the server path", () => {
|
||||
expect(binarySessionInputSchema.safeParse({}).success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { isAbsoluteLocalPath } from "../domain/localPath.js";
|
||||
|
||||
/** Optional expectations used to compare the live session with its caller. */
|
||||
export const binarySessionInputSchema = z.object({
|
||||
expected_package_version: z.string().min(1).optional(),
|
||||
|
|
@ -7,5 +9,15 @@ export const binarySessionInputSchema = z.object({
|
|||
.string()
|
||||
.regex(/^[a-f0-9]{64}$/u)
|
||||
.optional(),
|
||||
expected_server_path: z.string().min(1).optional(),
|
||||
expected_server_path: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"expected_server_path must be an absolute local filesystem path (for example /opt/rea/dist/main.js)",
|
||||
})
|
||||
.describe(
|
||||
"Absolute local filesystem path expected for the running server; relative paths are rejected.",
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { isAbsoluteLocalPath } from "../domain/localPath.js";
|
||||
|
||||
import { artifactComparisonInputSchema } from "../domain/artifactComparison.js";
|
||||
import { bundleComparisonInputSchema } from "../domain/bundleComparison.js";
|
||||
import { callPathInputSchema } from "../domain/callPath.js";
|
||||
|
|
@ -36,7 +38,16 @@ export const addressContextInputSchema = z.strictObject({
|
|||
|
||||
/** Session-owned Evidence bundle import options. */
|
||||
export const importEvidenceBundleInputSchema = z.strictObject({
|
||||
path: z.string().min(1),
|
||||
path: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"path must be an absolute local filesystem path (for example /tmp/evidence.json or C:\\rea\\evidence.json)",
|
||||
})
|
||||
.describe(
|
||||
"Absolute local filesystem path for the evidence bundle to import; relative paths are rejected.",
|
||||
),
|
||||
});
|
||||
|
||||
/** Evidence references for deterministic process comparison. */
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export const TOOL_EXAMPLE_OVERRIDES: Readonly<
|
|||
open_binary: { path: "/tmp/fixture" },
|
||||
export_evidence_bundle: { path: "/tmp/evidence.json" },
|
||||
inspect_address_context: { address: "0x1000" },
|
||||
import_evidence_bundle: { path: "evidence.json" },
|
||||
import_evidence_bundle: { path: "/tmp/evidence.json" },
|
||||
capture_process_scenario: {
|
||||
executable: "/usr/bin/true",
|
||||
working_directory: "/tmp",
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ it("does not require duplicate origin scope declarations", () => {
|
|||
const scenario = browserScenarioSchema.parse({
|
||||
browser: {
|
||||
mode: "launch",
|
||||
executable_path: "chrome",
|
||||
executable_path: "/opt/chromium/chrome",
|
||||
},
|
||||
start_url: { url: "https://app.example.test/" },
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { isAbsoluteLocalPath } from "./localPath.js";
|
||||
import { browserNetworkContentSelectionSchema } from "./browserNetworkEvidence.js";
|
||||
|
||||
import {
|
||||
|
|
@ -11,7 +12,14 @@ export const scenarioIdentifierSchema = z
|
|||
.string()
|
||||
.regex(/^[A-Za-z][A-Za-z0-9._-]*$/u);
|
||||
|
||||
const browserExecutablePathSchema = z.string().trim().min(1);
|
||||
const browserExecutablePathSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"executable_path must be an absolute local filesystem path (for example /opt/chromium/chrome or C:\\chromium\\chrome.exe)",
|
||||
});
|
||||
|
||||
const browserScenarioBaseUrlSchema = z
|
||||
.string()
|
||||
|
|
@ -67,7 +75,9 @@ export type BrowserScenarioUrl = z.infer<typeof browserScenarioUrlSchema>;
|
|||
export const browserScenarioBrowserSchema = z.discriminatedUnion("mode", [
|
||||
z.strictObject({
|
||||
mode: z.literal("launch"),
|
||||
executable_path: browserExecutablePathSchema,
|
||||
executable_path: browserExecutablePathSchema.describe(
|
||||
"Absolute local filesystem path for the browser executable; relative paths are rejected.",
|
||||
),
|
||||
headless: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,21 @@
|
|||
import { z } from "zod";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { jsonValueSchema } from "../jsonValue.js";
|
||||
import { isAbsoluteLocalPath } from "../localPath.js";
|
||||
|
||||
const bytes = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
||||
const range = z.strictObject({ offset: bytes, length: bytes.positive() });
|
||||
const target = {
|
||||
path: z.string().min(1).describe("Local firmware file; never executed"),
|
||||
path: z
|
||||
.string()
|
||||
.min(1)
|
||||
.refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"path must be an absolute local filesystem path (for example /tmp/firmware.bin or C:\\firmware\\firmware.bin)",
|
||||
})
|
||||
.describe(
|
||||
"Absolute local firmware file; never executed. Relative paths are rejected.",
|
||||
),
|
||||
};
|
||||
|
||||
/** Explicit firmware inspection and extraction intent, independent of engines. */
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { z } from "zod";
|
||||
|
||||
const pathInputSchema = z.string().trim().min(1);
|
||||
import { isAbsoluteLocalPath } from "../localPath.js";
|
||||
|
||||
const pathInputSchema = z.string().trim().min(1).refine(isAbsoluteLocalPath, {
|
||||
message:
|
||||
"Electron executable, application, and root paths must be absolute local filesystem paths (for example /Applications/Electron.app/Contents/MacOS/Electron)",
|
||||
});
|
||||
const absolutePathSchema = z
|
||||
.string()
|
||||
.min(1)
|
||||
|
|
@ -60,9 +65,17 @@ const actionSchema = z.discriminatedUnion("kind", [
|
|||
|
||||
/** Input for one explicit, provider-owned Electron runtime experiment. */
|
||||
export const electronActiveObservationInputSchema = z.strictObject({
|
||||
executable_path: pathInputSchema,
|
||||
application_path: pathInputSchema,
|
||||
application_root: pathInputSchema.optional(),
|
||||
executable_path: pathInputSchema.describe(
|
||||
"Absolute local filesystem path for the Electron executable; relative paths are rejected.",
|
||||
),
|
||||
application_path: pathInputSchema.describe(
|
||||
"Absolute local filesystem path for the Electron application; relative paths are rejected.",
|
||||
),
|
||||
application_root: pathInputSchema
|
||||
.optional()
|
||||
.describe(
|
||||
"Absolute local filesystem root for application-relative paths; omit to derive it from the application path. Relative paths are rejected.",
|
||||
),
|
||||
args: z.array(z.string()).default([]),
|
||||
actions: z.array(actionSchema).default([]),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -89,6 +89,24 @@ cliTest.skipIf(process.platform !== "linux")(
|
|||
},
|
||||
);
|
||||
|
||||
cliTest.skipIf(process.platform !== "linux")(
|
||||
"resolves relative firmware CLI paths against the operator working directory",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-firmware-cli-relative-");
|
||||
await writeFile(join(root, "input.bin"), "firmware fixture");
|
||||
const cliResult = await cli.run({
|
||||
arguments: ["inspect-firmware-regions", "input.bin", "--json"],
|
||||
cwd: root,
|
||||
environment: { REA_LOG_LEVEL: "silent" },
|
||||
timeoutMs: 10_000,
|
||||
});
|
||||
// A relative path must pass input validation and reach the provider,
|
||||
// which reports the missing firmware engine — not a validation error.
|
||||
expect(cliResult.exitCode).toBe(1);
|
||||
expect(cliResult.json).toMatchObject({ code: "capability_unavailable" });
|
||||
},
|
||||
);
|
||||
|
||||
cliTest(
|
||||
"preserves optional JavaScript recovery configuration through CLI and MCP",
|
||||
async ({ cli }) => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { isAbsolute, join, resolve } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { parseCliJsonInput } from "../../../src/cliJsonInput.js";
|
||||
import {
|
||||
parseCliJsonInput,
|
||||
resolveCliJsonPaths,
|
||||
} from "../../../src/cliJsonInput.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
describe("CLI JSON input", () => {
|
||||
|
|
@ -51,3 +54,41 @@ describe("CLI JSON input", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveCliJsonPaths", () => {
|
||||
it("resolves named relative fields against the operator working directory", () => {
|
||||
expect(
|
||||
resolveCliJsonPaths({ executable_path: "chrome" }, [["executable_path"]]),
|
||||
).toEqual({ executable_path: resolve("chrome") });
|
||||
expect(isAbsolute(resolve("chrome"))).toBe(true);
|
||||
});
|
||||
|
||||
it("leaves absolute fields unchanged", () => {
|
||||
const value = { path: "/tmp/evidence.json" };
|
||||
expect(resolveCliJsonPaths(value, [["path"]])).toEqual(value);
|
||||
});
|
||||
|
||||
it("resolves nested key paths and preserves sibling fields", () => {
|
||||
expect(
|
||||
resolveCliJsonPaths(
|
||||
{
|
||||
browser: { mode: "launch", executable_path: "chrome" },
|
||||
actions: [],
|
||||
},
|
||||
[["browser", "executable_path"]],
|
||||
),
|
||||
).toEqual({
|
||||
browser: { mode: "launch", executable_path: resolve("chrome") },
|
||||
actions: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("passes through non-objects, missing keys, and non-string fields", () => {
|
||||
expect(resolveCliJsonPaths(null, [["path"]])).toBe(null);
|
||||
expect(resolveCliJsonPaths("text", [["path"]])).toBe("text");
|
||||
expect(resolveCliJsonPaths({ other: 1 }, [["path"]])).toEqual({
|
||||
other: 1,
|
||||
});
|
||||
expect(resolveCliJsonPaths({ path: 42 }, [["path"]])).toEqual({ path: 42 });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue