fix(target): explain the JavaScript directory analysis route (#733)
This commit is contained in:
parent
9dd6cef90a
commit
8d44b3edd3
|
|
@ -25,6 +25,13 @@ Tool results include their Evidence inline. Prompts do not require a bundle or
|
|||
resource fetch to read a result; an Evidence ID is only a reference when a
|
||||
prompt argument or tool explicitly accepts one.
|
||||
|
||||
For a JavaScript/Electron application directory, `investigate_feature` points
|
||||
directly to `analyze_javascript_application` with `input_path`; it does not
|
||||
require `open_binary`. The CLI equivalent is `rea analyze <directory>`.
|
||||
`open_binary` and the doctor target check admit files and macOS app bundles.
|
||||
A directory outside that opening route is reported with an available analysis
|
||||
action; this does not establish support for every directory format.
|
||||
|
||||
For target paths in `compare_application_versions`, the prompt suggests
|
||||
opening each target before calling `inspect_artifact`; inspection operates on
|
||||
the active target and returns the graph and findings together inline.
|
||||
|
|
|
|||
|
|
@ -562,8 +562,8 @@
|
|||
},
|
||||
"digests": {
|
||||
"tools_sha256": "bbd9aacf7bb0bc54a8f8743491583482e1176252513d99780a309d0fddeb3df0",
|
||||
"prompts_sha256": "4762db348821fec787a5b8c6423f2c48a71f6f0ec2afee97b16ad69030ec504b",
|
||||
"combined_sha256": "b4f7e5860d90b029876b58fe7cd4a8041fef1f2add23440e37e90827d808e503",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "8778381e23b001485ddab201a9a41bdc3c2a0631ccadb1afa83371f6c2fcbe6c",
|
||||
"providers_sha256": "e094c361b60ec930a6062a1a90ee05536ce411541dba5b51ace7e2da2ba25af0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
|
|||
6,src/domain/errors.ts:AnalysisProtocolError,Application protocol fails,Raw constructor message,Can expose protocol internals,"Analysis returned an unreadable result. Retry once; if it continues, run `rea doctor`.",implemented,focused assertions passed; full `npm run check` passed
|
||||
7,src/domain/errors.ts:HopperProcessError,Analysis subprocess stops,"Hopper bridge stopped unexpectedly with code {exitCode}",Exposes provider bridge and process code,"Analysis could not start or stopped unexpectedly. Run `rea doctor`, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
8,src/domain/errors.ts:HopperStartError,Analysis subprocess cannot start,Hopper application bridge could not be started,Exposes provider and bridge implementation,"Analysis could not start or stopped unexpectedly. Run `rea doctor`, then try again.",implemented,focused assertions passed; full `npm run check` passed
|
||||
9,src/domain/errors.ts:BinaryTargetError,Target cannot open,"Cannot open artifact: {reason}",May expose raw reason and lacks checks,"REA could not open that app or binary. Check that the path exists, is readable, and points to a supported file.",implemented,focused assertions passed; full `npm run check` passed
|
||||
9,src/domain/errors.ts:BinaryTargetError,Target cannot open,"Cannot open artifact: {reason}",May expose raw reason and lacks checks,"Preserve the target admission reason; directories name analyze_javascript_application and rea analyze as available alternatives.",implemented,focused assertions passed; full `npm run check` passed
|
||||
10,src/domain/errors.ts:ConfigurationError,Environment configuration invalid,Raw constructor message,May expose provider-specific variables and lacks recovery,"REA configuration is invalid. Run `rea doctor` and fix the reported setting.",implemented,focused assertions passed; full `npm run check` passed
|
||||
11,src/domain/errors.ts:ArtifactOperationError,Artifact inventory or extraction fails,"Artifact {operation} failed: {reason} at {logicalPath} (declared_sha256=...)",Exposes paths hashes and internal reason,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
|
||||
12,src/domain/errors.ts:EvidenceFileError,Evidence file operation fails,"Evidence bundle {operation} failed: {reason}",Internal reason token gives weak recovery,"Reason-specific plain message with safe recovery step.",implemented,focused assertions passed; full `npm run check` passed
|
||||
|
|
|
|||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"manifest_id": "ecm_125743de92b0cbb08a3cb11ee0d9305f60e5c4fb81152b8b748d348f91f99ee8",
|
||||
"manifest_id": "ecm_c47a9e9f524d47b6eee9639d0cda8bba3c4d1a4dd0d6f38d15b5b9ef2a9c765e",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "26338313efd5c941e1f3e1f1d35e2605d47749f674c359458e76d5a470b0cbb1"
|
||||
"sha256": "3912bba1d6a2269d514b913d8cb0f0c64d2621d5f225904a3194d0d4a7aecf80"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, an
|
|||
metadata:
|
||||
version: "24"
|
||||
tool_count: 118
|
||||
catalog_digest: "b4f7e5860d90b029876b58fe7cd4a8041fef1f2add23440e37e90827d808e503"
|
||||
catalog_digest: "8778381e23b001485ddab201a9a41bdc3c2a0631ccadb1afa83371f6c2fcbe6c"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
|
|
|||
|
|
@ -217,7 +217,15 @@ const resolveAppBundle = async (
|
|||
const metadata = await stat(path);
|
||||
if (!metadata.isDirectory()) return ok(path);
|
||||
if (extname(path).toLowerCase() !== ".app")
|
||||
return err(new BinaryTargetError(path, "target is not an app or file"));
|
||||
return err(
|
||||
new BinaryTargetError(
|
||||
path,
|
||||
"this opening route requires a file or macOS app bundle",
|
||||
{
|
||||
constraint: "directory_requires_file",
|
||||
},
|
||||
),
|
||||
);
|
||||
const plistPath = join(path, "Contents", "Info.plist");
|
||||
let name: string;
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -500,3 +500,25 @@ describe("doctor Linux and Hopper discovery", () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("doctor target admission diagnostics", () => {
|
||||
it("preserves route-specific target diagnostics from the host", async () => {
|
||||
const targetCheck = {
|
||||
name: "target",
|
||||
ok: false,
|
||||
classification: "unsupported_target" as const,
|
||||
detail: "/fixture/javascript-app",
|
||||
remediation: "Use analyze_javascript_application with input_path.",
|
||||
};
|
||||
const result = await runDoctor(
|
||||
targetCheck.detail,
|
||||
host({
|
||||
inspectTarget: () => Promise.resolve(targetCheck),
|
||||
validTarget: () => {
|
||||
throw new Error("the detailed target check should be used");
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(result.checks).toContainEqual(targetCheck);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { access, readFile, readdir } from "node:fs/promises";
|
|||
import { homedir } from "node:os";
|
||||
import { delimiter, join } from "node:path";
|
||||
|
||||
import { analysisErrorRemediationAction } from "../domain/analysisErrorPresentation.js";
|
||||
import { parseBinaryTarget } from "./BinaryTargetResolver.js";
|
||||
import { execFileOutput } from "../process/ExecFileOutput.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
|
@ -40,6 +41,7 @@ export interface DoctorCheck {
|
|||
| "healthy"
|
||||
| "missing_dependency"
|
||||
| "unsupported_host"
|
||||
| "unsupported_target"
|
||||
| "missing_analysis_engine"
|
||||
| "config_drift";
|
||||
readonly detail?: string;
|
||||
|
|
@ -79,6 +81,8 @@ export interface DoctorHost {
|
|||
macosVersion(): Promise<string | undefined>;
|
||||
linuxDistribution(): Promise<LinuxDistribution | undefined>;
|
||||
validTarget(path: string): Promise<boolean>;
|
||||
/** Route-specific target admission details, when supplied by the host. */
|
||||
inspectTarget?(path: string): Promise<DoctorCheck>;
|
||||
executable(path: string): Promise<boolean>;
|
||||
supportedLinuxHopper(path: string): Promise<boolean>;
|
||||
linuxDemoRuntimeCheck(): Promise<DoctorCheck>;
|
||||
|
|
@ -333,6 +337,33 @@ export const systemDoctorHost = (
|
|||
async validTarget(path) {
|
||||
return (await parseBinaryTarget(path, process.cwd(), architecture)).ok;
|
||||
},
|
||||
async inspectTarget(path) {
|
||||
const result = await parseBinaryTarget(path, process.cwd(), architecture);
|
||||
if (result.ok)
|
||||
return {
|
||||
name: "target",
|
||||
ok: true,
|
||||
classification: "healthy",
|
||||
detail: path,
|
||||
};
|
||||
const error = result.error;
|
||||
return {
|
||||
name: "target",
|
||||
ok: false,
|
||||
classification:
|
||||
error.constraint === "directory_requires_file"
|
||||
? "unsupported_target"
|
||||
: "config_drift",
|
||||
detail: path,
|
||||
details: {
|
||||
reason: error.reason,
|
||||
...(error.constraint === undefined
|
||||
? {}
|
||||
: { constraint: error.constraint }),
|
||||
},
|
||||
remediation: analysisErrorRemediationAction(error),
|
||||
};
|
||||
},
|
||||
executable: (path) =>
|
||||
executableAvailable(
|
||||
path,
|
||||
|
|
|
|||
|
|
@ -217,10 +217,12 @@ const optionalTargetCheck = async (
|
|||
): Promise<DoctorCheck | undefined> =>
|
||||
target === undefined
|
||||
? undefined
|
||||
: check("target", await host.validTarget(target), target, {
|
||||
remediation: "Supply a readable local app or program path.",
|
||||
classification: "config_drift",
|
||||
});
|
||||
: host.inspectTarget !== undefined
|
||||
? host.inspectTarget(target)
|
||||
: check("target", await host.validTarget(target), target, {
|
||||
remediation: "Supply a readable local app or program path.",
|
||||
classification: "config_drift",
|
||||
});
|
||||
|
||||
const check = (
|
||||
name: string,
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@ export const PROMPT_CONTRACTS = [
|
|||
{
|
||||
tools: ["open_binary", "analyze_javascript_application"],
|
||||
instruction:
|
||||
"For a JavaScript/Electron artifact, reconstruct its application graph without execution when useful; open target_path only when the required target is not active.",
|
||||
"For a JavaScript/Electron application directory, call analyze_javascript_application with input_path=target_path directly; no open_binary call is needed. For a file or macOS app bundle, open target_path only when the required target is not active.",
|
||||
},
|
||||
{
|
||||
tools: ["list_documents", "set_current_document"],
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ export const analysisErrorRemediationAction = (
|
|||
: `Use the active REA session ${error.ownerRunId} or close it before opening this target again.`;
|
||||
if (error instanceof HopperRemoteError)
|
||||
return "Review the Hopper diagnostic details; correct the request or retry if the failure was transient.";
|
||||
if (
|
||||
error instanceof BinaryTargetError &&
|
||||
error.constraint === "directory_requires_file"
|
||||
)
|
||||
return "For a JavaScript/Electron application directory, call analyze_javascript_application with input_path or run `rea analyze <directory>`. For binary analysis, select its executable file.";
|
||||
if (error instanceof AnalysisInputError)
|
||||
return "Correct the listed arguments and retry.";
|
||||
if (error instanceof UnknownRegistryError && error.reason === "not-found")
|
||||
|
|
@ -129,7 +134,9 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
|
|||
return "REA configuration is invalid. Run `rea doctor` and fix the reported setting.";
|
||||
if (error instanceof NoBinaryOpenError) return error.message;
|
||||
if (error instanceof BinaryTargetError)
|
||||
return "REA could not open that app or binary. Check that the path exists, is readable, and points to a supported file.";
|
||||
return error.constraint === "directory_requires_file"
|
||||
? "open_binary accepts files and macOS app bundles. This target is a directory; JavaScript/Electron application directories can be analyzed directly with analyze_javascript_application or `rea analyze <directory>`."
|
||||
: `${error.message}. Check that the path exists, is readable, and points to a supported file.`;
|
||||
if (error._tag === "ProcessCaptureError")
|
||||
return (
|
||||
error.userMessage ??
|
||||
|
|
|
|||
|
|
@ -334,7 +334,14 @@ const lifecycleErrorDetails = (
|
|||
error.userCategory === "cancelled"
|
||||
)
|
||||
return { operation: "process_capture", cleanup: "complete" };
|
||||
if (error instanceof BinaryTargetError) return { path: error.path };
|
||||
if (error instanceof BinaryTargetError)
|
||||
return {
|
||||
path: error.path,
|
||||
reason: error.reason,
|
||||
...(error.constraint === undefined
|
||||
? {}
|
||||
: { constraint: error.constraint }),
|
||||
};
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -15,14 +15,21 @@ export class NoBinaryOpenError extends AnalysisError {
|
|||
}
|
||||
}
|
||||
|
||||
/** Additional constraints identified while admitting a filesystem target. */
|
||||
export interface BinaryTargetErrorOptions extends ErrorOptions {
|
||||
readonly constraint?: "directory_requires_file";
|
||||
}
|
||||
|
||||
/** A supplied target path could not be safely opened as a supported app or binary. */
|
||||
export class BinaryTargetError extends AnalysisError {
|
||||
readonly _tag = "BinaryTargetError";
|
||||
readonly constraint: BinaryTargetErrorOptions["constraint"];
|
||||
constructor(
|
||||
readonly path: string,
|
||||
reason: string,
|
||||
options?: ErrorOptions,
|
||||
readonly reason: string,
|
||||
options?: BinaryTargetErrorOptions,
|
||||
) {
|
||||
super(`Cannot open artifact: ${reason}`, options);
|
||||
this.constraint = options?.constraint;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
import { chmod, realpath, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { systemDoctorHost } from "../../../src/application/Doctor.js";
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
describe("target route diagnostics", () => {
|
||||
it("identifies the directory opening constraint and gives the direct analysis route", async () => {
|
||||
const directory = await createTestTempDirectory("rea-js-target-");
|
||||
await writeFile(join(directory, "index.js"), "export const answer = 42;\n");
|
||||
const result = await parseBinaryTarget(directory);
|
||||
if (result.ok) throw new Error("directory was admitted as a file");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
details: {
|
||||
path: await realpath(directory),
|
||||
constraint: "directory_requires_file",
|
||||
},
|
||||
});
|
||||
expect(projected.message).toContain("analyze_javascript_application");
|
||||
expect(projected.remediation.action).toContain("input_path");
|
||||
const doctor = systemDoctorHost();
|
||||
expect(await doctor.inspectTarget?.(directory)).toMatchObject({
|
||||
name: "target",
|
||||
ok: false,
|
||||
classification: "unsupported_target",
|
||||
details: { constraint: "directory_requires_file" },
|
||||
remediation: projected.remediation.action,
|
||||
});
|
||||
const missing = await parseBinaryTarget(join(directory, "missing"));
|
||||
if (missing.ok) throw new Error("missing target was admitted");
|
||||
expect(projectAnalysisError(missing.error)).toMatchObject({
|
||||
details: { reason: expect.stringContaining("does not exist") },
|
||||
message: expect.stringContaining("does not exist"),
|
||||
});
|
||||
expect(
|
||||
await doctor.inspectTarget?.(join(directory, "missing")),
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
classification: "config_drift",
|
||||
details: { reason: expect.stringContaining("does not exist") },
|
||||
});
|
||||
});
|
||||
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
|
||||
"preserves a real filesystem permission denial",
|
||||
async () => {
|
||||
const directory = await createTestTempDirectory("rea-target-denied-");
|
||||
const path = join(directory, "index.js");
|
||||
await writeFile(path, "export const answer = 42;");
|
||||
await chmod(path, 0o000);
|
||||
try {
|
||||
const result = await parseBinaryTarget(path);
|
||||
if (result.ok) throw new Error("unreadable target was admitted");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
details: { reason: expect.stringContaining("permission denied") },
|
||||
message: expect.stringContaining("permission denied"),
|
||||
});
|
||||
expect(await systemDoctorHost().inspectTarget?.(path)).toMatchObject({
|
||||
ok: false,
|
||||
classification: "config_drift",
|
||||
details: { reason: expect.stringContaining("permission denied") },
|
||||
});
|
||||
} finally {
|
||||
await chmod(path, 0o600);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -70,6 +70,9 @@ describe("guided prompts over MCP", () => {
|
|||
if (content?.type !== "text") throw new Error("missing prompt text");
|
||||
expect(content.text).toContain('"feature":"license validation"');
|
||||
expect(content.text).toContain("`trace_feature`");
|
||||
expect(content.text).toContain(
|
||||
"input_path=target_path directly; no open_binary call is needed",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders every workflow without executing analysis or accepting extra context", async () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue