Files
reverse-engineering-anything/src/cli/utilityCommands.ts
T
morluto 53fe7d6d3b refactor(config): take the environment as an input at every configuration read (#554)
* fix(platform): accept absolute paths for any host and stop blaming the ceiling for a missing grant

Addresses #548 and #545.

#548: processScenarioSchema required a leading "/" for executable,
working_directory and filesystem_roots, so no process scenario could be
expressed on Windows at all. The report suggested node:path's isAbsolute, but
that answers for the host running the check: on Linux `C:\tools` is not
absolute, so a scenario captured on Windows would fail validation when it is
replayed, compared, or asserted in a test on another host. A boundary schema
must not vary with its evaluator, so the check accepts the POSIX form, a
Windows drive path and a UNC share everywhere. The rejection message now names
the constraint instead of leaving the caller to infer it from "invalid request".
The HTTP `path` field inside `checkpoints` is an URL path and is deliberately
left alone.

I checked whether this merely moved the failure downstream: containment is
enforced by isPathWithinRoot, which already uses host-aware path.relative, so
validation really was the only blocker.

#545: when a scope is inside the administrator ceiling but no grant was ever
issued, the denial still pointed at widening the ceiling -- advice that cannot
help, since the ceiling already permits the operation. The `missing` payload is
computed against candidate *grants*, so it describes what a grant must cover,
not a ceiling gap. `PermissionRemediation` gains a `grant` variant saying
exactly that, and PermissionAuthority selects it when the denial is not a
ceiling violation. An explicitly requested remediation is still honoured, and a
genuine ceiling violation still says "configure".

* refactor(config): take the environment as an input at every configuration read

Completes B-2. Every `parseConfig(process.env)` site now reads from an
injected environment, so configuration-driven behaviour is a function of an
argument rather than of the machine running it.

The twelve sites were not equivalent, and treating them as one number would
have hidden the difference:

- **src/application/ (3 sites) were genuine defects.** `runSessionStatus`
  already injected its `logger` but hardcoded `process.env` for config, so the
  MCP path's `dependencies.env` never reached it; `captureProcessScenarioFile`
  and `runAnalysis` had the same problem. `runAnalysis` gained `environment` on
  the options object it already had, which is where its other injected
  dependencies live.

- **The CLI command layer (9 sites) is a documented boundary,** where
  `process.env` is legitimate. Rather than 9 independent ambient reads, they
  now share one seam: `createCli(environment)` passes it to the seven
  registrars that need configuration, and the nested helpers (`electronContext`,
  `runtimeContext`, `loadConfiguredAuthority`, `inspectJavaScriptReplay`) take
  it explicitly.

Every new parameter defaults to `process.env`, so all existing call sites are
unchanged and production behaviour is identical. The point is that the ambient
read is now a documented default at a boundary rather than a hidden dependency
deep inside behaviour.

`process.env` now survives in `src/` only as those default parameters —
nothing reads it at the point of use.

* style(cli): apply prettier formatting to the runtime observation command
2026-10-05 20:00:16 +08:00

230 lines
7.4 KiB
TypeScript

import { z } from "incur";
import { jsonValueSchema } from "../domain/jsonValue.js";
import {
runCapabilityStatus,
runProviderAnalysis,
runProviderStatus,
} from "../application/DirectAnalysis.js";
import { importReferenceSource } from "../application/ReferenceSourceImport.js";
import { projectReferenceSourceImportError } from "../application/ReferenceSourceImportTypes.js";
import { parseConfig } from "../config.js";
import { projectAnalysisError } from "../domain/errors.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import { logCliCommand } from "../cliLogging.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import type { Logger } from "../logger.js";
import type { CliInstance } from "./types.js";
export const registerUtilityCommands = (
cli: CliInstance,
logger: Logger,
environment: Readonly<Record<string, string | undefined>> = process.env,
): void => {
registerCapabilityCommands(cli, logger);
registerNativeCommands(cli, logger);
for (const [command, operation] of [
[CLI_COMMANDS.observeNativeUi, "observe_native_ui"],
[CLI_COMMANDS.captureNativeUiScenario, "capture_native_ui_scenario"],
] as const) {
cli.command(command, {
description:
"Observe or run an approved scenario in one exact native application window",
args: z.object({
path: z.string().describe("Local native executable or app path"),
}),
options: z.object({
pid: z
.number()
.int()
.positive()
.describe("Approved existing application PID"),
windowId: z
.number()
.int()
.positive()
.describe("Exact selected application window ID"),
observationApproved: z
.boolean()
.default(false)
.describe("Approve passive observation of this PID/window"),
actionsApproved: z
.boolean()
.default(false)
.describe("Independently approve scenario actions"),
restore: z
.enum(["leave-as-is"])
.optional()
.describe("Explicit choice to leave application state as-is"),
steps: z
.string()
.optional()
.describe("JSON array of approved declarative AX actions or waits"),
screenshot: z
.boolean()
.default(true)
.describe("Capture only the selected window"),
accessibility: z
.boolean()
.default(true)
.describe("Read the selected window accessibility tree"),
maxNodes: z
.number()
.int()
.min(1)
.max(2000)
.default(500)
.describe("Maximum accessibility nodes per capture"),
}),
alias: {
windowId: "window-id",
observationApproved: "observation-approved",
actionsApproved: "actions-approved",
maxNodes: "max-nodes",
},
run: ({ args, options }) =>
logCliCommand(logger, command, () =>
runProviderAnalysis(
args.path,
operation,
{
pid: options.pid,
window_id: options.windowId,
observation_approved: options.observationApproved,
screenshot: options.screenshot,
accessibility: options.accessibility,
max_nodes: options.maxNodes,
...(operation === "capture_native_ui_scenario"
? {
actions_approved: options.actionsApproved,
...(options.restore === undefined
? {}
: { restore: options.restore }),
steps:
options.steps === undefined
? []
: jsonValueSchema.parse(JSON.parse(options.steps)),
}
: {}),
},
logger,
),
),
});
}
registerReferenceSourceCommand(cli, logger, environment);
};
const registerCapabilityCommands = (cli: CliInstance, logger: Logger): void => {
for (const command of [
CLI_COMMANDS.capabilities,
CLI_COMMANDS.providers,
] as const) {
cli.command(command, {
description:
command === "capabilities"
? "List provider capabilities and side effects"
: "List configured analysis providers",
run: () =>
logCliCommand(logger, command, () =>
command === CLI_COMMANDS.providers
? runProviderStatus(logger)
: runCapabilityStatus(logger),
),
});
}
};
const registerNativeCommands = (cli: CliInstance, logger: Logger): void => {
for (const [command, tool] of [
[CLI_COMMANDS.inspectMacho, "inspect_macho"],
[CLI_COMMANDS.inspectSignature, "inspect_signature"],
[CLI_COMMANDS.listArchitectures, "list_architectures"],
] as const) {
cli.command(command, {
description: `Run ${tool} without launching Hopper`,
args: z.object({ path: z.string().describe("Mach-O or app path") }),
run: ({ args }) =>
logCliCommand(logger, command, () =>
runProviderAnalysis(args.path, tool, {}, logger),
),
});
}
cli.command(CLI_COMMANDS.inspectPlist, {
description: "Parse app plist metadata without launching Hopper",
args: z.object({ path: z.string().describe("App or Mach-O path") }),
options: z.object({
relativePath: z
.string()
.default("Contents/Info.plist")
.describe("Plist path relative to the app root"),
}),
alias: { relativePath: "relative-path" },
run: ({ args, options }) =>
logCliCommand(logger, "inspect-plist", () =>
runProviderAnalysis(
args.path,
"inspect_plist",
{ relative_path: options.relativePath },
logger,
),
),
});
cli.command(CLI_COMMANDS.demangleSwift, {
description: "Demangle Swift symbols without Hopper",
args: z.object({
path: z.string().describe("Artifact path used for evidence identity"),
symbols: z
.array(z.string().min(1))
.min(1)
.describe("Swift mangled symbols to demangle"),
}),
run: ({ args }) =>
logCliCommand(logger, "demangle-swift", () =>
runProviderAnalysis(
args.path,
"demangle_swift",
{ symbols: args.symbols },
logger,
),
),
});
};
const registerReferenceSourceCommand = (
cli: CliInstance,
logger: Logger,
environment: Readonly<Record<string, string | undefined>> = process.env,
): void => {
cli.command(CLI_COMMANDS.importReferenceSource, {
description: "Import a source tree as historical reference only",
args: z.object({
root: z
.string()
.describe("Local source directory to import as historical reference"),
}),
run: ({ args }) =>
logCliCommand(logger, "import-reference-source", async () => {
const config = parseConfig(environment);
if (!config.ok)
return {
error: "Import failed",
...projectAnalysisError(config.error),
};
const imported = await importReferenceSource({
root: args.root,
caller: "rea-cli",
policy: config.value.referenceSourcePolicy,
importer: PRODUCT_IDENTITY.packageName,
importerVersion: null,
});
return imported.ok
? imported.value
: {
error: "Import failed",
...projectReferenceSourceImportError(imported.error),
};
}),
});
};