rea-agents 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -27
- package/bridge/hopper_bridge.py +161 -12
- package/dist/application/AnalysisSnapshotCache.js +131 -0
- package/dist/application/AnalysisSnapshotFiles.js +31 -0
- package/dist/application/ArtifactInventory.js +4 -0
- package/dist/application/AuthorizedArtifactInventory.js +16 -0
- package/dist/application/BinarySession.js +66 -36
- package/dist/application/BinarySessionPort.js +1 -0
- package/dist/application/BoundedJsonFiles.js +109 -0
- package/dist/application/CrossVersionInventory.js +98 -0
- package/dist/application/CrossVersionInvestigation.js +295 -0
- package/dist/application/DirectAnalysis.js +115 -22
- package/dist/application/Doctor.js +30 -1
- package/dist/application/EvidenceBundleFiles.js +15 -108
- package/dist/application/EvidenceLedger.js +3 -2
- package/dist/application/InvestigationProviders.js +48 -0
- package/dist/application/InvestigationWorkspaceStore.js +214 -0
- package/dist/application/LinuxHopper.js +72 -48
- package/dist/application/ProcessCaptureAuthority.js +2 -2
- package/dist/application/ProcessCaptureError.js +12 -0
- package/dist/application/ProcessCaptureLifecycle.js +19 -1
- package/dist/application/ProcessCli.js +98 -31
- package/dist/application/ProcessHarness.js +2 -2
- package/dist/application/ReferenceSourceImportEntries.js +18 -3
- package/dist/application/ReferenceSourceImportTypes.js +32 -0
- package/dist/application/Setup.js +33 -60
- package/dist/application/SetupClients.js +19 -0
- package/dist/application/SetupInstallFailure.js +53 -0
- package/dist/application/SetupPlan.js +31 -0
- package/dist/application/Uninstall.js +33 -23
- package/dist/application/UnknownEvidence.js +33 -0
- package/dist/application/Upgrade.js +1 -1
- package/dist/application/runtime.js +1 -1
- package/dist/artifacts/ArtifactProvider.js +2 -5
- package/dist/cli.js +31 -11
- package/dist/cliEvidenceCommands.js +11 -12
- package/dist/cliInvestigationCommands.js +86 -0
- package/dist/cliOutput.js +41 -0
- package/dist/cliProcessCommands.js +14 -4
- package/dist/config.js +39 -24
- package/dist/contracts/promptContracts.js +256 -0
- package/dist/contracts/sessionLifecycleInputs.js +11 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +10 -6
- package/dist/contracts/toolOutputSchemas.js +33 -4
- package/dist/domain/analysisSnapshot.js +149 -0
- package/dist/domain/changedBehavior.js +30 -7
- package/dist/domain/errors.js +180 -41
- package/dist/domain/evidence.js +4 -2
- package/dist/domain/evidenceBundle.js +28 -0
- package/dist/domain/hopperStartupFailure.js +55 -0
- package/dist/domain/investigationWorkspace.js +214 -0
- package/dist/domain/jsonValue.js +2 -0
- package/dist/domain/nativeInspection.js +3 -2
- package/dist/domain/processCapture.js +5 -4
- package/dist/domain/processComparison.js +5 -4
- package/dist/hopper/BridgeLauncher.js +47 -20
- package/dist/hopper/HopperClient.js +17 -2
- package/dist/hopper/HopperProvider.js +1 -0
- package/dist/main.js +47 -24
- package/dist/server/createServer.js +2 -0
- package/dist/server/promptCompletion.js +144 -0
- package/dist/server/registerEnhancedTools.js +2 -6
- package/dist/server/registerEvidenceTools.js +2 -8
- package/dist/server/registerFunctionComparisonTool.js +1 -1
- package/dist/server/registerInvestigationTools.js +31 -11
- package/dist/server/registerOfficialTools.js +6 -13
- package/dist/server/registerProcessComparisonTool.js +1 -7
- package/dist/server/registerPrompts.js +67 -0
- package/dist/server/registerSessionStatusTool.js +11 -0
- package/dist/server/registerSessionTools.js +52 -18
- package/dist/server/sessionEvidence.js +2 -8
- package/dist/server/sessionToolPolicies.js +1 -42
- package/dist/server/toolResult.js +7 -3
- package/install.sh +11 -11
- package/package.json +5 -3
- package/scripts/hopper-demo-x11.py +349 -0
- package/scripts/rea.mjs +6 -3
- package/skills/rea-analysis/SKILL.md +9 -2
|
@@ -1,42 +1,69 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { parseConfig } from "../config.js";
|
|
3
|
+
import { AnalysisError, projectAnalysisError } from "../domain/errors.js";
|
|
3
4
|
import { createEvidence, parseEvidence } from "../domain/evidence.js";
|
|
4
5
|
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
5
6
|
import { compareProcessCaptures, LEGACY_PROCESS_CAPTURE_MESSAGE, parseProcessScenario, parseProcessCapture, } from "../domain/processCapture.js";
|
|
6
7
|
import { captureProcessScenario } from "./ProcessHarness.js";
|
|
7
8
|
import { PROCESS_PROVIDER, createProcessCaptureEvidence, } from "./ProcessEvidence.js";
|
|
8
9
|
const MAX_INPUT_BYTES = 64 * 1024 * 1024;
|
|
10
|
+
/** Distinguish a safe process-command failure from successful Evidence. */
|
|
11
|
+
export const isProcessCliErrorOutput = (value) => typeof value === "object" &&
|
|
12
|
+
value !== null &&
|
|
13
|
+
"error" in value &&
|
|
14
|
+
value.error === "Process command failed" &&
|
|
15
|
+
"category" in value &&
|
|
16
|
+
typeof value.category === "string" &&
|
|
17
|
+
"message" in value &&
|
|
18
|
+
typeof value.message === "string";
|
|
9
19
|
/** Capture one JSON scenario through the same policy and evidence contract as MCP. */
|
|
10
20
|
export const captureProcessScenarioFile = async (path) => {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
try {
|
|
22
|
+
const input = await readJson(path);
|
|
23
|
+
let scenario;
|
|
24
|
+
try {
|
|
25
|
+
scenario = parseProcessScenario(input);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new ProcessCliFailure("invalid_input", "Process scenario is invalid. Check its required fields and limits, then try again.");
|
|
29
|
+
}
|
|
30
|
+
const config = parseConfig(process.env);
|
|
31
|
+
if (!config.ok)
|
|
32
|
+
return cliAnalysisError(config.error);
|
|
33
|
+
const captured = await captureProcessScenario(scenario, config.value.processExecutionPolicy);
|
|
34
|
+
if (!captured.ok)
|
|
35
|
+
return cliAnalysisError(captured.error);
|
|
36
|
+
return createProcessCaptureEvidence(scenario, captured.value);
|
|
37
|
+
}
|
|
38
|
+
catch (cause) {
|
|
39
|
+
return projectProcessCliError(cause);
|
|
40
|
+
}
|
|
19
41
|
};
|
|
20
42
|
/** Compare two capture Evidence files and emit derived comparison Evidence. */
|
|
21
43
|
export const compareProcessEvidenceFiles = async (leftPath, rightPath) => {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
try {
|
|
45
|
+
const left = parseCaptureEvidence(await readJson(leftPath));
|
|
46
|
+
const right = parseCaptureEvidence(await readJson(rightPath));
|
|
47
|
+
const comparison = compareProcessCaptures(left.capture, right.capture);
|
|
48
|
+
return createEvidence(undefined, PROCESS_PROVIDER, {
|
|
49
|
+
predicateType: "rea.process-comparison/v3",
|
|
50
|
+
operation: "compare_process_captures",
|
|
51
|
+
parameters: {
|
|
52
|
+
left_evidence_id: left.id,
|
|
53
|
+
right_evidence_id: right.id,
|
|
54
|
+
left_normalization: left.capture.normalization,
|
|
55
|
+
right_normalization: right.capture.normalization,
|
|
56
|
+
},
|
|
57
|
+
result: jsonValueSchema.parse(comparison),
|
|
58
|
+
confidence: "derived",
|
|
59
|
+
authority: "analyst-inference",
|
|
60
|
+
limitations: comparison.limitations,
|
|
61
|
+
evidenceLinks: [left.id, right.id],
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (cause) {
|
|
65
|
+
return projectProcessCliError(cause);
|
|
66
|
+
}
|
|
40
67
|
};
|
|
41
68
|
const parseCaptureEvidence = (input) => {
|
|
42
69
|
const evidence = parseEvidence(input);
|
|
@@ -45,8 +72,8 @@ const parseCaptureEvidence = (input) => {
|
|
|
45
72
|
evidence.provider.id !== PROCESS_PROVIDER.id ||
|
|
46
73
|
evidence.provider.version !== PROCESS_PROVIDER.version) {
|
|
47
74
|
if (evidence.predicate_type === "rea.process-capture/v3")
|
|
48
|
-
throw new
|
|
49
|
-
throw new
|
|
75
|
+
throw new ProcessCliFailure("invalid_input", LEGACY_PROCESS_CAPTURE_MESSAGE);
|
|
76
|
+
throw new ProcessCliFailure("invalid_input", "Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.");
|
|
50
77
|
}
|
|
51
78
|
return {
|
|
52
79
|
id: evidence.evidence_id,
|
|
@@ -54,8 +81,48 @@ const parseCaptureEvidence = (input) => {
|
|
|
54
81
|
};
|
|
55
82
|
};
|
|
56
83
|
const readJson = async (path) => {
|
|
57
|
-
|
|
84
|
+
let bytes;
|
|
85
|
+
try {
|
|
86
|
+
bytes = await readFile(path);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new ProcessCliFailure("invalid_input", "Process input file could not be read. Check that the path exists and is readable.");
|
|
90
|
+
}
|
|
58
91
|
if (bytes.length > MAX_INPUT_BYTES)
|
|
59
|
-
throw new
|
|
60
|
-
|
|
92
|
+
throw new ProcessCliFailure("truncated", "Process input file is too large. Reduce it below 64 MiB, then try again.");
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(bytes.toString("utf8"));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new ProcessCliFailure("invalid_input", "Process input file is not valid JSON. Repair the file, then try again.");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
class ProcessCliFailure extends Error {
|
|
101
|
+
category;
|
|
102
|
+
userMessage;
|
|
103
|
+
constructor(category, userMessage) {
|
|
104
|
+
super(userMessage);
|
|
105
|
+
this.category = category;
|
|
106
|
+
this.userMessage = userMessage;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const cliAnalysisError = (error) => ({
|
|
110
|
+
error: "Process command failed",
|
|
111
|
+
...projectAnalysisError(error),
|
|
112
|
+
});
|
|
113
|
+
/** Project any process CLI failure without exposing its cause. */
|
|
114
|
+
export const projectProcessCliError = (cause) => {
|
|
115
|
+
if (cause instanceof ProcessCliFailure)
|
|
116
|
+
return {
|
|
117
|
+
error: "Process command failed",
|
|
118
|
+
category: cause.category,
|
|
119
|
+
message: cause.userMessage,
|
|
120
|
+
};
|
|
121
|
+
if (cause instanceof AnalysisError)
|
|
122
|
+
return cliAnalysisError(cause);
|
|
123
|
+
return {
|
|
124
|
+
error: "Process command failed",
|
|
125
|
+
category: "execution_failure",
|
|
126
|
+
message: "Process command could not complete. Check the input files and run `rea doctor`, then try again.",
|
|
127
|
+
};
|
|
61
128
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node-pty";
|
|
2
2
|
import { validateProcessCapture } from "../domain/processCapture.js";
|
|
3
3
|
import { err, ok } from "../domain/result.js";
|
|
4
|
-
import { ProcessCaptureError } from "./ProcessCaptureError.js";
|
|
4
|
+
import { ProcessCaptureError, processCaptureCancelled, } from "./ProcessCaptureError.js";
|
|
5
5
|
export { ProcessCaptureError } from "./ProcessCaptureError.js";
|
|
6
6
|
import { startLoopbackReplay } from "./LoopbackReplay.js";
|
|
7
7
|
import { startProcessSampler } from "./ProcessSampling.js";
|
|
@@ -218,7 +218,7 @@ const completeCapture = async (options) => {
|
|
|
218
218
|
runtime.checkpoints.trigger("root_exit");
|
|
219
219
|
const { reason } = options.exit;
|
|
220
220
|
if (reason === "cancelled")
|
|
221
|
-
throw
|
|
221
|
+
throw processCaptureCancelled();
|
|
222
222
|
const settlement = await observeSettlement(options.runId, [
|
|
223
223
|
runtime.terminal.pid,
|
|
224
224
|
...options.samples.flatMap(({ process_group_id }) => process_group_id === null ? [] : [process_group_id]),
|
|
@@ -3,16 +3,31 @@ import { posix } from "node:path";
|
|
|
3
3
|
import { classifyReferenceSourcePath, detectReferenceSourceLanguage, } from "../domain/referenceSourceClassification.js";
|
|
4
4
|
import { parseReferenceSourceImports } from "../domain/referenceSourceImportParsing.js";
|
|
5
5
|
import { PARSEABLE_REFERENCE_SOURCE_LANGUAGES } from "./ReferenceSourceImportTypes.js";
|
|
6
|
+
/** Project a low-level entry failure into safe import guidance. */
|
|
7
|
+
export const projectReferenceSourceEntryFailure = (entry) => {
|
|
8
|
+
if (entry.code === "cancelled")
|
|
9
|
+
return "This entry was not read because the import was cancelled. Start the import again when ready.";
|
|
10
|
+
if (entry.code === "limit")
|
|
11
|
+
return "This entry was not read because an import limit was reached. Import a smaller directory or raise the configured limits.";
|
|
12
|
+
if (entry.code === "unsupported")
|
|
13
|
+
return "This entry cannot be read safely on this system. Exclude it or import the directory on a supported system.";
|
|
14
|
+
if (entry.kind === "directory")
|
|
15
|
+
return "This directory could not be read. Check its permissions, then try again.";
|
|
16
|
+
if (entry.kind === "symlink")
|
|
17
|
+
return "This symbolic link could not be read safely. Check the link and its permissions, then try again.";
|
|
18
|
+
return "This file could not be read. Check its permissions, then try again.";
|
|
19
|
+
};
|
|
6
20
|
const hashBytes = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
7
21
|
const failedEntry = (entry) => {
|
|
8
22
|
const classifications = classifyReferenceSourcePath(entry.path);
|
|
23
|
+
const limitation = projectReferenceSourceEntryFailure(entry);
|
|
9
24
|
if (entry.kind === "directory")
|
|
10
25
|
return {
|
|
11
26
|
path: entry.path,
|
|
12
27
|
kind: "directory",
|
|
13
28
|
classifications,
|
|
14
29
|
tree_state: entry.code === "limit" ? "partial" : "unreadable",
|
|
15
|
-
limitations: [
|
|
30
|
+
limitations: [limitation],
|
|
16
31
|
};
|
|
17
32
|
if (entry.kind === "symlink")
|
|
18
33
|
return {
|
|
@@ -21,7 +36,7 @@ const failedEntry = (entry) => {
|
|
|
21
36
|
target: "<unreadable>",
|
|
22
37
|
target_state: "unreadable",
|
|
23
38
|
classifications,
|
|
24
|
-
limitations: [
|
|
39
|
+
limitations: [limitation],
|
|
25
40
|
};
|
|
26
41
|
return {
|
|
27
42
|
path: entry.path,
|
|
@@ -31,7 +46,7 @@ const failedEntry = (entry) => {
|
|
|
31
46
|
language: null,
|
|
32
47
|
classifications: entry.kind === "file" ? classifications : ["unknown"],
|
|
33
48
|
content_state: entry.code === "limit" ? "too-large" : "unreadable",
|
|
34
|
-
limitations: [
|
|
49
|
+
limitations: [limitation],
|
|
35
50
|
};
|
|
36
51
|
};
|
|
37
52
|
const resolveInternalSpecifier = (fromPath, specifier, filePaths) => {
|
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
/** Safe CLI projection for a historical-source import failure. */
|
|
2
|
+
export const projectReferenceSourceImportError = (error) => {
|
|
3
|
+
if (error.code === "cancelled")
|
|
4
|
+
return {
|
|
5
|
+
category: "cancelled",
|
|
6
|
+
message: "Reference source import was cancelled. Start it again when ready.",
|
|
7
|
+
};
|
|
8
|
+
if (error.code === "invalid-limits")
|
|
9
|
+
return {
|
|
10
|
+
category: "invalid_input",
|
|
11
|
+
message: "Reference source limits are invalid. Use positive integer limits, then try again.",
|
|
12
|
+
};
|
|
13
|
+
if (error.code === "invalid-root")
|
|
14
|
+
return {
|
|
15
|
+
category: "invalid_input",
|
|
16
|
+
message: "Reference source directory could not be opened. Check that the path exists, is readable, and points to a directory.",
|
|
17
|
+
};
|
|
18
|
+
if (error.code === "policy")
|
|
19
|
+
return {
|
|
20
|
+
category: "permission_required",
|
|
21
|
+
message: "Reference source directory is not approved. Add its directory to `REA_REFERENCE_ROOTS_JSON`, restart REA, then try again.",
|
|
22
|
+
};
|
|
23
|
+
if (error.code === "io")
|
|
24
|
+
return {
|
|
25
|
+
category: "execution_failure",
|
|
26
|
+
message: "Reference source files could not be read. Check directory permissions and try again.",
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
category: "execution_failure",
|
|
30
|
+
message: "Reference source could not be indexed. Check that the source tree is readable, then try again.",
|
|
31
|
+
};
|
|
32
|
+
};
|
|
1
33
|
export const DEFAULT_REFERENCE_SOURCE_IGNORE_PATTERNS = [
|
|
2
34
|
".git/",
|
|
3
35
|
".git/hooks/",
|
|
@@ -9,50 +9,14 @@ import { supportsNodeVersion } from "../domain/runtimeVersion.js";
|
|
|
9
9
|
import { runDoctor, systemDoctorHost } from "./Doctor.js";
|
|
10
10
|
import { installLinuxHopper, readLinuxDistribution, } from "./LinuxHopper.js";
|
|
11
11
|
import { installMacHopper } from "./MacHopper.js";
|
|
12
|
+
import { configureDetectedClients } from "./SetupClients.js";
|
|
12
13
|
import { installCanonicalSkill } from "./SetupSkill.js";
|
|
14
|
+
import { setupPlan } from "./SetupPlan.js";
|
|
15
|
+
import { setupInstallFailure, } from "./SetupInstallFailure.js";
|
|
13
16
|
export { installCanonicalSkill } from "./SetupSkill.js";
|
|
14
17
|
const registrationCommand = () => process.env.npm_command === "exec"
|
|
15
18
|
? ["npx", "-y", PRODUCT_IDENTITY.packageName, "mcp"]
|
|
16
19
|
: [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
|
|
17
|
-
const setupPlan = (platform, hopperPath, clients) => [
|
|
18
|
-
...(hopperPath === undefined
|
|
19
|
-
? [
|
|
20
|
-
{
|
|
21
|
-
kind: "install_hopper",
|
|
22
|
-
target: platform === "darwin"
|
|
23
|
-
? "~/Applications/Hopper Disassembler.app"
|
|
24
|
-
: "system package manager",
|
|
25
|
-
detail: "Download the official Hopper package, verify it, install it, and open Hopper for activation.",
|
|
26
|
-
external: true,
|
|
27
|
-
},
|
|
28
|
-
]
|
|
29
|
-
: []),
|
|
30
|
-
...clients
|
|
31
|
-
.filter(({ format }) => format !== "unsupported")
|
|
32
|
-
.map((client) => ({
|
|
33
|
-
kind: "configure_client",
|
|
34
|
-
target: client.configPath,
|
|
35
|
-
detail: `Add the REA MCP registration for ${client.name}; preserve unrelated configuration.`,
|
|
36
|
-
external: false,
|
|
37
|
-
})),
|
|
38
|
-
{
|
|
39
|
-
kind: "install_skill",
|
|
40
|
-
target: "~/.agents/skills/rea-analysis/SKILL.md",
|
|
41
|
-
detail: "Install or update the bundled REA analysis skill.",
|
|
42
|
-
external: false,
|
|
43
|
-
},
|
|
44
|
-
];
|
|
45
|
-
const configureDetectedClients = async (options) => {
|
|
46
|
-
for (const client of options.detectedClients) {
|
|
47
|
-
const result = await options.host.configureClient(client, options.hopperPath, registrationCommand());
|
|
48
|
-
options.clients[client.name] = result;
|
|
49
|
-
if (result.status === "failed")
|
|
50
|
-
return `${client.name} configuration ${result.reason} verification failed; no successful configuration was reported.`;
|
|
51
|
-
if (result.status === "configured")
|
|
52
|
-
options.appliedActions.push(`configured_${client.name}`);
|
|
53
|
-
}
|
|
54
|
-
return undefined;
|
|
55
|
-
};
|
|
56
20
|
/**
|
|
57
21
|
* Install prerequisites and configure detected clients idempotently.
|
|
58
22
|
* Discovery always precedes mutation. Interactive confirmation or explicit
|
|
@@ -74,8 +38,13 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
|
74
38
|
if (unsupported !== undefined)
|
|
75
39
|
return fail(unsupported);
|
|
76
40
|
let hopperPath = await host.hopperPath();
|
|
41
|
+
const initialDoctor = await host.doctor();
|
|
42
|
+
const linuxHopperRepairNeeded = initialDoctor.checks.some(({ name, ok, detail }) => !ok &&
|
|
43
|
+
(name === "hopper-demo-runtime" ||
|
|
44
|
+
(name === "hopper-version" && detail === "/opt/hopper/bin/Hopper")));
|
|
45
|
+
const installHopper = hopperPath === undefined || linuxHopperRepairNeeded;
|
|
77
46
|
const detectedClients = await host.detectedClients();
|
|
78
|
-
plannedActions = setupPlan(host.platform,
|
|
47
|
+
plannedActions = setupPlan(host.platform, installHopper, detectedClients);
|
|
79
48
|
let approved = options.approved;
|
|
80
49
|
let interactiveApproval = false;
|
|
81
50
|
if (!approved && confirm !== undefined && !options.structured) {
|
|
@@ -91,24 +60,26 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
|
91
60
|
doctor: await host.doctor(),
|
|
92
61
|
remediation: "Review the setup plan, then rerun interactively or with --yes.",
|
|
93
62
|
};
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (hopperPath === undefined)
|
|
63
|
+
if (installHopper && (interactiveApproval || options.installHopper)) {
|
|
64
|
+
const installed = await host.installHopper();
|
|
65
|
+
if (installed.status === "failed")
|
|
98
66
|
return {
|
|
99
67
|
status: "needs_human",
|
|
100
68
|
plannedActions,
|
|
101
69
|
appliedActions,
|
|
102
70
|
clients,
|
|
103
71
|
doctor: await host.doctor(),
|
|
104
|
-
|
|
72
|
+
code: installed.code,
|
|
73
|
+
remediation: installed.remediation,
|
|
105
74
|
};
|
|
75
|
+
hopperPath = installed.launcherPath;
|
|
106
76
|
appliedActions.push("installed_hopper");
|
|
107
77
|
}
|
|
108
78
|
const clientFailure = await configureDetectedClients({
|
|
109
79
|
host,
|
|
110
80
|
detectedClients,
|
|
111
81
|
hopperPath,
|
|
82
|
+
command: registrationCommand(),
|
|
112
83
|
clients,
|
|
113
84
|
appliedActions,
|
|
114
85
|
});
|
|
@@ -116,29 +87,29 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
|
116
87
|
return fail(clientFailure);
|
|
117
88
|
const skill = await host.installSkill();
|
|
118
89
|
if (skill === "failed")
|
|
119
|
-
return fail("
|
|
90
|
+
return fail("REA analysis skill could not be installed or verified. Check permissions for `~/.agents/skills`, then rerun setup.");
|
|
120
91
|
if (skill === "installed")
|
|
121
92
|
appliedActions.push("installed_skill");
|
|
122
93
|
const doctor = await host.doctor();
|
|
123
|
-
const
|
|
124
|
-
const ready = doctor.healthy && !activationRequired;
|
|
94
|
+
const remediation = finalSetupRemediation(host.platform, appliedActions.includes("installed_hopper"), doctor.healthy, hopperPath);
|
|
125
95
|
return {
|
|
126
|
-
status:
|
|
96
|
+
status: remediation === undefined ? "ready" : "needs_human",
|
|
127
97
|
plannedActions,
|
|
128
98
|
appliedActions,
|
|
129
99
|
clients,
|
|
130
100
|
doctor,
|
|
131
|
-
...(
|
|
132
|
-
? {}
|
|
133
|
-
: {
|
|
134
|
-
remediation: activationRequired
|
|
135
|
-
? "Open Hopper, complete its one-time activation, then rerun rea doctor --json."
|
|
136
|
-
: hopperPath === undefined
|
|
137
|
-
? "Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis."
|
|
138
|
-
: "Run rea doctor and apply each reported remediation.",
|
|
139
|
-
}),
|
|
101
|
+
...(remediation === undefined ? {} : { remediation }),
|
|
140
102
|
};
|
|
141
103
|
};
|
|
104
|
+
const finalSetupRemediation = (platform, installedHopper, healthy, hopperPath) => {
|
|
105
|
+
if (platform === "darwin" && installedHopper)
|
|
106
|
+
return "Open Hopper, choose its demo mode or activate a license, then rerun rea doctor --json.";
|
|
107
|
+
if (healthy)
|
|
108
|
+
return undefined;
|
|
109
|
+
return hopperPath === undefined
|
|
110
|
+
? "Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis."
|
|
111
|
+
: "Run rea doctor and apply each reported remediation.";
|
|
112
|
+
};
|
|
142
113
|
const hostRemediation = async (host) => {
|
|
143
114
|
if (host.platform !== "darwin" && host.platform !== "linux")
|
|
144
115
|
return "REA supports Hopper on macOS and selected 64-bit Linux distributions.";
|
|
@@ -167,7 +138,9 @@ const systemSetupHost = () => {
|
|
|
167
138
|
const result = process.platform === "linux"
|
|
168
139
|
? await installLinuxHopper()
|
|
169
140
|
: await installMacHopper();
|
|
170
|
-
|
|
141
|
+
if (result.status === "installed")
|
|
142
|
+
return result;
|
|
143
|
+
return setupInstallFailure(result.reason);
|
|
171
144
|
},
|
|
172
145
|
detectedClients: () => detectClients(homedir()),
|
|
173
146
|
configureClient: (client, hopperPath, command) => client.format === "unsupported"
|
|
@@ -179,7 +152,7 @@ const systemSetupHost = () => {
|
|
|
179
152
|
doctor: () => runDoctor(undefined, doctorHost),
|
|
180
153
|
};
|
|
181
154
|
};
|
|
182
|
-
/** Detect supported
|
|
155
|
+
/** Detect supported agents from stable per-user installation markers. */
|
|
183
156
|
export const detectClients = async (home) => {
|
|
184
157
|
const detected = [];
|
|
185
158
|
for (const candidate of supportedClients(home))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const failedConfigurationMessage = (reason) => {
|
|
2
|
+
if (reason === "backup")
|
|
3
|
+
return "Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup.";
|
|
4
|
+
if (reason === "write")
|
|
5
|
+
return "Agent configuration could not be updated. Check file permissions, then rerun setup.";
|
|
6
|
+
return "Agent configuration could not be verified after writing. Repair the configuration file or restore its `.rea.backup`, then rerun setup.";
|
|
7
|
+
};
|
|
8
|
+
/** Configure each detected agent, stopping after the first failed transaction. */
|
|
9
|
+
export const configureDetectedClients = async (options) => {
|
|
10
|
+
for (const client of options.detectedClients) {
|
|
11
|
+
const result = await options.host.configureClient(client, options.hopperPath, options.command);
|
|
12
|
+
options.clients[client.name] = result;
|
|
13
|
+
if (result.status === "failed")
|
|
14
|
+
return failedConfigurationMessage(result.reason);
|
|
15
|
+
if (result.status === "configured")
|
|
16
|
+
options.appliedActions.push(`configured_${client.name}`);
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Map provider installer failures to stable setup codes and safe recovery. */
|
|
2
|
+
export const setupInstallFailure = (reason) => {
|
|
3
|
+
switch (reason) {
|
|
4
|
+
case "unsupported_host":
|
|
5
|
+
return {
|
|
6
|
+
status: "failed",
|
|
7
|
+
code: "unsupported_host",
|
|
8
|
+
remediation: "Use a supported host, then rerun rea setup.",
|
|
9
|
+
};
|
|
10
|
+
case "integrity":
|
|
11
|
+
return {
|
|
12
|
+
status: "failed",
|
|
13
|
+
code: "integrity_mismatch",
|
|
14
|
+
remediation: "The Hopper download failed integrity verification. Retry setup; if it repeats, update REA before installing.",
|
|
15
|
+
};
|
|
16
|
+
case "authorization_or_package_manager":
|
|
17
|
+
return {
|
|
18
|
+
status: "failed",
|
|
19
|
+
code: "authorization_or_package_manager_failed",
|
|
20
|
+
remediation: "Allow the system package-manager operation or install the planned Hopper package and Linux runtime dependencies, then rerun rea setup.",
|
|
21
|
+
};
|
|
22
|
+
case "launcher_missing":
|
|
23
|
+
return {
|
|
24
|
+
status: "failed",
|
|
25
|
+
code: "launcher_missing",
|
|
26
|
+
remediation: "The package operation completed but Hopper is not runnable. Run rea doctor and repair the reported launcher issue.",
|
|
27
|
+
};
|
|
28
|
+
case "runtime_dependencies":
|
|
29
|
+
return {
|
|
30
|
+
status: "failed",
|
|
31
|
+
code: "runtime_dependency_unavailable",
|
|
32
|
+
remediation: "Hopper was installed but required shared libraries are missing. Rerun rea setup, then apply the hopper-demo-runtime remediation from rea doctor.",
|
|
33
|
+
};
|
|
34
|
+
case "unsupported_hopper_build":
|
|
35
|
+
return {
|
|
36
|
+
status: "failed",
|
|
37
|
+
code: "unsupported_hopper_build",
|
|
38
|
+
remediation: "The installed Hopper launcher does not match the build supported by this REA release. Unset HOPPER_LAUNCHER_PATH, reinstall through rea setup, or update REA.",
|
|
39
|
+
};
|
|
40
|
+
case "cancelled":
|
|
41
|
+
return {
|
|
42
|
+
status: "failed",
|
|
43
|
+
code: "setup_cancelled",
|
|
44
|
+
remediation: "Setup was cancelled before Hopper was installed. Rerun rea setup when ready.",
|
|
45
|
+
};
|
|
46
|
+
default:
|
|
47
|
+
return {
|
|
48
|
+
status: "failed",
|
|
49
|
+
code: "download_failed",
|
|
50
|
+
remediation: "REA could not download or unpack the official Hopper package. Check network and filesystem access, then retry setup.",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Build the complete setup mutation plan before approval. */
|
|
2
|
+
export const setupPlan = (platform, installHopper, clients) => [
|
|
3
|
+
...(installHopper
|
|
4
|
+
? [
|
|
5
|
+
{
|
|
6
|
+
kind: "install_hopper",
|
|
7
|
+
target: platform === "darwin"
|
|
8
|
+
? "~/Applications/Hopper Disassembler.app"
|
|
9
|
+
: "system package manager",
|
|
10
|
+
detail: platform === "linux"
|
|
11
|
+
? "Download, verify, and install Hopper plus its Xvfb demo-session dependencies. For the supported demo build, REA uses a private display and selects Hopper's offered demo mode for each analysis session."
|
|
12
|
+
: "Download the official Hopper package, verify it, and install it. Hopper may show its demo or license prompt when first opened.",
|
|
13
|
+
external: true,
|
|
14
|
+
},
|
|
15
|
+
]
|
|
16
|
+
: []),
|
|
17
|
+
...clients
|
|
18
|
+
.filter(({ format }) => format !== "unsupported")
|
|
19
|
+
.map((client) => ({
|
|
20
|
+
kind: "configure_client",
|
|
21
|
+
target: client.configPath,
|
|
22
|
+
detail: `Add the REA MCP registration for ${client.name}; preserve unrelated configuration.`,
|
|
23
|
+
external: false,
|
|
24
|
+
})),
|
|
25
|
+
{
|
|
26
|
+
kind: "install_skill",
|
|
27
|
+
target: "~/.agents/skills/rea-analysis/SKILL.md",
|
|
28
|
+
detail: "Install or update the bundled REA analysis skill.",
|
|
29
|
+
external: false,
|
|
30
|
+
},
|
|
31
|
+
];
|