rea-agents 1.1.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 +94 -28
- 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 +84 -44
- 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/CommandShimReplay.js +136 -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 +16 -101
- 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 +25 -0
- package/dist/application/ProcessCaptureCapability.js +23 -0
- package/dist/application/ProcessCaptureError.js +17 -0
- package/dist/application/ProcessCaptureLifecycle.js +287 -0
- package/dist/application/ProcessCheckpoints.js +129 -0
- package/dist/application/ProcessCli.js +128 -0
- package/dist/application/ProcessEvidence.js +38 -0
- package/dist/application/ProcessHarness.js +252 -272
- package/dist/application/ProcessNormalization.js +10 -1
- package/dist/application/ProcessOwnership.js +39 -1
- package/dist/application/ProcessSampling.js +39 -23
- package/dist/application/ReferenceSourceImportEntries.js +18 -3
- package/dist/application/ReferenceSourceImportTypes.js +32 -0
- package/dist/application/Setup.js +44 -96
- package/dist/application/SetupClients.js +19 -0
- package/dist/application/SetupInstallFailure.js +53 -0
- package/dist/application/SetupPlan.js +31 -0
- package/dist/application/SetupSkill.js +44 -0
- package/dist/application/TerminalRenderer.js +92 -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 +2 -2
- package/dist/artifacts/ArtifactProvider.js +20 -11
- package/dist/artifacts/ArtifactReader.js +3 -1
- package/dist/artifacts/AsarArtifactReader.js +8 -1
- package/dist/artifacts/DirectoryArtifactReader.js +1 -0
- package/dist/artifacts/MachOSliceArtifactReader.js +1 -0
- package/dist/artifacts/NativeDmgArtifactReader.js +151 -0
- package/dist/artifacts/ZipArtifactReader.js +1 -0
- package/dist/cli.js +57 -32
- package/dist/cliEvidenceCommands.js +11 -12
- package/dist/cliInvestigationCommands.js +86 -0
- package/dist/cliOutput.js +41 -0
- package/dist/cliProcessCommands.js +29 -0
- package/dist/config.js +41 -24
- package/dist/contracts/artifactToolContracts.js +1 -0
- package/dist/contracts/investigationExamples.js +3 -3
- package/dist/contracts/processCaptureExample.js +50 -7
- 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 +13 -8
- package/dist/contracts/toolOutputSchemas.js +33 -4
- package/dist/domain/analysisSnapshot.js +149 -0
- package/dist/domain/changedBehavior.js +32 -9
- package/dist/domain/errors.js +186 -39
- 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 +100 -293
- package/dist/domain/processCaptureValidation.js +124 -0
- package/dist/domain/processComparison.js +177 -32
- package/dist/domain/processScenario.js +370 -0
- package/dist/domain/reconstructionVerification.js +3 -3
- package/dist/domain/staticRuntimeCorrelation.js +3 -2
- package/dist/hopper/BridgeLauncher.js +47 -20
- package/dist/hopper/HopperClient.js +17 -2
- package/dist/hopper/HopperProvider.js +1 -0
- package/dist/identity.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 +28 -14
- package/dist/server/registerPrompts.js +67 -0
- package/dist/server/registerSessionStatusTool.js +11 -0
- package/dist/server/registerSessionTools.js +55 -43
- package/dist/server/sessionEvidence.js +2 -8
- package/dist/server/sessionToolPolicies.js +2 -48
- package/dist/server/toolResult.js +7 -3
- package/install.sh +11 -11
- package/package.json +9 -3
- package/scripts/hopper-demo-x11.py +349 -0
- package/scripts/prepare-node-pty.mjs +35 -0
- package/scripts/rea.mjs +6 -3
- package/skills/rea-analysis/SKILL.md +27 -5
|
@@ -43,7 +43,13 @@ const systemHost = {
|
|
|
43
43
|
process.kill(-processGroupId, signal);
|
|
44
44
|
},
|
|
45
45
|
};
|
|
46
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Verify run-token ownership before signaling a POSIX process group.
|
|
48
|
+
*
|
|
49
|
+
* Process IDs and group IDs can be reused. REA therefore re-reads every live
|
|
50
|
+
* member's environment immediately before signaling and fails closed if any
|
|
51
|
+
* member cannot be inspected or lacks the per-capture run token.
|
|
52
|
+
*/
|
|
47
53
|
export const cleanupOwnedProcessGroup = async (ownership, host = systemHost) => {
|
|
48
54
|
let members;
|
|
49
55
|
try {
|
|
@@ -97,6 +103,38 @@ export const cleanupOwnedProcessGroup = async (ownership, host = systemHost) =>
|
|
|
97
103
|
}
|
|
98
104
|
return { cleaned: true, signaled: true };
|
|
99
105
|
};
|
|
106
|
+
/** Observe one group without signaling it, failing closed on identity doubt. */
|
|
107
|
+
export const observeOwnedProcessGroup = async (ownership, host = systemHost) => {
|
|
108
|
+
let members;
|
|
109
|
+
try {
|
|
110
|
+
members = await host.listMembers(ownership.processGroupId);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return {
|
|
114
|
+
state: "unverifiable",
|
|
115
|
+
reason: "process group could not be inspected",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (members.length === 0)
|
|
119
|
+
return { state: "empty" };
|
|
120
|
+
for (const member of members) {
|
|
121
|
+
try {
|
|
122
|
+
if ((await host.environment(member.pid)).REA_PROCESS_RUN_ID !==
|
|
123
|
+
ownership.runId)
|
|
124
|
+
return {
|
|
125
|
+
state: "unverifiable",
|
|
126
|
+
reason: "process ownership did not match",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return {
|
|
131
|
+
state: "unverifiable",
|
|
132
|
+
reason: "process ownership could not be revalidated",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { state: "alive" };
|
|
137
|
+
};
|
|
100
138
|
const commandMatches = (actual, expected) => {
|
|
101
139
|
const normalizedActual = actual.trim();
|
|
102
140
|
const normalizedExpected = expected.trim();
|
|
@@ -24,17 +24,25 @@ const parseProcStat = (identifier, stat) => {
|
|
|
24
24
|
.split(/\s+/);
|
|
25
25
|
const pid = Number(identifier);
|
|
26
26
|
const parentPid = Number(fields[1]);
|
|
27
|
+
const processGroupId = Number(fields[2]);
|
|
28
|
+
const sessionId = Number(fields[3]);
|
|
27
29
|
const startTime = fields[19];
|
|
28
30
|
if (!Number.isSafeInteger(pid) ||
|
|
29
31
|
pid <= 0 ||
|
|
30
32
|
!Number.isSafeInteger(parentPid) ||
|
|
31
33
|
parentPid < 0 ||
|
|
34
|
+
!Number.isSafeInteger(processGroupId) ||
|
|
35
|
+
processGroupId <= 0 ||
|
|
36
|
+
!Number.isSafeInteger(sessionId) ||
|
|
37
|
+
sessionId <= 0 ||
|
|
32
38
|
startTime === undefined ||
|
|
33
39
|
!/^\d+$/.test(startTime))
|
|
34
40
|
return undefined;
|
|
35
41
|
return {
|
|
36
42
|
pid,
|
|
37
43
|
parent_pid: parentPid,
|
|
44
|
+
process_group_id: processGroupId,
|
|
45
|
+
session_id: sessionId,
|
|
38
46
|
command: "",
|
|
39
47
|
startTime,
|
|
40
48
|
};
|
|
@@ -104,13 +112,15 @@ const inspectProcess = async (pid, expectedParent, signal, identities) => {
|
|
|
104
112
|
return {
|
|
105
113
|
pid: before.pid,
|
|
106
114
|
parent_pid: before.parent_pid,
|
|
115
|
+
process_group_id: before.process_group_id,
|
|
116
|
+
session_id: before.session_id,
|
|
107
117
|
startTime: before.startTime,
|
|
108
118
|
command,
|
|
109
119
|
children: children ?? [],
|
|
110
120
|
};
|
|
111
121
|
};
|
|
112
122
|
const sampleLinux = async (context) => {
|
|
113
|
-
const { rootPid, limit, signal,
|
|
123
|
+
const { rootPid, limit, signal, identities } = context;
|
|
114
124
|
if (limit <= 0)
|
|
115
125
|
return [];
|
|
116
126
|
const rootStat = await readProcessStat(rootPid, signal);
|
|
@@ -131,11 +141,9 @@ const sampleLinux = async (context) => {
|
|
|
131
141
|
for (const node of batchResults) {
|
|
132
142
|
if (node === undefined)
|
|
133
143
|
continue;
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
rows.push(node);
|
|
138
|
-
}
|
|
144
|
+
if (rows.length >= limit)
|
|
145
|
+
break;
|
|
146
|
+
rows.push(node);
|
|
139
147
|
if (rows.length >= limit)
|
|
140
148
|
break;
|
|
141
149
|
for (const child of node.children) {
|
|
@@ -152,15 +160,17 @@ const sampleLinux = async (context) => {
|
|
|
152
160
|
return rows;
|
|
153
161
|
};
|
|
154
162
|
const readPsRows = async (signal) => {
|
|
155
|
-
const { stdout } = await execFileAsync("ps", ["-axo", "pid=,ppid=,command="], { signal });
|
|
163
|
+
const { stdout } = await execFileAsync("ps", ["-axo", "pid=,ppid=,pgid=,sess=,command="], { signal });
|
|
156
164
|
return stdout
|
|
157
165
|
.split("\n")
|
|
158
|
-
.map((line) => /\s*(\d+)\s+(\d+)\s+(.*)/u.exec(line))
|
|
166
|
+
.map((line) => /\s*(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(.*)/u.exec(line))
|
|
159
167
|
.filter((match) => match !== null)
|
|
160
168
|
.map((match) => ({
|
|
161
169
|
pid: Number(match[1]),
|
|
162
170
|
parent_pid: Number(match[2]),
|
|
163
|
-
|
|
171
|
+
process_group_id: Number(match[3]),
|
|
172
|
+
session_id: Number(match[4]),
|
|
173
|
+
command: match[5] ?? "",
|
|
164
174
|
startTime: undefined,
|
|
165
175
|
}))
|
|
166
176
|
.filter((row) => Number.isSafeInteger(row.pid) &&
|
|
@@ -170,7 +180,7 @@ const readPsRows = async (signal) => {
|
|
|
170
180
|
.sort((left, right) => left.pid - right.pid);
|
|
171
181
|
};
|
|
172
182
|
const samplePs = async (context) => {
|
|
173
|
-
const { rootPid, limit, signal
|
|
183
|
+
const { rootPid, limit, signal } = context;
|
|
174
184
|
if (limit <= 0)
|
|
175
185
|
return [];
|
|
176
186
|
const rows = await readPsRows(signal);
|
|
@@ -194,9 +204,8 @@ const samplePs = async (context) => {
|
|
|
194
204
|
continue;
|
|
195
205
|
visited.add(pid);
|
|
196
206
|
const row = rowByPid.get(pid);
|
|
197
|
-
if (row !== undefined
|
|
207
|
+
if (row !== undefined)
|
|
198
208
|
result.push(row);
|
|
199
|
-
}
|
|
200
209
|
if (result.length >= limit)
|
|
201
210
|
break;
|
|
202
211
|
for (const child of childrenByParent.get(pid) ?? []) {
|
|
@@ -212,11 +221,9 @@ const sampleProcesses = async (context) => {
|
|
|
212
221
|
const rows = process.platform === "linux"
|
|
213
222
|
? await sampleLinux(context)
|
|
214
223
|
: await samplePs(context);
|
|
215
|
-
const { elapsedMs,
|
|
224
|
+
const { elapsedMs, identities } = context;
|
|
216
225
|
const samples = [];
|
|
217
226
|
for (const row of rows) {
|
|
218
|
-
if (sampledPids.has(row.pid))
|
|
219
|
-
continue;
|
|
220
227
|
if (row.startTime !== undefined) {
|
|
221
228
|
const existing = identities.get(row.pid);
|
|
222
229
|
if (existing !== undefined && existing !== row.startTime)
|
|
@@ -228,6 +235,8 @@ const sampleProcesses = async (context) => {
|
|
|
228
235
|
pid: row.pid,
|
|
229
236
|
parent_pid: row.parent_pid,
|
|
230
237
|
command: row.command,
|
|
238
|
+
process_group_id: row.process_group_id,
|
|
239
|
+
session_id: row.session_id,
|
|
231
240
|
});
|
|
232
241
|
}
|
|
233
242
|
return samples;
|
|
@@ -235,31 +244,38 @@ const sampleProcesses = async (context) => {
|
|
|
235
244
|
/** Start bounded process-tree sampling and expose an awaited stop. */
|
|
236
245
|
export const startProcessSampler = (rootPid, started, limit, samples) => {
|
|
237
246
|
const identities = new Map();
|
|
238
|
-
const
|
|
247
|
+
const lastObservations = new Map();
|
|
239
248
|
let pending;
|
|
240
249
|
let stopped = false;
|
|
241
250
|
let abortCurrent;
|
|
242
251
|
let partial = false;
|
|
243
252
|
const sample = () => {
|
|
244
|
-
if (stopped || pending !== undefined
|
|
253
|
+
if (stopped || pending !== undefined)
|
|
245
254
|
return;
|
|
246
255
|
const controller = new AbortController();
|
|
247
256
|
abortCurrent = () => controller.abort();
|
|
248
257
|
pending = sampleProcesses({
|
|
249
258
|
rootPid,
|
|
250
259
|
elapsedMs: Date.now() - started,
|
|
251
|
-
limit: limit
|
|
252
|
-
sampledPids,
|
|
260
|
+
limit: limit + 1,
|
|
253
261
|
identities,
|
|
254
262
|
signal: controller.signal,
|
|
255
263
|
})
|
|
256
264
|
.then((values) => {
|
|
257
265
|
for (const value of values) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
266
|
+
const observation = JSON.stringify({
|
|
267
|
+
parent_pid: value.parent_pid,
|
|
268
|
+
command: value.command,
|
|
269
|
+
process_group_id: value.process_group_id,
|
|
270
|
+
session_id: value.session_id,
|
|
271
|
+
});
|
|
272
|
+
if (lastObservations.get(value.pid) === observation)
|
|
273
|
+
continue;
|
|
274
|
+
if (samples.length >= limit) {
|
|
275
|
+
partial = true;
|
|
261
276
|
continue;
|
|
262
|
-
|
|
277
|
+
}
|
|
278
|
+
lastObservations.set(value.pid, observation);
|
|
263
279
|
samples.push(value);
|
|
264
280
|
}
|
|
265
281
|
})
|
|
@@ -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,6 +9,11 @@ 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";
|
|
13
|
+
import { installCanonicalSkill } from "./SetupSkill.js";
|
|
14
|
+
import { setupPlan } from "./SetupPlan.js";
|
|
15
|
+
import { setupInstallFailure, } from "./SetupInstallFailure.js";
|
|
16
|
+
export { installCanonicalSkill } from "./SetupSkill.js";
|
|
12
17
|
const registrationCommand = () => process.env.npm_command === "exec"
|
|
13
18
|
? ["npx", "-y", PRODUCT_IDENTITY.packageName, "mcp"]
|
|
14
19
|
: [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
|
|
@@ -33,35 +38,13 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
|
33
38
|
if (unsupported !== undefined)
|
|
34
39
|
return fail(unsupported);
|
|
35
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;
|
|
36
46
|
const detectedClients = await host.detectedClients();
|
|
37
|
-
plannedActions =
|
|
38
|
-
...(hopperPath === undefined
|
|
39
|
-
? [
|
|
40
|
-
{
|
|
41
|
-
kind: "install_hopper",
|
|
42
|
-
target: host.platform === "darwin"
|
|
43
|
-
? "~/Applications/Hopper Disassembler.app"
|
|
44
|
-
: "system package manager",
|
|
45
|
-
detail: "Download the official Hopper package, verify it, install it, and open Hopper for activation.",
|
|
46
|
-
external: true,
|
|
47
|
-
},
|
|
48
|
-
]
|
|
49
|
-
: []),
|
|
50
|
-
...detectedClients
|
|
51
|
-
.filter(({ format }) => format !== "unsupported")
|
|
52
|
-
.map((client) => ({
|
|
53
|
-
kind: "configure_client",
|
|
54
|
-
target: client.configPath,
|
|
55
|
-
detail: `Add the REA MCP registration for ${client.name}; preserve unrelated configuration.`,
|
|
56
|
-
external: false,
|
|
57
|
-
})),
|
|
58
|
-
{
|
|
59
|
-
kind: "install_skill",
|
|
60
|
-
target: "~/.agents/skills/rea-analysis/SKILL.md",
|
|
61
|
-
detail: "Install or update the bundled REA analysis skill.",
|
|
62
|
-
external: false,
|
|
63
|
-
},
|
|
64
|
-
];
|
|
47
|
+
plannedActions = setupPlan(host.platform, installHopper, detectedClients);
|
|
65
48
|
let approved = options.approved;
|
|
66
49
|
let interactiveApproval = false;
|
|
67
50
|
if (!approved && confirm !== undefined && !options.structured) {
|
|
@@ -77,53 +60,56 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
|
77
60
|
doctor: await host.doctor(),
|
|
78
61
|
remediation: "Review the setup plan, then rerun interactively or with --yes.",
|
|
79
62
|
};
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (hopperPath === undefined)
|
|
63
|
+
if (installHopper && (interactiveApproval || options.installHopper)) {
|
|
64
|
+
const installed = await host.installHopper();
|
|
65
|
+
if (installed.status === "failed")
|
|
84
66
|
return {
|
|
85
67
|
status: "needs_human",
|
|
86
68
|
plannedActions,
|
|
87
69
|
appliedActions,
|
|
88
70
|
clients,
|
|
89
71
|
doctor: await host.doctor(),
|
|
90
|
-
|
|
72
|
+
code: installed.code,
|
|
73
|
+
remediation: installed.remediation,
|
|
91
74
|
};
|
|
75
|
+
hopperPath = installed.launcherPath;
|
|
92
76
|
appliedActions.push("installed_hopper");
|
|
93
77
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
78
|
+
const clientFailure = await configureDetectedClients({
|
|
79
|
+
host,
|
|
80
|
+
detectedClients,
|
|
81
|
+
hopperPath,
|
|
82
|
+
command: registrationCommand(),
|
|
83
|
+
clients,
|
|
84
|
+
appliedActions,
|
|
85
|
+
});
|
|
86
|
+
if (clientFailure !== undefined)
|
|
87
|
+
return fail(clientFailure);
|
|
102
88
|
const skill = await host.installSkill();
|
|
103
89
|
if (skill === "failed")
|
|
104
|
-
return fail("
|
|
90
|
+
return fail("REA analysis skill could not be installed or verified. Check permissions for `~/.agents/skills`, then rerun setup.");
|
|
105
91
|
if (skill === "installed")
|
|
106
92
|
appliedActions.push("installed_skill");
|
|
107
93
|
const doctor = await host.doctor();
|
|
108
|
-
const
|
|
109
|
-
const ready = doctor.healthy && !activationRequired;
|
|
94
|
+
const remediation = finalSetupRemediation(host.platform, appliedActions.includes("installed_hopper"), doctor.healthy, hopperPath);
|
|
110
95
|
return {
|
|
111
|
-
status:
|
|
96
|
+
status: remediation === undefined ? "ready" : "needs_human",
|
|
112
97
|
plannedActions,
|
|
113
98
|
appliedActions,
|
|
114
99
|
clients,
|
|
115
100
|
doctor,
|
|
116
|
-
...(
|
|
117
|
-
? {}
|
|
118
|
-
: {
|
|
119
|
-
remediation: activationRequired
|
|
120
|
-
? "Open Hopper, complete its one-time activation, then rerun rea doctor --json."
|
|
121
|
-
: hopperPath === undefined
|
|
122
|
-
? "Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis."
|
|
123
|
-
: "Run rea doctor and apply each reported remediation.",
|
|
124
|
-
}),
|
|
101
|
+
...(remediation === undefined ? {} : { remediation }),
|
|
125
102
|
};
|
|
126
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
|
+
};
|
|
127
113
|
const hostRemediation = async (host) => {
|
|
128
114
|
if (host.platform !== "darwin" && host.platform !== "linux")
|
|
129
115
|
return "REA supports Hopper on macOS and selected 64-bit Linux distributions.";
|
|
@@ -152,7 +138,9 @@ const systemSetupHost = () => {
|
|
|
152
138
|
const result = process.platform === "linux"
|
|
153
139
|
? await installLinuxHopper()
|
|
154
140
|
: await installMacHopper();
|
|
155
|
-
|
|
141
|
+
if (result.status === "installed")
|
|
142
|
+
return result;
|
|
143
|
+
return setupInstallFailure(result.reason);
|
|
156
144
|
},
|
|
157
145
|
detectedClients: () => detectClients(homedir()),
|
|
158
146
|
configureClient: (client, hopperPath, command) => client.format === "unsupported"
|
|
@@ -164,7 +152,7 @@ const systemSetupHost = () => {
|
|
|
164
152
|
doctor: () => runDoctor(undefined, doctorHost),
|
|
165
153
|
};
|
|
166
154
|
};
|
|
167
|
-
/** Detect supported
|
|
155
|
+
/** Detect supported agents from stable per-user installation markers. */
|
|
168
156
|
export const detectClients = async (home) => {
|
|
169
157
|
const detected = [];
|
|
170
158
|
for (const candidate of supportedClients(home))
|
|
@@ -353,46 +341,6 @@ export const configureTomlClient = async (client, hopperPath, command = [
|
|
|
353
341
|
...(backupPath === undefined ? {} : { backupPath }),
|
|
354
342
|
};
|
|
355
343
|
};
|
|
356
|
-
/** Transactionally install or upgrade the versioned canonical REA skill. */
|
|
357
|
-
export const installCanonicalSkill = async (home) => {
|
|
358
|
-
const destination = join(home, ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md");
|
|
359
|
-
const backup = `${destination}.rea.backup`;
|
|
360
|
-
let original;
|
|
361
|
-
try {
|
|
362
|
-
const content = await readFile(new URL(`../../skills/${PRODUCT_IDENTITY.skillName}/SKILL.md`, import.meta.url), "utf8");
|
|
363
|
-
original = await readFile(destination, "utf8").catch(() => undefined);
|
|
364
|
-
if (original === content)
|
|
365
|
-
return "unchanged";
|
|
366
|
-
await mkdir(dirname(destination), { recursive: true });
|
|
367
|
-
if (original !== undefined)
|
|
368
|
-
await writeFileAtomic(backup, original, {
|
|
369
|
-
encoding: "utf8",
|
|
370
|
-
mode: 0o600,
|
|
371
|
-
});
|
|
372
|
-
await writeFileAtomic(destination, content, {
|
|
373
|
-
encoding: "utf8",
|
|
374
|
-
mode: 0o600,
|
|
375
|
-
});
|
|
376
|
-
if ((await readFile(destination, "utf8")) !== content)
|
|
377
|
-
throw new Error("skill readback mismatch");
|
|
378
|
-
return "installed";
|
|
379
|
-
}
|
|
380
|
-
catch {
|
|
381
|
-
try {
|
|
382
|
-
if (original === undefined)
|
|
383
|
-
await rm(destination, { force: true });
|
|
384
|
-
else
|
|
385
|
-
await writeFileAtomic(destination, original, {
|
|
386
|
-
encoding: "utf8",
|
|
387
|
-
mode: 0o600,
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
catch {
|
|
391
|
-
// The backup remains beside the skill for explicit operator recovery.
|
|
392
|
-
}
|
|
393
|
-
return "failed";
|
|
394
|
-
}
|
|
395
|
-
};
|
|
396
344
|
const major = (version) => Number.parseInt(version.split(".")[0] ?? "0", 10);
|
|
397
345
|
const exists = async (path) => {
|
|
398
346
|
try {
|
|
@@ -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
|
+
];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import writeFileAtomic from "write-file-atomic";
|
|
4
|
+
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
5
|
+
/** Transactionally install or upgrade the versioned canonical REA skill. */
|
|
6
|
+
export const installCanonicalSkill = async (home) => {
|
|
7
|
+
const destination = join(home, ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md");
|
|
8
|
+
const backup = `${destination}.rea.backup`;
|
|
9
|
+
let original;
|
|
10
|
+
try {
|
|
11
|
+
const content = await readFile(new URL(`../../skills/${PRODUCT_IDENTITY.skillName}/SKILL.md`, import.meta.url), "utf8");
|
|
12
|
+
original = await readFile(destination, "utf8").catch(() => undefined);
|
|
13
|
+
if (original === content)
|
|
14
|
+
return "unchanged";
|
|
15
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
16
|
+
if (original !== undefined)
|
|
17
|
+
await writeFileAtomic(backup, original, {
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
mode: 0o600,
|
|
20
|
+
});
|
|
21
|
+
await writeFileAtomic(destination, content, {
|
|
22
|
+
encoding: "utf8",
|
|
23
|
+
mode: 0o600,
|
|
24
|
+
});
|
|
25
|
+
if ((await readFile(destination, "utf8")) !== content)
|
|
26
|
+
throw new Error("skill readback mismatch");
|
|
27
|
+
return "installed";
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
try {
|
|
31
|
+
if (original === undefined)
|
|
32
|
+
await rm(destination, { force: true });
|
|
33
|
+
else
|
|
34
|
+
await writeFileAtomic(destination, original, {
|
|
35
|
+
encoding: "utf8",
|
|
36
|
+
mode: 0o600,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// The backup remains beside the skill for explicit operator recovery.
|
|
41
|
+
}
|
|
42
|
+
return "failed";
|
|
43
|
+
}
|
|
44
|
+
};
|