fix(cli): advertise required CLI selector choices (#6759)

* fix(artifacts): retain caller selection and applicable publication recovery

* fix(artifacts): retain DMG producer failures and bound integrity recovery claims

* fix(apk): classify malformed supported Zip64 records as input failures

* fix(browser): compare script and tool facts without metadata identity churn

* fix(environment): retain path segments counters and parent diagnostics

* fix(discovery): retain Frida optional provider failure dispatch

* test(native): share Swift context in vtable fixture admission

* fix(native): narrow capability refusal before retaining its reason

* fix(javascript): retain private receiver and iterative disposal effects

* fix(native): preserve negative Mach-O admission identity

* fix(javascript): bind Next page admission to source context

* fix: preserve process filesystem scope and admission failures

* fix(evidence): reject runtime locations for managed static facts

* fix(hopper): bind ordinary executable opens to selected bytes

* fix(browser): admit legacy capture uncertainty at retained input boundaries

* fix(unknowns): distinguish rejected creation from committed mutation delivery

* fix(evidence): preserve authentication failures and diagnose prior commitments

* fix(cli): preserve explicit Evidence record pairing in bundle comparison

* fix(artifacts): publish addressable inspection sources through both adapters

* docs(evidence): define authentication and committed delivery recovery

* fix(javascript): own reference roles and invocation result identities

* fix(hopper): pass private X authority cookie through stdin

* fix(ghidra): preserve catalog and metadata admission failures

* fix(jeb): retain selected inputs and completed primary replies

* fix(native): preserve literal environment map members

* test: retain direct hash cleanup retainer contract

* test: distinguish corrected port normalization commitment

* fix: recheck attached worker policy after final artifacts

* fix(javascript): share iteration and equality invocation facts

* fix(artifacts): preserve typed resource limit diagnoses

* fix(javascript): preserve semantic query identity and source coverage

* docs(readme): align translated setup and analysis facts

* fix(cli): reject surplus positional arguments before execution

* fix(kuna): retain setting-specific doctor recovery

* fix(setup): preserve semantic configuration refusal during uninstall

* fix(setup): retain publication operation failures and recovery facts

* fix(setup): distinguish Hopper inspection and archive staging failures

* docs(managed): distinguish oracle conformance from host verification

* docs(skill): keep one setup prerequisite policy

* fix(firmware): preserve admission and extraction coverage outcomes

* fix(contracts): preserve safe defaults and client literal guidance

* fix(javascript): retain worker storage and projection failures

* fix(evidence): preserve valid output paths near filesystem pathname limits

* fix(cli): accept named native API selectors beginning with a dash

* fix(artifacts): retain DMG carrier observations when child traversal is unavailable

* fix(artifacts): retain dylib enumeration readers through failed cleanup

* fix(browser): resolve empty storage-world origins from exact frame evidence

* fix(browser): retain unknown popup storage initialization coverage

* test(reconstruction): isolate derived surface ownership in fixtures

* test(filesystem): probe actual permission denial before host assertions

* test(managed): assert large issue retention without full-array diffs

* fix(javascript): preserve source facts across graph projections

* fix(javascript): preserve copied values and export publication lifetimes

* fix(javascript): preserve per-field publication and provenance coverage

* fix(javascript): retain initialized class self binding at body reads

* fix(artifacts): retain permission omissions through JavaScript lookups

* test(graph): narrow summary owner and complete PE fixture facts

* test(javascript): supply source graph to native handoff checks

* fix(sourcemaps): bind cleanup status to remaining codec owners

* fix(browser): retain excluded source-map declaration coverage

* test(javascript): preserve exact empty property keys in declarations

* fix(browser): keep code absence unknown without worker inventory

* fix(browser): preserve WebMCP optional fields and replay coverage gaps

* fix(browser): publish executable examples with truthful capture coverage

* fix(browser): preserve observed document invalidation and scope recovery

* fix(native): share exact-name xref ambiguity resolution

* fix(hopper): own bookmark API capability checks

* fix(native): preserve procedure selector coordinate intent

* test(discovery): select the declared GDB console contract

* fix(ghidra): bind session refusal to retained cleanup owner

* fix(apple): retain selected dylib identity after path removal

* fix(apple): preserve asset failures and authored resource coverage

* fix(apple): retain binary UID identity through archive projection

* fix: retain completed historical replies through finalization

* fix: retain unresolved browser secret declarations as input errors

* test(browser): resolve network fixture secrets through typed input contract

* fix(environment): distinguish destination refusal and write access failures

* fix(artifacts): preserve exact ZIP and DMG caller cancellation reasons

* fix(artifacts): require regular source-map target occurrences

* fix(reference): separate local filesystem diagnostics from graph commitments

* fix(setup): retain proven PATH aliases in installed registrations

* test(package): join owned updater after registry fixture failure

* fix(javascript): preserve recovery acquisition and cancellation failures

* fix(session): retain cleanup blockers and snapshot request identity

* fix(evidence): preserve format-owned JSON recovery diagnostics

* test(evidence): cover BOM recovery across bundle commands

* fix(tracing): retain caller reasons at traversal checkpoints

* fix(inventory): require released cleanup before capability fallback

* fix(hopper): drain retained publishers after current close failure

* fix(native): preserve unknown Swift extension contexts

* fix(apple): qualify unexamined nib content coverage

* fix(frida): preserve session refusal and close execution effects

* fix(rizin): retain cancellation and recover cleanup correlation

* fix: retain Electron file admission facts across adapters

* fix: preserve selected Electron display authorization

* fix(javascript): retain unavailable HTML reference scopes

* fix(javascript): preserve capture timing and detached receiver authority

* fix(ida): preserve producer failure and caller abort outcomes

* fix(cli): retain nested facts in Markdown tables

* fix(cli): present argument guidance without validator cause dumps

* docs(setup): define optional native update verification limits

* docs(clients): distinguish discovery and schema recovery boundaries

* docs(setup): explain npm workspace launcher failures

* fix(verification): bind Copilot schema profile to observed inputs

* fix(javascript): scope Next package admission by container identity

* fix(apple): align asset allowance and category fixture types

* fix(protocols): honor runtime queue and Frida identity types

* test: parse Electron listing Evidence before comparison

* fix(evidence): omit absent format recovery metadata

* fix(contracts): validate historical source examples as exact JSON values

* fix(javascript): consume per-field CommonJS publication facts

* fix(gdb): correlate raw-output completion and retain read-only TTY proof

* fix(native): retain completed LLDB facts across output-close cancellation

* fix(hopper): retain failed private-display probe cleanup authority

* style(lifecycle): format traversal and fallback edits

* fix(apple): own ordered nib collections and registry parentage

* fix(process): attribute trace omissions without changing shared successors

* fix(cli): share builtin argument ownership with MCP routing

* fix(cli): count emitted documents and normalize token windows

* fix(inspector): retain captures through authorization cleanup failures

* fix(artifacts): retain declared ASAR companion occurrence facts

* fix(javascript): separate endpoint syntax correspondence from target identity

* fix: distinguish native process exit from sampling failure

* fix(javascript): retain selected-root access and filesystem failure facts

* fix(browser): retain truthful cleanup facts and document request ownership

* fix(native): share exact Objective-C class symbol interpretation

* fix(go): retain completed artifact facts through finalization

* fix(native): preserve seed analysis failures in value traces

* fix(native): classify observed dSYM inspection refusal

* test(apple): assert observed collection edge contract

* fix(javascript): retain primary interruption through worker cleanup

* fix(cutter): retain completion-read failures during cleanup retry

* fix(cli): validate output windows and finish pending writes

* fix(mcp): require absolute JavaScript application inputs

* fix(capabilities): report missing navigation targets

* fix(cli): support system Bash completion registration

* fix(cli): retain narrowed token-count format handling

* test(ida): preserve exact optional resource method shape

* test(javascript): retain valid unknown endpoint coverage shape

* fix(ghidra): try keg JDKs after failed PATH launchers

* fix(browser): retain capacity refusal through terminal progress

* fix(reference): require exact optional JSON diagnostics at the source schema

* fix(apple): retain reader outcomes and cleanup ownership

* fix(elf): qualify exports without dynamic symbol declarations

* fix(mips): retain unexamined RELR declaration coverage

* fix(metadata): preserve canonical PowerPC subject identity

* fix(native): preserve LLDB discovery failure classifications

* fix(native): report absent launch process before cleanup classification

* fix(rizin): preserve primary outcomes across final cleanup

* fix(browser): preserve admission failures and retain policy transport cleanup

* fix(objdump): preserve input issues and provider launch failures

* test(apple): attribute omitted edges to their node capacity limit

* chore(metadata): synchronize packaged skill version

* fix(cli): consume canonical output validation state

* fix(views): preserve owning layout field diagnostics

* test(pyinstaller): distinguish path and metadata allowance failures

* fix(native): compose the selected command cause once

* fix(setup): require HTTPS throughout Hopper download redirects

* fix(ghidra): narrow fallback probe failures before projection

* fix(native): require observed symbol origin for named identity

* fix(javascript): retain unavailable source processing scopes

* fix(cli): share declared output-error redaction

* docs(windows): explain MCP registration launcher limits

* fix(session): preserve binding commitments during restoration

* fix(environment): retain primary facts through server cleanup failure

* fix(pe): distinguish unsupported overlapping header layouts

* fix(cli): keep positional admission placeholders omittable

* test(cli): expose JavaScript progress workflow failure output

* test(browser): match retained subframe limitations structurally

* fix(javascript): preserve capture and property-key effect ownership

* fix(ghidra): attribute import policy conflicts to settings

* fix(native): validate Mach-O command framing before admission

* docs(mcp): state plist inspection session prerequisite

* fix(electron): separate script frame admission from declared source labels

* test(environment): read asynchronous owner observations through contracts

* test(javascript): distinguish resolver URLs from existing module targets

* fix(ghidra): retain neutral recovery for observed SIGKILL

* fix(hopper): preserve unknown relationship completeness

* docs(native): clarify Darwin runtime breakpoint names

* fix(browser): preserve unknown script ownership through reconciliation

* fix(electron): keep optional provenance fields within JSON evidence values

* fix(native): retain Mach-O architecture discriminant narrowing

* test: give oversized registration groups semantic owners

* test: group boundary registration by observed behavior

* fix: normalize retained terminal observations with sampled identities

* fix: identify missing Electron lineage ownership markers

* fix(ida): retain registration constraints and identity refusal guidance

* fix(kuna): retain published exports through staging cleanup failures

* fix(nativeaot): charge only examined candidate table entries

* docs: clarify batch decompilation retention limits

* fix(javascript): distinguish deferred bootstrap installer writes

* fix(native)!: preserve UI scalar facts and selection failures

BREAKING CHANGE: Accessibility node value can also be a JSON number or boolean. Strings and null retain their prior meaning.

* fix(javascript): retain activation order for captured scope exits

* test(pyinstaller): use an absolute drive path for refusal coverage

* fix(javascript): preserve route selection owners and uncertainty

* fix(javascript): prove native URL constructor paths

* refactor(tests): separate fixtures and registration groups

* test(native): distinguish qualified symbol identity controls

* fix(ida): preserve busy details across timeout attribution

* fix(cli): unify builtin and global completion scope

* test(ghidra): group exit-signal evidence controls

* fix: retain narrowed terminal observation values

* fix(cli): bind completion input to public registry type

* fix(flutter): qualify unsupported unified snapshot profiles

* test(native): bind origin comparison to containing entries

* fix(cli): remove completion markers on system Bash

* test(ida): preserve physical workspace traversal controls

* fix(native): retain unknown UI executable identity

* fix(native): unify LLDB discovery preparation failures

* fix(evidence): preserve primary semantics during pending cleanup

* fix(evidence): retain owner finalization reports across cleanup envelopes

* test(browser): inspect provenance in owned graph observations

* test(flutter): assert canonical unsupported profile error code

* fix(javascript): capture primitive return values at evaluation

* fix(mcp): own cancellation and raw input throughout startup

* test(ghidra): construct exact import policy targets

* fix(gdb): preserve configured executable launch recovery

* test(ghidra): own fixed import target fixtures

* docs(readme): align localized guide link occurrences

* test(evidence): retain structured input issues through cleanup owners

* test(mcp): await observed EOF before releasing startup

* fix(electron): retain bounded source frame claims

* fix(elf): validate declared file extents before provider admission

* fix(elf): align metadata and admission fixture error types

* test(wasm): place parsed interface comparison at provider boundary

* test(ida): place capability composition at provider boundary

* test(browser): place PNG contract execution at provider boundary

* perf(test): share SDK catalog compilation across deferred clients

* fix(cli): trim repeated selector discovery guidance

* revert(test): remove unproven SDK compiler sharing

This reverts commit 56eca648d. The equivalent completed two-client measurement increased elapsed and CPU time, so compiler sharing has not established a benefit.

* refactor(managed): retain decoded metadata token facts

* test(managed): scope cleanup failures to snapshot reads

* test(cli): bound selector rejection cases by command

* test(mips): distinguish damaged ELF admission from partial ABI evidence

* test(reconstruction): declare authored function symbol identity

* test(rizin): register terminal admission at provider boundary

* test(process): align root admission fixture with observer allocation ownership

* test(mips): project inspected ABI facts without undefined overrides

* refactor(browser): remove obsolete web script reconciliation

* refactor(javascript): remove obsolete in-process projection runner

* refactor(managed): retain TypeDef row ownership through projections

* test(process): assert canonical admission failure through cleanup envelopes

* fix(managed): select types from retained definition records

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-24 06:19:39 +08:00
1 parent 488c21ab2e
commit 9dde3fd6a4
7 files changed
+515 -50

No files matched your search

+12
View File
@@ -1,5 +1,17 @@
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
/** Each separately emitted args/options schema must expose the selector choice. */
export const requiredSelectorGuidance = (alternatives: string): string =>
`Supply at least one of ${alternatives}.`;
export const addressOrProcedureGuidance = requiredSelectorGuidance(
"the positional address or --procedure",
);
/** Agreement details belong on the command, rather than every schema field. */
export const equalSelectorGuidance =
"Equal values are accepted; conflicts fail.";
/** Preserve a CLI selector supplied positionally or through its named option. */
export const resolveCliAnalysisSelector = (
positional: string | undefined,
+44 -26
View File
@@ -2,7 +2,12 @@ import { z } from "incur";
import { realpath, stat } from "node:fs/promises";
import { findAppBundleResourceRoot } from "../application/AppBundleExecutable.js";
import { resolveCliAnalysisSelector } from "./analysisSelector.js";
import {
addressOrProcedureGuidance,
equalSelectorGuidance,
requiredSelectorGuidance,
resolveCliAnalysisSelector,
} from "./analysisSelector.js";
import type { DirectAnalysis } from "../composition/directAnalysis.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
@@ -20,6 +25,17 @@ import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { registerDecompileDirectoryCommand } from "./decompileDirectoryCommand.js";
const decompileSelectorGuidance = `${addressOrProcedureGuidance} Alternatively, use --file-offset alone.`;
const namedAddressGuidance = requiredSelectorGuidance(
"the positional address or --address",
);
const querySelectorGuidance = requiredSelectorGuidance(
"the positional query or --query",
);
const patternSelectorGuidance = requiredSelectorGuidance(
"the positional pattern or --pattern",
);
/** Register provider-neutral binary overview and procedure CLI commands. */
export const registerCoreBinaryCommands = (
cli: CliInstance,
@@ -158,15 +174,14 @@ const registerDecompileCommand = (
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
): void => {
cli.command(CLI_COMMANDS.decompile, {
description:
"Read one part of an app as code; requires a positional selector, --procedure, or --file-offset",
description: `Read one part of an app as code. ${decompileSelectorGuidance} Equal positional and --procedure values are accepted; conflicts fail.`,
args: z.object({
path: z.string().describe("App or program path"),
address: z
.string()
.min(1)
.optional()
.describe("Procedure name or address"),
.describe(`Procedure name or address. ${decompileSelectorGuidance}`),
}),
options: z.object({
procedure: z
@@ -174,14 +189,14 @@ const registerDecompileCommand = (
.min(1)
.optional()
.describe(
"Required selector: supply this option or the positional selector (decompile also accepts --file-offset); use --procedure=<value> for leading dashes",
`Procedure name or address; use --procedure=<value> for leading dashes. ${decompileSelectorGuidance}`,
),
"file-offset": z
.string()
.regex(/^0x[0-9a-fA-F]+$/u)
.optional()
.describe(
"Ghidra source-file byte offset; mapped internally to an analysis address",
`Ghidra source-file byte offset; mapped internally to an analysis address. ${decompileSelectorGuidance}`,
),
snapshot: snapshotSelectionOption,
...nativeOpenOptionFields,
@@ -252,11 +267,14 @@ const registerXrefsCommand = (
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
): void => {
cli.command(CLI_COMMANDS.xrefs, {
description:
"List bounded references to an analyzed address; requires a positional selector or --address",
description: `List bounded references to an analyzed address. ${namedAddressGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
address: z.string().min(1).optional().describe("Address or symbol name"),
address: z
.string()
.min(1)
.optional()
.describe(`Address or symbol name. ${namedAddressGuidance}`),
}),
options: z.object({
address: z
@@ -264,7 +282,7 @@ const registerXrefsCommand = (
.min(1)
.optional()
.describe(
"Required selector: supply this option or the positional selector; use --address=<value> for leading dashes",
`Address or symbol name; use --address=<value> for leading dashes. ${namedAddressGuidance}`,
),
snapshot: snapshotSelectionOption,
...nativeOpenOptionFields,
@@ -294,11 +312,14 @@ const registerTraceCommand = (
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
): void => {
cli.command(CLI_COMMANDS.trace, {
description:
"Trace a literal feature through analyzed references; requires a positional selector or --query",
description: `Trace a literal feature through analyzed references. ${querySelectorGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
query: z.string().min(1).optional().describe("Literal feature query"),
query: z
.string()
.min(1)
.optional()
.describe(`Literal feature query. ${querySelectorGuidance}`),
}),
options: z.object({
query: z
@@ -306,7 +327,7 @@ const registerTraceCommand = (
.min(1)
.optional()
.describe(
"Required selector: supply this option or the positional selector; use --query=<value> for leading dashes",
`Complete literal feature query; use --query=<value> for leading dashes. ${querySelectorGuidance}`,
),
caseSensitive: z
.boolean()
@@ -341,15 +362,14 @@ const registerFunctionCommand = (
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
): void => {
cli.command(CLI_COMMANDS.function, {
description:
"Analyze one complete function with evidence; requires a positional selector or --procedure",
description: `Analyze one complete function with evidence. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
address: z
.string()
.min(1)
.optional()
.describe("Procedure name or address"),
.describe(`Procedure name or address. ${addressOrProcedureGuidance}`),
}),
options: z.object({
procedure: z
@@ -357,7 +377,7 @@ const registerFunctionCommand = (
.min(1)
.optional()
.describe(
"Required selector: supply this option or the positional selector (decompile also accepts --file-offset); use --procedure=<value> for leading dashes",
`Procedure name or address; use --procedure=<value> for leading dashes. ${addressOrProcedureGuidance}`,
),
snapshot: snapshotSelectionOption,
...nativeOpenOptionFields,
@@ -387,15 +407,14 @@ const registerInstructionsCommand = (
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
): void => {
cli.command(CLI_COMMANDS.instructions, {
description:
"Read every raw instruction without decompiling; requires a positional selector or --procedure",
description: `Read every raw instruction without decompiling. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
address: z
.string()
.min(1)
.optional()
.describe("Procedure name or address"),
.describe(`Procedure name or address. ${addressOrProcedureGuidance}`),
}),
options: z.object({
procedure: z
@@ -403,7 +422,7 @@ const registerInstructionsCommand = (
.min(1)
.optional()
.describe(
"Required selector: supply this option or the positional selector (decompile also accepts --file-offset); use --procedure=<value> for leading dashes",
`Procedure name or address; use --procedure=<value> for leading dashes. ${addressOrProcedureGuidance}`,
),
snapshot: snapshotSelectionOption,
...nativeOpenOptionFields,
@@ -433,15 +452,14 @@ const registerSearchCommand = (
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
): void => {
cli.command(CLI_COMMANDS.search, {
description:
"Search every analyzed string or procedure name; requires a positional selector or --pattern",
description: `Search every analyzed string or procedure name. ${patternSelectorGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
pattern: z
.string()
.min(1)
.optional()
.describe("Literal text or regex pattern"),
.describe(`Literal text or regex pattern. ${patternSelectorGuidance}`),
}),
options: z.object({
pattern: z
@@ -449,7 +467,7 @@ const registerSearchCommand = (
.min(1)
.optional()
.describe(
"Required selector: supply this option or the positional selector; use --pattern=<value> for leading dashes",
`Complete literal text or regex pattern; use --pattern=<value> for leading dashes. ${patternSelectorGuidance}`,
),
kind: z
.enum(["strings", "procedures"])
+11 -4
View File
@@ -1,6 +1,10 @@
import { nativeDispatchMetadataInputSchema } from "../domain/native/objcSwiftMetadata.js";
import { z } from "incur";
import { resolveCliAnalysisSelector } from "./analysisSelector.js";
import {
addressOrProcedureGuidance,
equalSelectorGuidance,
resolveCliAnalysisSelector,
} from "./analysisSelector.js";
import type { DirectAnalysis } from "../composition/directAnalysis.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
@@ -357,17 +361,20 @@ const registerNativeApiCommand = (
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
): void => {
cli.command(CLI_COMMANDS.inspectNativeApi, {
description: "Reconstruct one native function API boundary with evidence",
description: `Reconstruct one native function API boundary with evidence. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
address: z.string().optional().describe("Procedure name or address"),
address: z
.string()
.optional()
.describe(`Procedure name or address. ${addressOrProcedureGuidance}`),
}),
options: z.object({
procedure: z
.string()
.optional()
.describe(
"Procedure selector; use this option for symbols beginning with a dash",
`Procedure selector; use this option for symbols beginning with a dash. ${addressOrProcedureGuidance}`,
),
snapshot: snapshotSelectionOption,
...nativeOpenOptionFields,
+9 -6
View File
@@ -18,6 +18,10 @@ import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
/** Script sources stay optional in the options schema; exactly one is required. */
const exactlyOneScriptSource =
"Provide exactly one of --script or --script-file.";
/** Register Frida discovery and one-shot instrumentation CLI operations. */
export const registerFridaCommands = (
cli: CliInstance,
@@ -136,8 +140,7 @@ export const registerFridaCommands = (
),
});
cli.command(CLI_COMMANDS.instrumentWithFrida, {
description:
"Run one Frida script against a selected process and detach; requires exactly one of --script or --script-file",
description: `Run one Frida script against a selected process and detach. ${exactlyOneScriptSource}`,
args: z.object({
mode: z
.enum(["attach", "spawn"])
@@ -157,14 +160,14 @@ export const registerFridaCommands = (
script: z
.string()
.optional()
.describe(
"Inline JavaScript to load; supply exactly one of --script or --script-file",
),
.describe(`Inline JavaScript to load. ${exactlyOneScriptSource}`),
scriptFile: z
.string()
.min(1)
.optional()
.describe("Path to JavaScript source file"),
.describe(
`Path to JavaScript source file. ${exactlyOneScriptSource}`,
),
argument: z
.array(z.string())
.optional()
+39 -13
View File
@@ -1,6 +1,6 @@
import { describe, expect } from "vitest";
import { cliTest } from "../../support/cli/cliFixture.js";
import { cliTest, runFileCli } from "../../support/cli/cliFixture.js";
const selectors = [
["function", "procedure", "analyze_function"],
@@ -45,10 +45,10 @@ describe("CLI analysis selector boundaries", () => {
});
},
);
cliTest(
"rejects missing and conflicting selectors before opening a target",
async ({ cli }) => {
for (const [command, option, operation] of selectors) {
for (const [command, option, operation] of selectors) {
cliTest(
`rejects missing and conflicting ${command} selectors before opening a target`,
async ({ cli }) => {
for (const [selection, reason] of [
[[], "missing_argument"],
[["first", `--${option}=-second`], "invalid_value"],
@@ -69,14 +69,11 @@ describe("CLI analysis selector boundaries", () => {
details: { operation, issues: [{ path: [option], reason }] },
});
}
}
},
);
cliTest(
"keeps named dash values out of global-option parsing",
async ({ cli }) => {
for (const [command, option] of selectors) {
},
);
cliTest(
`keeps named ${command} dash values out of global-option parsing`,
async ({ cli }) => {
const result = await cli.run({
arguments: [
command,
@@ -89,6 +86,35 @@ describe("CLI analysis selector boundaries", () => {
});
expect(result.exitCode).not.toBe(0);
expect(result.json).toMatchObject({ code: "target_unavailable" });
},
);
}
cliTest(
"admits positional, named, and identical selectors before opening a missing target",
async ({ cli }) => {
for (const [command, option] of selectors) {
for (const selection of [
["selector"],
[`--${option}=selector`],
["selector", `--${option}=selector`],
]) {
const label = `${command} ${selection.join(" ")}`;
const result = await runFileCli(cli, {
arguments: [
command,
"does-not-exist",
...selection,
"--format",
"json",
],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(result.exitCode, label).not.toBe(0);
expect(result.json, label).toMatchObject({
code: "target_unavailable",
});
}
}
},
);
@@ -1,5 +1,6 @@
import { Ajv2020 } from "ajv/dist/2020.js";
import { Cli, z } from "incur";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { createCli } from "../../../src/cli.js";
@@ -370,4 +371,345 @@ describe("reference-source outline-depth input schema", () => {
expect(validate({ outlineDepth: 2 })).toBe(true);
});
});
import { resolve } from "node:path";
const atLeastOneSelector = (alternatives: string): string =>
`Supply at least one of ${alternatives}.`;
const ADDRESS_OR_PROCEDURE = atLeastOneSelector(
"the positional address or --procedure",
);
const DECOMPILE_SELECTORS = `${ADDRESS_OR_PROCEDURE} Alternatively, use --file-offset alone.`;
const NAMED_ADDRESS = atLeastOneSelector("the positional address or --address");
const QUERY_SELECTOR = atLeastOneSelector("the positional query or --query");
const PATTERN_SELECTOR = atLeastOneSelector(
"the positional pattern or --pattern",
);
const EXACTLY_ONE_SCRIPT = "Provide exactly one of --script or --script-file.";
const TARGET = "target.bin";
interface SelectorRequest {
readonly args: Record<string, unknown>;
readonly options: Record<string, unknown>;
}
interface SelectorField {
readonly group: "args" | "options";
readonly name: string;
readonly preserved?: readonly string[];
}
interface SelectorDiscovery {
readonly command: string;
readonly guidance: string;
readonly choice: "at least one" | "exactly one";
readonly fields: readonly SelectorField[];
/** Each side is valid alone, including when the other group omits its selector. */
readonly representable: readonly SelectorRequest[];
/** Cross-group omission stays valid because the schemas are separate. */
readonly omitted: SelectorRequest;
readonly rejected: readonly {
readonly args?: Record<string, unknown>;
readonly options?: Record<string, unknown>;
}[];
}
const selectorDiscovery: readonly SelectorDiscovery[] = [
{
command: "inspect-native-api",
guidance: ADDRESS_OR_PROCEDURE,
choice: "at least one",
fields: [
{ group: "args", name: "address" },
{
group: "options",
name: "procedure",
preserved: ["symbols beginning with a dash"],
},
],
representable: [
{ args: { path: TARGET, address: "main" }, options: {} },
{ args: { path: TARGET }, options: { procedure: "--help" } },
{
args: { path: TARGET, address: "main" },
options: { procedure: "main" },
},
],
omitted: { args: { path: TARGET }, options: {} },
rejected: [
{ args: { path: TARGET, address: 1 } },
{ options: { procedure: 1 } },
],
},
{
command: "function",
guidance: ADDRESS_OR_PROCEDURE,
choice: "at least one",
fields: [
{ group: "args", name: "address" },
{
group: "options",
name: "procedure",
preserved: ["use --procedure=<value> for leading dashes"],
},
],
representable: [
{ args: { path: TARGET, address: "main" }, options: {} },
{ args: { path: TARGET }, options: { procedure: "--help" } },
{
args: { path: TARGET, address: "main" },
options: { procedure: "main" },
},
],
omitted: { args: { path: TARGET }, options: {} },
rejected: [
{ args: { path: TARGET, address: "" } },
{ options: { procedure: "" } },
],
},
{
command: "instructions",
guidance: ADDRESS_OR_PROCEDURE,
choice: "at least one",
fields: [
{ group: "args", name: "address" },
{
group: "options",
name: "procedure",
preserved: ["use --procedure=<value> for leading dashes"],
},
],
representable: [
{ args: { path: TARGET, address: "main" }, options: {} },
{ args: { path: TARGET }, options: { procedure: "--help" } },
{
args: { path: TARGET, address: "main" },
options: { procedure: "main" },
},
],
omitted: { args: { path: TARGET }, options: {} },
rejected: [
{ args: { path: TARGET, address: "" } },
{ options: { procedure: "" } },
],
},
{
command: "decompile",
guidance: DECOMPILE_SELECTORS,
choice: "at least one",
fields: [
{ group: "args", name: "address" },
{
group: "options",
name: "procedure",
preserved: ["use --procedure=<value> for leading dashes"],
},
{
group: "options",
name: "file-offset",
preserved: [
"Ghidra source-file byte offset",
"mapped internally to an analysis address",
],
},
],
representable: [
{ args: { path: TARGET, address: "0x140001010" }, options: {} },
{ args: { path: TARGET }, options: { procedure: "--help" } },
{ args: { path: TARGET }, options: { "file-offset": "0x210" } },
{
args: { path: TARGET, address: "0x140001010" },
options: { procedure: "0x140001010" },
},
],
omitted: { args: { path: TARGET }, options: {} },
rejected: [
{ args: { path: TARGET, address: "" } },
{ options: { procedure: "" } },
{ options: { "file-offset": "210" } },
{ options: { "file-offset": "0x" } },
],
},
{
command: "xrefs",
guidance: NAMED_ADDRESS,
choice: "at least one",
fields: [
{ group: "args", name: "address" },
{
group: "options",
name: "address",
preserved: ["use --address=<value> for leading dashes"],
},
],
representable: [
{ args: { path: TARGET, address: "main" }, options: {} },
{ args: { path: TARGET }, options: { address: "--help" } },
{
args: { path: TARGET, address: "main" },
options: { address: "main" },
},
],
omitted: { args: { path: TARGET }, options: {} },
rejected: [
{ args: { path: TARGET, address: "" } },
{ options: { address: "" } },
],
},
{
command: "trace",
guidance: QUERY_SELECTOR,
choice: "at least one",
fields: [
{ group: "args", name: "query" },
{
group: "options",
name: "query",
preserved: ["use --query=<value> for leading dashes"],
},
],
representable: [
{ args: { path: TARGET, query: "feature" }, options: {} },
{ args: { path: TARGET }, options: { query: "--help" } },
{
args: { path: TARGET, query: "feature" },
options: { query: "feature" },
},
],
omitted: { args: { path: TARGET }, options: {} },
rejected: [
{ args: { path: TARGET, query: "" } },
{ options: { query: "" } },
],
},
{
command: "search",
guidance: PATTERN_SELECTOR,
choice: "at least one",
fields: [
{ group: "args", name: "pattern" },
{
group: "options",
name: "pattern",
preserved: ["use --pattern=<value> for leading dashes"],
},
],
representable: [
{ args: { path: TARGET, pattern: "text" }, options: {} },
{ args: { path: TARGET }, options: { pattern: "--help" } },
{
args: { path: TARGET, pattern: "text" },
options: { pattern: "text" },
},
],
omitted: { args: { path: TARGET }, options: {} },
rejected: [
{ args: { path: TARGET, pattern: "" } },
{ options: { pattern: "" } },
],
},
{
command: "instrument-with-frida",
guidance: EXACTLY_ONE_SCRIPT,
choice: "exactly one",
fields: [
{ group: "options", name: "script" },
{ group: "options", name: "scriptFile" },
],
representable: [
{
args: { mode: "attach", target: "71" },
options: { script: "send(1)" },
},
{
args: { mode: "attach", target: "71" },
options: { scriptFile: "source.js" },
},
{
args: { mode: "attach", target: "71" },
options: { script: "send(1)", scriptFile: "source.js" },
},
],
omitted: { args: { mode: "attach", target: "71" }, options: {} },
rejected: [
{ args: { mode: "attach", target: "" } },
{ args: { mode: "debug", target: "71" } },
{ options: { scriptFile: "" } },
],
},
];
const propertyDescription = (property: unknown, label: string): string => {
expect(property, label).toMatchObject({
description: expect.any(String),
});
if (
typeof property !== "object" ||
property === null ||
!("description" in property) ||
typeof property.description !== "string"
)
throw new Error(`${label} description was not on the schema property`);
return property.description;
};
describe("required CLI selector discovery", () => {
it.each(selectorDiscovery)(
"$command names every permitted selector alternative in schema, help, and the full LLM manifest",
async (entry) => {
const cli = createCli({});
const advertised = await advertisedSchema(cli, entry.command);
const otherChoice =
entry.choice === "at least one" ? "exactly one" : "at least one";
for (const field of entry.fields) {
const label = `${entry.command} ${field.group}.${field.name}`;
const description = propertyDescription(
advertised[field.group]?.properties[field.name],
label,
);
expect(description, label).toContain(entry.guidance);
expect(description, label).toContain(entry.choice);
expect(description, label).not.toContain(otherChoice);
for (const preserved of field.preserved ?? [])
expect(description, label).toContain(preserved);
expect(advertised[field.group]?.required ?? [], label).not.toContain(
field.name,
);
}
const help = await serve(cli, [entry.command, "--help"]);
const manifest = await serve(cli, [entry.command, "--llms-full"]);
expect(help.exitCode, entry.command).toBe(0);
expect(manifest.exitCode, entry.command).toBe(0);
for (const surface of [help.stdout, manifest.stdout]) {
expect(surface).toContain(entry.guidance);
expect(surface).toContain(entry.choice);
expect(surface).not.toContain(otherChoice);
for (const field of entry.fields)
for (const preserved of field.preserved ?? [])
expect(surface).toContain(preserved);
}
const ajv = new Ajv2020({ strict: false });
expect(advertised.args, entry.command).toBeDefined();
expect(advertised.options, entry.command).toBeDefined();
const validateArgs = ajv.compile(advertised.args ?? {});
const validateOptions = ajv.compile(advertised.options ?? {});
expect(validateArgs({}), entry.command).toBe(false);
for (const request of [...entry.representable, entry.omitted]) {
expect(
validateArgs(request.args),
`${entry.command} args ${JSON.stringify(validateArgs.errors)}`,
).toBe(true);
expect(
validateOptions(request.options),
`${entry.command} options ${JSON.stringify(validateOptions.errors)}`,
).toBe(true);
}
for (const request of entry.rejected) {
if (request.args !== undefined)
expect(validateArgs(request.args), entry.command).toBe(false);
if (request.options !== undefined)
expect(validateOptions(request.options), entry.command).toBe(false);
}
},
);
});
+57
View File
@@ -493,6 +493,11 @@ it.each([
"script",
"Provide exactly one of --script or --script-file.",
],
[
["attach", "71", "--script", "send(1)", "--script-file", "source.js"],
"script",
"Provide exactly one of --script or --script-file.",
],
[
["attach", "71", "--script", "send(1)", "--argument", "ignored"],
"argument",
@@ -526,3 +531,55 @@ it.each([
expect(starts).toBe(0);
},
);
it("advances each Frida script source to the attach target error without attaching or spawning", async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-frida-script-source-"));
const scriptPath = join(directory, "source.js");
try {
await writeFile(scriptPath, "send(1);\n");
for (const scriptArguments of [
["--script", "send(1)"],
["--script-file", scriptPath],
]) {
let starts = 0;
const provider = {
async startSession() {
starts += 1;
throw new Error("Frida must not attach or spawn for an invalid PID");
},
} as unknown as FridaInstrumentationPort;
let stdout = "";
await createCli({}, undefined, provider).serve(
[
"instrument-with-frida",
"attach",
"abc",
...scriptArguments,
"--json",
],
{
env: {},
exit: () => undefined,
stdout: (text) => {
stdout += text;
},
},
);
expect(JSON.parse(stdout)).toMatchObject({
code: "invalid_request",
details: {
issues: [
{
path: ["target"],
reason: "invalid_value",
message: "attach mode needs a positive integer process ID.",
},
],
},
});
expect(starts).toBe(0);
}
} finally {
await rm(directory, { recursive: true, force: true });
}
});