mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(cli): preserve canonical input bounds and required choices
This commit is contained in:
@@ -242,6 +242,20 @@ which some clients require even alongside `prefixItems`. Tuples with a rest
|
||||
schema also need explicit minimum lengths. Check parser/schema parity for empty,
|
||||
short, overlong, and malformed values in both input and output projections.
|
||||
|
||||
CLI translations must reuse the canonical semantic fields, enum membership and
|
||||
list bounds. Document transport differences such as relative path spelling and
|
||||
JSON text decoding; reject empty selections before path normalization. Preserve
|
||||
required alternatives across positional arguments and options. When the CLI
|
||||
schema format separates those groups and cannot express their relationship,
|
||||
state the required choice in command discovery/help and retain aggregate admission
|
||||
before target access. Test emitted schemas and parser behavior together, including
|
||||
omission, empty strings, alternatives, conflicts and boundary list counts.
|
||||
|
||||
Expected input failures must retain caller field/index paths and safe constraints.
|
||||
Internal operation/input envelopes are not caller paths. Reuse the input issue
|
||||
projector at the admission owner; a schema cause alone does not supply correction
|
||||
metadata to either adapter.
|
||||
|
||||
When discoverability changes, evaluate representative broad, direct, and negative
|
||||
tasks on the intended host. Record host/model/catalog, selected tools, arguments,
|
||||
errors, retries, and task outcomes. Schema validity and keyword heuristics do not
|
||||
|
||||
@@ -155,7 +155,8 @@ const registerDecompileCommand = (
|
||||
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.decompile, {
|
||||
description: "Read one part of an app as code",
|
||||
description:
|
||||
"Read one part of an app as code; requires a positional selector, --procedure, or --file-offset",
|
||||
args: z.object({
|
||||
path: z.string().describe("App or program path"),
|
||||
address: z
|
||||
@@ -170,7 +171,7 @@ const registerDecompileCommand = (
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(
|
||||
"Procedure name or address; use --procedure=<value> for leading dashes",
|
||||
"Required selector: supply this option or the positional selector (decompile also accepts --file-offset); use --procedure=<value> for leading dashes",
|
||||
),
|
||||
"file-offset": z
|
||||
.string()
|
||||
@@ -249,7 +250,8 @@ const registerXrefsCommand = (
|
||||
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.xrefs, {
|
||||
description: "List bounded references to an analyzed address",
|
||||
description:
|
||||
"List bounded references to an analyzed address; requires a positional selector or --address",
|
||||
args: z.object({
|
||||
path: z.string().describe("App or program path"),
|
||||
address: z.string().min(1).optional().describe("Address or symbol name"),
|
||||
@@ -260,7 +262,7 @@ const registerXrefsCommand = (
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(
|
||||
"Address or symbol name; use --address=<value> for leading dashes",
|
||||
"Required selector: supply this option or the positional selector; use --address=<value> for leading dashes",
|
||||
),
|
||||
snapshot: snapshotSelectionOption,
|
||||
...nativeOpenOptionFields,
|
||||
@@ -290,7 +292,8 @@ const registerTraceCommand = (
|
||||
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.trace, {
|
||||
description: "Trace a literal feature through analyzed references",
|
||||
description:
|
||||
"Trace a literal feature through analyzed references; requires a positional selector or --query",
|
||||
args: z.object({
|
||||
path: z.string().describe("App or program path"),
|
||||
query: z.string().min(1).optional().describe("Literal feature query"),
|
||||
@@ -301,7 +304,7 @@ const registerTraceCommand = (
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(
|
||||
"Complete literal feature query; use --query=<value> for leading dashes",
|
||||
"Required selector: supply this option or the positional selector; use --query=<value> for leading dashes",
|
||||
),
|
||||
caseSensitive: z
|
||||
.boolean()
|
||||
@@ -336,7 +339,8 @@ const registerFunctionCommand = (
|
||||
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.function, {
|
||||
description: "Analyze one complete function with evidence",
|
||||
description:
|
||||
"Analyze one complete function with evidence; requires a positional selector or --procedure",
|
||||
args: z.object({
|
||||
path: z.string().describe("App or program path"),
|
||||
address: z
|
||||
@@ -351,7 +355,7 @@ const registerFunctionCommand = (
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(
|
||||
"Procedure name or address; use --procedure=<value> for leading dashes",
|
||||
"Required selector: supply this option or the positional selector (decompile also accepts --file-offset); use --procedure=<value> for leading dashes",
|
||||
),
|
||||
snapshot: snapshotSelectionOption,
|
||||
...nativeOpenOptionFields,
|
||||
@@ -381,7 +385,8 @@ const registerInstructionsCommand = (
|
||||
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.instructions, {
|
||||
description: "Read every raw instruction without decompiling",
|
||||
description:
|
||||
"Read every raw instruction without decompiling; requires a positional selector or --procedure",
|
||||
args: z.object({
|
||||
path: z.string().describe("App or program path"),
|
||||
address: z
|
||||
@@ -396,7 +401,7 @@ const registerInstructionsCommand = (
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(
|
||||
"Procedure name or address; use --procedure=<value> for leading dashes",
|
||||
"Required selector: supply this option or the positional selector (decompile also accepts --file-offset); use --procedure=<value> for leading dashes",
|
||||
),
|
||||
snapshot: snapshotSelectionOption,
|
||||
...nativeOpenOptionFields,
|
||||
@@ -426,7 +431,8 @@ const registerSearchCommand = (
|
||||
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.search, {
|
||||
description: "Search every analyzed string or procedure name",
|
||||
description:
|
||||
"Search every analyzed string or procedure name; requires a positional selector or --pattern",
|
||||
args: z.object({
|
||||
path: z.string().describe("App or program path"),
|
||||
pattern: z
|
||||
@@ -441,7 +447,7 @@ const registerSearchCommand = (
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(
|
||||
"Complete literal text or regex pattern; use --pattern=<value> for leading dashes",
|
||||
"Required selector: supply this option or the positional selector; use --pattern=<value> for leading dashes",
|
||||
),
|
||||
kind: z
|
||||
.enum(["strings", "procedures"])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import {
|
||||
analysisEvidenceDeliveryOptions,
|
||||
deliverCliAnalysisEvidence,
|
||||
@@ -20,7 +21,7 @@ export const registerDesktopApplicationCommands = (
|
||||
description:
|
||||
"Identify desktop runtime candidates from bounded directory and header observations",
|
||||
args: z.object({
|
||||
path: z.string().describe("Local installation directory"),
|
||||
path: localPathStringSchema.describe("Local installation directory"),
|
||||
}),
|
||||
options: z.object({
|
||||
max_entries: z
|
||||
|
||||
@@ -136,7 +136,8 @@ export const registerFridaCommands = (
|
||||
),
|
||||
});
|
||||
cli.command(CLI_COMMANDS.instrumentWithFrida, {
|
||||
description: "Run one Frida script against a selected process and detach",
|
||||
description:
|
||||
"Run one Frida script against a selected process and detach; requires exactly one of --script or --script-file",
|
||||
args: z.object({
|
||||
mode: z
|
||||
.enum(["attach", "spawn"])
|
||||
@@ -153,7 +154,12 @@ export const registerFridaCommands = (
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe("Frida device identifier"),
|
||||
script: z.string().optional().describe("Inline JavaScript to load"),
|
||||
script: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Inline JavaScript to load; supply exactly one of --script or --script-file",
|
||||
),
|
||||
scriptFile: z
|
||||
.string()
|
||||
.min(1)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { javascriptRecoveryInputSchema } from "../domain/javascript/javascriptRecovery.js";
|
||||
import { z } from "incur";
|
||||
import { JavaScriptRecoveryService } from "../application/javascript/JavaScriptRecoveryService.js";
|
||||
import { createJavaScriptRecoveryProvider } from "../composition/javascriptRecovery.js";
|
||||
@@ -21,30 +22,17 @@ export const registerJavaScriptRecoveryCommands = (
|
||||
description:
|
||||
"Recover readable JavaScript modules with source provenance and verified files",
|
||||
args: z.object({
|
||||
path: z
|
||||
.string()
|
||||
.describe("Absolute UTF-8 JavaScript input; never executed"),
|
||||
outputDirectory: z.string().describe("Absolute absent output directory"),
|
||||
path: javascriptRecoveryInputSchema.shape.path,
|
||||
outputDirectory: javascriptRecoveryInputSchema.shape.output_directory,
|
||||
}),
|
||||
options: z.object({
|
||||
additionalPath: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"Related absolute UTF-8 JavaScript file; repeat for multiple chunks",
|
||||
),
|
||||
extractionMode: z
|
||||
.enum(["structural", "heuristic", "inspection"])
|
||||
.optional()
|
||||
.describe(
|
||||
"Structural boundaries, heuristic fallback, or inspection-only regions",
|
||||
),
|
||||
rewriteLevel: z
|
||||
.enum(["minimal", "standard", "aggressive"])
|
||||
.optional()
|
||||
.describe(
|
||||
"Upstream syntax recovery level; runtime equivalence remains unknown",
|
||||
),
|
||||
additionalPath: javascriptRecoveryInputSchema.shape.additional_paths,
|
||||
extractionMode: javascriptRecoveryInputSchema.shape.extraction_mode
|
||||
.removeDefault()
|
||||
.optional(),
|
||||
rewriteLevel: javascriptRecoveryInputSchema.shape.rewrite_level
|
||||
.removeDefault()
|
||||
.optional(),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
|
||||
@@ -24,12 +24,7 @@ import type { CliInstance } from "./types.js";
|
||||
import { SUPPORTED_CLIENT_DEFINITIONS } from "../application/SupportedClients.js";
|
||||
|
||||
const supportedClientIds = SUPPORTED_CLIENT_DEFINITIONS.map(({ name }) => name);
|
||||
const supportedClientSchema = z
|
||||
.string()
|
||||
.refine(
|
||||
(candidate) => supportedClientIds.some((name) => name === candidate),
|
||||
"Unsupported agent integration",
|
||||
);
|
||||
const supportedClientSchema = z.enum(supportedClientIds);
|
||||
const skillClientSchema = z.union([z.literal("shared"), supportedClientSchema]);
|
||||
|
||||
/** Register setup, doctor, uninstall, and update CLI commands. */
|
||||
|
||||
@@ -121,16 +121,14 @@ const registerNativeUiCommands = (
|
||||
...nativeUiScopeOptions,
|
||||
steps: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("JSON array of declarative AX actions or waits"),
|
||||
.describe(
|
||||
"Required JSON array of one or more declarative AX actions or waits",
|
||||
),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.captureNativeUiScenario, async () => {
|
||||
const parameters = nativeUiScope(options);
|
||||
const decoded =
|
||||
options.steps === undefined
|
||||
? { ok: true as const, value: undefined }
|
||||
: safeParseJson(options.steps);
|
||||
const decoded = safeParseJson(options.steps);
|
||||
if (!decoded.ok)
|
||||
return invalidNativeUiScenarioInput(operation, [
|
||||
{ path: ["steps"], reason: "invalid_format", expected: "JSON" },
|
||||
@@ -144,7 +142,7 @@ const registerNativeUiCommands = (
|
||||
operation,
|
||||
projectInputIssues(parsed.error.issues, {
|
||||
...parameters,
|
||||
...(options.steps === undefined ? {} : { steps: decoded.value }),
|
||||
steps: decoded.value,
|
||||
}),
|
||||
);
|
||||
const { native_ui_deadline_ms: deadline, ...scenario } = parsed.data;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { localPathStringSchema } from "../domain/localPath.js";
|
||||
import { compareWasmInterfaces } from "../application/wasm/WasmInterfaceComparisonService.js";
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
@@ -10,7 +11,10 @@ import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
import { inspectWasmArtifactInputSchema } from "../domain/wasm/wasmArtifact.js";
|
||||
import {
|
||||
inspectWasmArtifactInputSchema,
|
||||
wasmArtifactPathsSchema,
|
||||
} from "../domain/wasm/wasmArtifact.js";
|
||||
|
||||
/** Resolve all caller-selected artifact paths through the shared workflow. */
|
||||
export const registerWasmCommands = (
|
||||
@@ -45,16 +49,14 @@ export const registerWasmCommands = (
|
||||
cli.command(wasmArtifactModule.cliCommand, {
|
||||
description: "Inspect an explicit local WASM artifact with WABT",
|
||||
args: z.object({
|
||||
path: z.string().describe("Local WASM artifact"),
|
||||
path: localPathStringSchema.describe("Local WASM artifact"),
|
||||
}),
|
||||
options: z.object({
|
||||
commandTimeoutMs: inspectWasmArtifactInputSchema.shape.command_timeout_ms,
|
||||
glue_paths: z
|
||||
.array(z.string())
|
||||
glue_paths: wasmArtifactPathsSchema(localPathStringSchema)
|
||||
.optional()
|
||||
.describe("Selected local JavaScript glue paths"),
|
||||
candidate_paths: z
|
||||
.array(z.string())
|
||||
candidate_paths: wasmArtifactPathsSchema(localPathStringSchema)
|
||||
.optional()
|
||||
.describe("Explicit local WASM candidates"),
|
||||
}),
|
||||
|
||||
@@ -14,13 +14,17 @@ const artifact = z.strictObject({
|
||||
/** Default execution budget; upper bound is the runtime's signed 32-bit timer range. */
|
||||
export const DEFAULT_WASM_COMMAND_TIMEOUT_MS = 30_000;
|
||||
export const MAX_WASM_COMMAND_TIMEOUT_MS = 2_147_483_647;
|
||||
/** Selected artifact lists share the same resource bound across path transports. */
|
||||
export const wasmArtifactPathsSchema = (
|
||||
pathSchema: z.ZodString = absoluteLocalPathStringSchema,
|
||||
) => z.array(pathSchema).max(16);
|
||||
/** All artifacts are explicitly selected local files; references never trigger acquisition. */
|
||||
export const inspectWasmArtifactInputSchema = z.strictObject({
|
||||
path: absoluteLocalPathStringSchema.describe(
|
||||
"Absolute selected local WASM artifact path",
|
||||
),
|
||||
glue_paths: z.array(absoluteLocalPathStringSchema).max(16).default([]),
|
||||
candidate_paths: z.array(absoluteLocalPathStringSchema).max(16).default([]),
|
||||
glue_paths: wasmArtifactPathsSchema().default([]),
|
||||
candidate_paths: wasmArtifactPathsSchema().default([]),
|
||||
command_timeout_ms: z
|
||||
.number()
|
||||
.int()
|
||||
|
||||
@@ -112,6 +112,63 @@ describe("CLI advertised input schemas", () => {
|
||||
expect(mismatches).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves client choices, selected path bounds and required decoded steps", async () => {
|
||||
const cli = createCli({});
|
||||
const ajv = new Ajv2020({ strict: false });
|
||||
for (const command of ["setup", "doctor"]) {
|
||||
const schema = await advertisedSchema(cli, command);
|
||||
const validate = ajv.compile(schema.options ?? {});
|
||||
for (const client of ["codex", "unsupported-client"]) {
|
||||
const input = { client: [client] };
|
||||
expect(validate(input)).toBe(client === "codex");
|
||||
expect(
|
||||
parserSchema(cli, command, "options")?.safeParse(input).success,
|
||||
).toBe(client === "codex");
|
||||
}
|
||||
}
|
||||
const setup = await advertisedSchema(cli, "setup");
|
||||
const validateSkill = ajv.compile(setup.options ?? {});
|
||||
for (const skillClient of ["shared", "codex", "unsupported-client"]) {
|
||||
const input = { skillClient: [skillClient] };
|
||||
expect(validateSkill(input)).toBe(skillClient !== "unsupported-client");
|
||||
expect(
|
||||
parserSchema(cli, "setup", "options")?.safeParse(input).success,
|
||||
).toBe(skillClient !== "unsupported-client");
|
||||
}
|
||||
for (const [command, field] of [
|
||||
["inspect-wasm-artifact", "glue_paths"],
|
||||
["inspect-wasm-artifact", "candidate_paths"],
|
||||
["recover-javascript-sources", "additionalPath"],
|
||||
]) {
|
||||
const schema = await advertisedSchema(cli, command);
|
||||
const validate = ajv.compile(schema.options ?? {});
|
||||
for (const count of [16, 17]) {
|
||||
const input = {
|
||||
[field]: Array.from({ length: count }, () => "./selected"),
|
||||
};
|
||||
expect(validate(input)).toBe(count === 16);
|
||||
expect(
|
||||
parserSchema(cli, command, "options")?.safeParse(input).success,
|
||||
).toBe(count === 16);
|
||||
}
|
||||
}
|
||||
const scenario = await advertisedSchema(cli, "capture-native-ui-scenario");
|
||||
expect(scenario.options?.required).toContain("steps");
|
||||
expect(scenario.options?.properties.steps).toMatchObject({
|
||||
description: expect.stringContaining("one or more"),
|
||||
});
|
||||
const desktop = await advertisedSchema(cli, "inspect-desktop-application");
|
||||
const validate = ajv.compile(desktop.args ?? {});
|
||||
for (const path of ["", ".", "./selected"]) {
|
||||
expect(validate({ path })).toBe(path !== "");
|
||||
expect(
|
||||
parserSchema(cli, "inspect-desktop-application", "args")?.safeParse({
|
||||
path,
|
||||
}).success,
|
||||
).toBe(path !== "");
|
||||
}
|
||||
});
|
||||
|
||||
it("allows an omitted WASM timeout while validating explicitly supplied values", async () => {
|
||||
const cli = createCli({});
|
||||
const advertised = await advertisedSchema(cli, "inspect-wasm-artifact");
|
||||
|
||||
Reference in New Issue
Block a user