rea-agents 1.4.0 → 1.6.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 +37 -13
- package/dist/application/AnalysisSnapshotCache.js +8 -6
- package/dist/application/ArtifactGraphConstruction.js +11 -2
- package/dist/application/ArtifactInventory.js +3 -2
- package/dist/application/AuthorizedArtifactInventory.js +2 -1
- package/dist/application/BinarySession.js +26 -11
- package/dist/application/BoundedJsonFiles.js +6 -29
- package/dist/application/BrowserEvidence.js +107 -0
- package/dist/application/BrowserObservationPort.js +1 -0
- package/dist/application/BrowserObservationService.js +116 -0
- package/dist/application/CapabilityInventory.js +14 -0
- package/dist/application/ClientConfigPath.js +33 -0
- package/dist/application/ConfiguredRoots.js +19 -0
- package/dist/application/CrossVersionInventory.js +16 -3
- package/dist/application/CrossVersionInvestigation.js +23 -62
- package/dist/application/CrossVersionInvestigationEvidence.js +64 -0
- package/dist/application/CrossVersionInvestigationReplay.js +170 -0
- package/dist/application/DeferredFileAuthorization.js +61 -0
- package/dist/application/DirectAnalysis.js +38 -18
- package/dist/application/Doctor.js +16 -11
- package/dist/application/ElectronEvidence.js +35 -0
- package/dist/application/ElectronObservationPort.js +1 -0
- package/dist/application/ElectronObservationService.js +46 -0
- package/dist/application/EvidenceLedger.js +11 -7
- package/dist/application/InvestigationWorkspaceStore.js +2 -26
- package/dist/application/PermissionAuthority.js +30 -0
- package/dist/application/ProcessCaptureAuthority.js +3 -2
- package/dist/application/ProcessCaptureLifecycle.js +19 -13
- package/dist/application/ProcessCheckpoints.js +1 -1
- package/dist/application/ProcessCli.js +17 -5
- package/dist/application/ProcessHarness.js +19 -26
- package/dist/application/ProcessNormalization.js +17 -2
- package/dist/application/ProcessOwnership.js +44 -23
- package/dist/application/ProcessSampling.js +111 -25
- package/dist/application/ProjectPermissionStore.js +107 -3
- package/dist/application/ReferenceSourceImport.js +13 -16
- package/dist/application/ReferenceSourceImportPolicy.js +3 -2
- package/dist/application/Setup.js +89 -30
- package/dist/application/SetupClients.js +7 -4
- package/dist/application/SetupInstallFailure.js +24 -0
- package/dist/application/SetupPlan.js +11 -7
- package/dist/application/SetupSkill.js +20 -3
- package/dist/application/TerminalRenderer.js +1 -1
- package/dist/application/Uninstall.js +20 -7
- package/dist/application/Upgrade.js +14 -3
- package/dist/artifacts/MachOSliceArtifactReader.js +1 -1
- package/dist/artifacts/NativeDmgArtifactReader.js +1 -1
- package/dist/artifacts/SafeOutputTree.js +3 -3
- package/dist/browser/CdpBrowserProvider.js +261 -0
- package/dist/browser/CdpCaptureCompleteness.js +90 -0
- package/dist/browser/CdpCaptureDocuments.js +299 -0
- package/dist/browser/CdpCaptureEvents.js +547 -0
- package/dist/browser/CdpCaptureStorage.js +53 -0
- package/dist/browser/CdpCaptureValues.js +68 -0
- package/dist/browser/CdpConnection.js +241 -0
- package/dist/browser/CdpElectronInspection.js +330 -0
- package/dist/browser/CdpElectronProvider.js +134 -0
- package/dist/browser/CdpEndpoint.js +173 -0
- package/dist/browser/CdpObservationSession.js +301 -0
- package/dist/browser/CdpOptionalCommand.js +15 -0
- package/dist/browser/CdpPageCapture.js +376 -0
- package/dist/browser/CdpSafeMetadata.js +203 -0
- package/dist/browser/CdpScreenshot.js +101 -0
- package/dist/browser/CdpTargetSession.js +47 -0
- package/dist/browser/CdpWebMcpDiscovery.js +242 -0
- package/dist/browser/ElectronFileScope.js +50 -0
- package/dist/browser/PngVisualDiff.js +177 -0
- package/dist/browser/SensitiveTextCapture.js +25 -0
- package/dist/browser/WebSourceMapFetcher.js +357 -0
- package/dist/catalogIdentity.js +10 -0
- package/dist/cli.js +6 -0
- package/dist/cliBrowserAdvancedCommands.js +169 -0
- package/dist/cliBrowserCommands.js +312 -0
- package/dist/cliElectronCommands.js +114 -0
- package/dist/cliInvestigationCommands.js +26 -34
- package/dist/cliPolicyCommands.js +60 -12
- package/dist/config.js +80 -1
- package/dist/contracts/browserToolContracts.js +381 -0
- package/dist/contracts/electronToolContracts.js +73 -0
- package/dist/contracts/errorSchemas.js +2 -0
- package/dist/contracts/toolContracts.js +5 -5
- package/dist/domain/browserCompleteness.js +101 -0
- package/dist/domain/browserObservation.js +587 -0
- package/dist/domain/browserSession.js +63 -0
- package/dist/domain/changedBehavior.js +4 -15
- package/dist/domain/electronObservation.js +162 -0
- package/dist/domain/errors.js +41 -0
- package/dist/domain/investigationWorkspace.js +4 -0
- package/dist/domain/jsonLimits.js +26 -0
- package/dist/domain/jsonShape.js +125 -0
- package/dist/domain/permissionPolicy.js +6 -2
- package/dist/domain/processCapture.js +3 -1
- package/dist/domain/processComparison.js +22 -34
- package/dist/domain/reconstructionVerification.js +2 -15
- package/dist/domain/reconstructionVerificationSchemas.js +2 -0
- package/dist/domain/referenceSourceGraph.js +3 -1
- package/dist/domain/staticRuntimeCorrelation.js +2 -0
- package/dist/domain/symbolAnalysis.js +1 -1
- package/dist/domain/webBundleAnalysis.js +195 -0
- package/dist/domain/webBundleAnalyzer.js +423 -0
- package/dist/domain/webCaptureDiff.js +184 -0
- package/dist/domain/webContentArtifact.js +47 -0
- package/dist/domain/webInventory.js +61 -0
- package/dist/domain/webMcpDiscovery.js +68 -0
- package/dist/domain/webScreenshot.js +114 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/hopper/BridgeLauncher.js +2 -3
- package/dist/hopper/HopperClient.js +51 -11
- package/dist/hopper/HopperProvider.js +6 -1
- package/dist/main.js +42 -31
- package/dist/native/CommandRunner.js +11 -4
- package/dist/native/NativeMacOSProvider.js +3 -2
- package/dist/native/NativeMachoInspection.js +21 -6
- package/dist/server/createServer.js +14 -0
- package/dist/server/registerBrowserTools.js +95 -0
- package/dist/server/registerElectronTools.js +36 -0
- package/dist/server/registerInvestigationTools.js +31 -37
- package/dist/server/registerProcessComparisonTool.js +2 -0
- package/dist/server/registerSessionTools.js +16 -2
- package/dist/server/runDerivedOperation.js +10 -2
- package/package.json +6 -1
- package/scripts/rea.mjs +2 -2
- package/skills/rea-analysis/SKILL.md +28 -3
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
+
const systemFileSystem = { lstat, realpath };
|
|
3
|
+
/**
|
|
4
|
+
* Resolve an existing, user-owned config symlink to one stable transaction
|
|
5
|
+
* path. Missing regular paths remain eligible for first-run creation.
|
|
6
|
+
*/
|
|
7
|
+
export const resolveClientConfigTransactionPath = async (requestedPath, fileSystem = systemFileSystem) => {
|
|
8
|
+
let requestedStats;
|
|
9
|
+
try {
|
|
10
|
+
requestedStats = await fileSystem.lstat(requestedPath);
|
|
11
|
+
}
|
|
12
|
+
catch (cause) {
|
|
13
|
+
return isMissing(cause) ? requestedPath : undefined;
|
|
14
|
+
}
|
|
15
|
+
if (!requestedStats.isSymbolicLink())
|
|
16
|
+
return requestedPath;
|
|
17
|
+
const currentUid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
18
|
+
if (currentUid === undefined ||
|
|
19
|
+
requestedStats.uid !== currentUid ||
|
|
20
|
+
fileSystem.realpath === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
try {
|
|
23
|
+
const canonicalPath = await fileSystem.realpath(requestedPath);
|
|
24
|
+
const targetStats = await fileSystem.lstat(canonicalPath);
|
|
25
|
+
return targetStats.isFile?.() === true && targetStats.uid === currentUid
|
|
26
|
+
? canonicalPath
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const isMissing = (cause) => cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
/** Canonicalize configured policy roots while ignoring stale missing entries. */
|
|
3
|
+
export const canonicalizeConfiguredRoots = async (roots) => {
|
|
4
|
+
const canonical = [];
|
|
5
|
+
for (const root of roots) {
|
|
6
|
+
try {
|
|
7
|
+
canonical.push(await realpath(root));
|
|
8
|
+
}
|
|
9
|
+
catch (cause) {
|
|
10
|
+
if (!isMissingPath(cause))
|
|
11
|
+
throw cause;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return canonical;
|
|
15
|
+
};
|
|
16
|
+
const isMissingPath = (cause) => typeof cause === "object" &&
|
|
17
|
+
cause !== null &&
|
|
18
|
+
"code" in cause &&
|
|
19
|
+
cause.code === "ENOENT";
|
|
@@ -7,6 +7,14 @@ import { paginateArtifactInventory, } from "./ArtifactInventory.js";
|
|
|
7
7
|
import { scanAuthorizedArtifactInventory } from "./AuthorizedArtifactInventory.js";
|
|
8
8
|
import { ARTIFACT_GRAPH_PROVIDER } from "./InvestigationProviders.js";
|
|
9
9
|
export const AUTOMATIC_RUN_LIMITATION = "Automatic cross-version runs compare shipped artifact structure only; they do not execute either version.";
|
|
10
|
+
/** Start both independent version scans while preserving left/right identity. */
|
|
11
|
+
export const scanVersionInventories = async (options, scanner = scanAuthorizedArtifactInventory) => {
|
|
12
|
+
const [left, right] = await Promise.all([
|
|
13
|
+
scanner(options.leftPath, options.inputRoots, options.limits, options.signal, options.integrity),
|
|
14
|
+
scanner(options.rightPath, options.inputRoots, options.limits, options.signal, options.integrity),
|
|
15
|
+
]);
|
|
16
|
+
return { left, right };
|
|
17
|
+
};
|
|
10
18
|
/** Authorize and scan both inputs once under the same bounded traversal policy. */
|
|
11
19
|
export const scanVersions = async (input, inputRoots, signal, integrityContinueEnabled = false) => {
|
|
12
20
|
try {
|
|
@@ -19,9 +27,14 @@ export const scanVersions = async (input, inputRoots, signal, integrityContinueE
|
|
|
19
27
|
enabled: integrityContinueEnabled,
|
|
20
28
|
maxMismatches: input.max_integrity_mismatches,
|
|
21
29
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
return ok(await scanVersionInventories({
|
|
31
|
+
leftPath: input.left_path,
|
|
32
|
+
rightPath: input.right_path,
|
|
33
|
+
inputRoots,
|
|
34
|
+
limits,
|
|
35
|
+
...(signal === undefined ? {} : { signal }),
|
|
36
|
+
integrity,
|
|
37
|
+
}));
|
|
25
38
|
}
|
|
26
39
|
catch (cause) {
|
|
27
40
|
if (cause instanceof ArtifactReaderFailure)
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { findChangedBehavior } from "../domain/changedBehavior.js";
|
|
3
|
-
import { createEvidence } from "../domain/evidence.js";
|
|
1
|
+
import {} from "../domain/evidence.js";
|
|
4
2
|
import { createEvidenceBundle, } from "../domain/evidenceBundle.js";
|
|
5
3
|
import { AnalysisCancelledError, AnalysisInputError, EvidenceIntegrityError, InvestigationWorkspaceError, } from "../domain/errors.js";
|
|
6
|
-
import { createLegacyInvestigationRunIdentity, createInvestigationRunIdentity, createInvestigationWorkspace, crossVersionInvestigationInputSchema, investigationRunSchema,
|
|
7
|
-
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
4
|
+
import { createLegacyInvestigationRunIdentity, createInvestigationRunIdentity, createInvestigationWorkspace, crossVersionInvestigationInputSchema, investigationRunSchema, reviseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
|
|
8
5
|
import { err, ok } from "../domain/result.js";
|
|
9
|
-
import {
|
|
6
|
+
import { createInventoryEvidencePages, runLimitations, scanVersions, targetFor, } from "./CrossVersionInventory.js";
|
|
10
7
|
import { readInvestigationWorkspace, writeInvestigationWorkspace, } from "./InvestigationWorkspaceStore.js";
|
|
11
|
-
import {
|
|
8
|
+
import { createArtifactComparisonEvidence, createChangedBehaviorEvidence, } from "./CrossVersionInvestigationEvidence.js";
|
|
9
|
+
import { selectCompletedInvestigationReplay } from "./CrossVersionInvestigationReplay.js";
|
|
12
10
|
/** Run or resume a deterministic cross-version artifact investigation. */
|
|
13
11
|
export const runCrossVersionInvestigation = async (input, policy, execution) => {
|
|
14
12
|
const parsed = crossVersionInvestigationInputSchema.safeParse(input);
|
|
@@ -24,6 +22,17 @@ export const runCrossVersionInvestigation = async (input, policy, execution) =>
|
|
|
24
22
|
return preflight;
|
|
25
23
|
if (isCancelled(execution.signal))
|
|
26
24
|
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
25
|
+
if (parsed.data.replay_run_id !== undefined) {
|
|
26
|
+
const replay = selectCompletedInvestigationReplay(initial.value, parsed.data);
|
|
27
|
+
if (!replay.ok)
|
|
28
|
+
return replay;
|
|
29
|
+
return completeOutcome(replay.value.workspace, replay.value.run, true, execution.session);
|
|
30
|
+
}
|
|
31
|
+
if (execution.authorizeInputRead !== undefined) {
|
|
32
|
+
const authorized = await execution.authorizeInputRead();
|
|
33
|
+
if (!authorized.ok)
|
|
34
|
+
return authorized;
|
|
35
|
+
}
|
|
27
36
|
await execution.progress?.report({
|
|
28
37
|
phase: "scan_versions",
|
|
29
38
|
completed: 0,
|
|
@@ -47,6 +56,7 @@ export const runCrossVersionInvestigation = async (input, policy, execution) =>
|
|
|
47
56
|
session: execution.session,
|
|
48
57
|
signal: execution.signal,
|
|
49
58
|
progress: execution.progress,
|
|
59
|
+
authorizeWorkspaceWrite: execution.authorizeWorkspaceWrite,
|
|
50
60
|
});
|
|
51
61
|
};
|
|
52
62
|
const continueInvestigation = async (context) => {
|
|
@@ -74,6 +84,11 @@ const continueInvestigation = async (context) => {
|
|
|
74
84
|
run.run_id === legacyIdentity.runId));
|
|
75
85
|
if (context.initial !== null && existing?.status === "complete")
|
|
76
86
|
return completeOutcome(context.initial, existing, true, context.session);
|
|
87
|
+
if (context.authorizeWorkspaceWrite !== undefined) {
|
|
88
|
+
const authorized = await context.authorizeWorkspaceWrite();
|
|
89
|
+
if (!authorized.ok)
|
|
90
|
+
return authorized;
|
|
91
|
+
}
|
|
77
92
|
const pages = createInventoryEvidencePages(context.input, context.snapshots);
|
|
78
93
|
if (!pages.ok)
|
|
79
94
|
return pages;
|
|
@@ -192,31 +207,11 @@ const ensureComparisonCheckpoint = async (input, state, policy) => {
|
|
|
192
207
|
});
|
|
193
208
|
return saved.ok ? ok({ workspace: saved.value, run }) : saved;
|
|
194
209
|
};
|
|
195
|
-
const createArtifactComparisonEvidence = (inventory, limit) => {
|
|
196
|
-
const comparison = compareArtifacts(inventory.left, inventory.right, 0, limit);
|
|
197
|
-
const leftIds = evidenceIds(inventory.left);
|
|
198
|
-
const rightIds = evidenceIds(inventory.right);
|
|
199
|
-
return createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, {
|
|
200
|
-
predicateType: "rea.artifact-comparison/v1",
|
|
201
|
-
operation: "compare_artifacts",
|
|
202
|
-
parameters: {
|
|
203
|
-
left_evidence_ids: leftIds,
|
|
204
|
-
right_evidence_ids: rightIds,
|
|
205
|
-
offset: 0,
|
|
206
|
-
limit,
|
|
207
|
-
},
|
|
208
|
-
result: jsonValueSchema.parse(comparison),
|
|
209
|
-
confidence: "derived",
|
|
210
|
-
authority: "analyst-inference",
|
|
211
|
-
limitations: comparison.limitations,
|
|
212
|
-
evidenceLinks: [...leftIds, ...rightIds],
|
|
213
|
-
});
|
|
214
|
-
};
|
|
215
210
|
const finalizeInvestigation = async (input, state, policy, session) => {
|
|
216
211
|
const comparison = comparisonEvidenceFor(state.workspace, state.run);
|
|
217
212
|
if (!comparison.ok)
|
|
218
213
|
return comparison;
|
|
219
|
-
const evidence = createChangedBehaviorEvidence(state, comparison.value);
|
|
214
|
+
const evidence = createChangedBehaviorEvidence(state.workspace, state.run, comparison.value);
|
|
220
215
|
const run = investigationRunSchema.parse({
|
|
221
216
|
...state.run,
|
|
222
217
|
status: "complete",
|
|
@@ -238,40 +233,6 @@ const finalizeInvestigation = async (input, state, policy, session) => {
|
|
|
238
233
|
});
|
|
239
234
|
return saved.ok ? completeOutcome(saved.value, run, false, session) : saved;
|
|
240
235
|
};
|
|
241
|
-
const createChangedBehaviorEvidence = (state, comparison) => {
|
|
242
|
-
const limit = Math.min(state.run.options.change_limit, 100);
|
|
243
|
-
const changed = findChangedBehavior([comparison], 0, limit);
|
|
244
|
-
const summary = investigationRunSummarySchema.parse({
|
|
245
|
-
schema_version: 1,
|
|
246
|
-
workspace_id: state.workspace.workspace_id,
|
|
247
|
-
run_id: state.run.run_id,
|
|
248
|
-
left_manifest_id: state.run.left.manifest_id,
|
|
249
|
-
right_manifest_id: state.run.right.manifest_id,
|
|
250
|
-
inventory_evidence_count: state.run.left_inventory_evidence_ids.length +
|
|
251
|
-
state.run.right_inventory_evidence_ids.length,
|
|
252
|
-
comparison_evidence_id: comparison.evidence_id,
|
|
253
|
-
limitations: state.run.limitations,
|
|
254
|
-
});
|
|
255
|
-
return createEvidence(undefined, CHANGED_BEHAVIOR_PROVIDER, {
|
|
256
|
-
predicateType: "rea.changed-behavior/v1",
|
|
257
|
-
operation: "find_changed_behavior",
|
|
258
|
-
parameters: {
|
|
259
|
-
workspace_id: state.workspace.workspace_id,
|
|
260
|
-
run_id: state.run.run_id,
|
|
261
|
-
comparison_evidence_ids: [comparison.evidence_id],
|
|
262
|
-
offset: 0,
|
|
263
|
-
limit,
|
|
264
|
-
},
|
|
265
|
-
result: jsonValueSchema.parse({
|
|
266
|
-
...changed,
|
|
267
|
-
investigation_run: summary,
|
|
268
|
-
}),
|
|
269
|
-
confidence: "derived",
|
|
270
|
-
authority: "analyst-inference",
|
|
271
|
-
limitations: [...changed.limitations, AUTOMATIC_RUN_LIMITATION],
|
|
272
|
-
evidenceLinks: changed.evidence_links,
|
|
273
|
-
});
|
|
274
|
-
};
|
|
275
236
|
const comparisonEvidenceFor = (workspace, run) => {
|
|
276
237
|
const evidence = workspace.bundle.records.find(({ evidence_id: id }) => id === run.comparison_evidence_id);
|
|
277
238
|
return evidence === undefined
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { compareArtifacts } from "../domain/artifactComparison.js";
|
|
2
|
+
import { findChangedBehavior } from "../domain/changedBehavior.js";
|
|
3
|
+
import { createEvidence } from "../domain/evidence.js";
|
|
4
|
+
import { investigationRunSummarySchema, } from "../domain/investigationWorkspace.js";
|
|
5
|
+
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
6
|
+
import { AUTOMATIC_RUN_LIMITATION } from "./CrossVersionInventory.js";
|
|
7
|
+
import { ARTIFACT_COMPARISON_PROVIDER, CHANGED_BEHAVIOR_PROVIDER, } from "./InvestigationProviders.js";
|
|
8
|
+
/** Derive deterministic artifact-comparison Evidence from inventory pages. */
|
|
9
|
+
export const createArtifactComparisonEvidence = (inventory, limit) => {
|
|
10
|
+
const comparison = compareArtifacts(inventory.left, inventory.right, 0, limit);
|
|
11
|
+
const leftIds = evidenceIds(inventory.left);
|
|
12
|
+
const rightIds = evidenceIds(inventory.right);
|
|
13
|
+
return createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, {
|
|
14
|
+
predicateType: "rea.artifact-comparison/v1",
|
|
15
|
+
operation: "compare_artifacts",
|
|
16
|
+
parameters: {
|
|
17
|
+
left_evidence_ids: leftIds,
|
|
18
|
+
right_evidence_ids: rightIds,
|
|
19
|
+
offset: 0,
|
|
20
|
+
limit,
|
|
21
|
+
},
|
|
22
|
+
result: jsonValueSchema.parse(comparison),
|
|
23
|
+
confidence: "derived",
|
|
24
|
+
authority: "analyst-inference",
|
|
25
|
+
limitations: comparison.limitations,
|
|
26
|
+
evidenceLinks: [...leftIds, ...rightIds],
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
/** Derive the deterministic final report for one comparison checkpoint. */
|
|
30
|
+
export const createChangedBehaviorEvidence = (workspace, run, comparison) => {
|
|
31
|
+
const limit = Math.min(run.options.change_limit, 100);
|
|
32
|
+
const changed = findChangedBehavior([comparison], 0, limit);
|
|
33
|
+
const summary = investigationRunSummarySchema.parse({
|
|
34
|
+
schema_version: 1,
|
|
35
|
+
workspace_id: workspace.workspace_id,
|
|
36
|
+
run_id: run.run_id,
|
|
37
|
+
left_manifest_id: run.left.manifest_id,
|
|
38
|
+
right_manifest_id: run.right.manifest_id,
|
|
39
|
+
inventory_evidence_count: run.left_inventory_evidence_ids.length +
|
|
40
|
+
run.right_inventory_evidence_ids.length,
|
|
41
|
+
comparison_evidence_id: comparison.evidence_id,
|
|
42
|
+
limitations: run.limitations,
|
|
43
|
+
});
|
|
44
|
+
return createEvidence(undefined, CHANGED_BEHAVIOR_PROVIDER, {
|
|
45
|
+
predicateType: "rea.changed-behavior/v1",
|
|
46
|
+
operation: "find_changed_behavior",
|
|
47
|
+
parameters: {
|
|
48
|
+
workspace_id: workspace.workspace_id,
|
|
49
|
+
run_id: run.run_id,
|
|
50
|
+
comparison_evidence_ids: [comparison.evidence_id],
|
|
51
|
+
offset: 0,
|
|
52
|
+
limit,
|
|
53
|
+
},
|
|
54
|
+
result: jsonValueSchema.parse({
|
|
55
|
+
...changed,
|
|
56
|
+
investigation_run: summary,
|
|
57
|
+
}),
|
|
58
|
+
confidence: "derived",
|
|
59
|
+
authority: "analyst-inference",
|
|
60
|
+
limitations: [...changed.limitations, AUTOMATIC_RUN_LIMITATION],
|
|
61
|
+
evidenceLinks: changed.evidence_links,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const evidenceIds = (records) => records.map(({ evidence_id: id }) => id);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { artifactInventoryResultSchema, } from "../domain/artifactGraph.js";
|
|
3
|
+
import { parseArtifactInventoryEvidence } from "../domain/artifactInventoryEvidence.js";
|
|
4
|
+
import { EvidenceIntegrityError, InvestigationWorkspaceError, } from "../domain/errors.js";
|
|
5
|
+
import { err, ok } from "../domain/result.js";
|
|
6
|
+
import { createArtifactComparisonEvidence, createChangedBehaviorEvidence, } from "./CrossVersionInvestigationEvidence.js";
|
|
7
|
+
import { AUTOMATIC_RUN_LIMITATION } from "./CrossVersionInventory.js";
|
|
8
|
+
import { ARTIFACT_GRAPH_PROVIDER } from "./InvestigationProviders.js";
|
|
9
|
+
/** Select and verify one caller-identified complete run without input access. */
|
|
10
|
+
export const selectCompletedInvestigationReplay = (workspace, input) => {
|
|
11
|
+
const run = workspace?.runs.find(({ run_id: runId }) => runId === input.replay_run_id);
|
|
12
|
+
if (workspace === null ||
|
|
13
|
+
run === undefined ||
|
|
14
|
+
run.status !== "complete" ||
|
|
15
|
+
!controlsMatch(run, input))
|
|
16
|
+
return replayConflict();
|
|
17
|
+
const records = new Map(workspace.bundle.records.map((record) => [record.evidence_id, record]));
|
|
18
|
+
const left = inventoryRecords(records, run.left_inventory_evidence_ids);
|
|
19
|
+
const right = inventoryRecords(records, run.right_inventory_evidence_ids);
|
|
20
|
+
if (!left.ok)
|
|
21
|
+
return left;
|
|
22
|
+
if (!right.ok)
|
|
23
|
+
return right;
|
|
24
|
+
if (!pathsMatch(left.value, input.left_path) ||
|
|
25
|
+
!pathsMatch(right.value, input.right_path))
|
|
26
|
+
return replayConflict();
|
|
27
|
+
const validated = validateLinkedEvidence(workspace, run, left.value, right.value);
|
|
28
|
+
return validated.ok ? ok({ workspace, run }) : validated;
|
|
29
|
+
};
|
|
30
|
+
const controlsMatch = (run, input) => isDeepStrictEqual(run.options, input.options) &&
|
|
31
|
+
run.integrity_policy === input.integrity_policy &&
|
|
32
|
+
run.integrity_continue_approved === input.integrity_continue_approved &&
|
|
33
|
+
run.max_integrity_mismatches === input.max_integrity_mismatches;
|
|
34
|
+
const inventoryRecords = (records, evidenceIds) => {
|
|
35
|
+
if (new Set(evidenceIds).size !== evidenceIds.length)
|
|
36
|
+
return replayIntegrity("Investigation replay repeats inventory Evidence");
|
|
37
|
+
const selected = evidenceIds.flatMap((evidenceId) => {
|
|
38
|
+
const evidence = records.get(evidenceId);
|
|
39
|
+
return evidence === undefined ? [] : [evidence];
|
|
40
|
+
});
|
|
41
|
+
return selected.length === evidenceIds.length
|
|
42
|
+
? ok(selected)
|
|
43
|
+
: replayIntegrity("Investigation replay is missing inventory Evidence");
|
|
44
|
+
};
|
|
45
|
+
const pathsMatch = (records, path) => records.every(({ subject }) => subject?.local_path === path);
|
|
46
|
+
const validateLinkedEvidence = (workspace, run, leftRecords, rightRecords) => {
|
|
47
|
+
try {
|
|
48
|
+
const left = validateInventory(leftRecords, run.left, run);
|
|
49
|
+
const right = validateInventory(rightRecords, run.right, run);
|
|
50
|
+
if (!left.ok)
|
|
51
|
+
return left;
|
|
52
|
+
if (!right.ok)
|
|
53
|
+
return right;
|
|
54
|
+
const expectedLimitations = runLimitations(left.value, right.value);
|
|
55
|
+
if (!isDeepStrictEqual(run.limitations, expectedLimitations))
|
|
56
|
+
return replayIntegrity("Investigation replay limitations disagree with inventory Evidence");
|
|
57
|
+
const comparison = createArtifactComparisonEvidence({ left: left.value.records, right: right.value.records }, run.options.change_limit);
|
|
58
|
+
const storedComparison = evidenceFor(workspace, run.comparison_evidence_id);
|
|
59
|
+
if (storedComparison === undefined ||
|
|
60
|
+
!isDeepStrictEqual(storedComparison, comparison))
|
|
61
|
+
return replayIntegrity("Investigation replay comparison Evidence is inconsistent");
|
|
62
|
+
const result = createChangedBehaviorEvidence(workspace, run, comparison);
|
|
63
|
+
const storedResult = evidenceFor(workspace, run.result_evidence_id);
|
|
64
|
+
return storedResult !== undefined && isDeepStrictEqual(storedResult, result)
|
|
65
|
+
? ok(null)
|
|
66
|
+
: replayIntegrity("Investigation replay result Evidence is inconsistent");
|
|
67
|
+
}
|
|
68
|
+
catch (cause) {
|
|
69
|
+
return replayIntegrity("Investigation replay linked Evidence is invalid", cause);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const validateInventory = (records, target, run) => {
|
|
73
|
+
const pages = records.map((record) => artifactInventoryResultSchema.parse(record.normalized_result));
|
|
74
|
+
const expectedPageCount = Math.max(1, Math.ceil((pages[0]?.manifest.node_count ?? 0) / run.options.page_size), Math.ceil((pages[0]?.manifest.occurrence_count ?? 0) / run.options.page_size), Math.ceil((pages[0]?.manifest.edge_count ?? 0) / run.options.page_size));
|
|
75
|
+
if (records.length !== expectedPageCount)
|
|
76
|
+
return replayIntegrity("Investigation replay inventory Evidence pagination is inconsistent");
|
|
77
|
+
if (!sameSnapshotMetadata(pages))
|
|
78
|
+
return replayIntegrity("Investigation replay inventory pages disagree on snapshot metadata");
|
|
79
|
+
for (const [index, record] of records.entries()) {
|
|
80
|
+
const page = pages[index];
|
|
81
|
+
if (page === undefined || !inventoryRecordMatches(record, page, run, index))
|
|
82
|
+
return replayIntegrity("Investigation replay inventory Evidence is inconsistent");
|
|
83
|
+
}
|
|
84
|
+
const result = parseArtifactInventoryEvidence(records);
|
|
85
|
+
if (!result.inventory.complete || !targetMatches(result.inventory, target))
|
|
86
|
+
return replayIntegrity("Investigation replay inventory commitment is inconsistent");
|
|
87
|
+
return ok({ records, result });
|
|
88
|
+
};
|
|
89
|
+
const inventoryRecordMatches = (record, page, run, index) => {
|
|
90
|
+
const offset = index * run.options.page_size;
|
|
91
|
+
const parameters = {
|
|
92
|
+
node_offset: offset,
|
|
93
|
+
node_limit: run.options.page_size,
|
|
94
|
+
occurrence_offset: offset,
|
|
95
|
+
occurrence_limit: run.options.page_size,
|
|
96
|
+
edge_offset: offset,
|
|
97
|
+
edge_limit: run.options.page_size,
|
|
98
|
+
max_entries: run.options.max_entries,
|
|
99
|
+
max_total_bytes: run.options.max_total_bytes,
|
|
100
|
+
max_entry_bytes: run.options.max_entry_bytes,
|
|
101
|
+
max_compression_ratio: run.options.max_compression_ratio,
|
|
102
|
+
max_depth: run.options.max_depth,
|
|
103
|
+
max_path_bytes: run.options.max_path_bytes,
|
|
104
|
+
integrity_policy: run.integrity_policy,
|
|
105
|
+
integrity_continue_approved: run.integrity_continue_approved,
|
|
106
|
+
max_integrity_mismatches: run.max_integrity_mismatches,
|
|
107
|
+
};
|
|
108
|
+
const locations = page.occurrences.items.map(({ logical_path: path }) => ({
|
|
109
|
+
kind: "artifact-path",
|
|
110
|
+
path,
|
|
111
|
+
}));
|
|
112
|
+
return (record.subject !== null &&
|
|
113
|
+
record.subject.name === record.subject.local_path.split("/").at(-1) &&
|
|
114
|
+
record.subject.digest.sha256 === page.manifest.root_sha256 &&
|
|
115
|
+
record.subject.format === page.manifest.root_format &&
|
|
116
|
+
record.subject.architecture === null &&
|
|
117
|
+
isDeepStrictEqual(record.provider, ARTIFACT_GRAPH_PROVIDER) &&
|
|
118
|
+
record.predicate_type === "rea.analysis/v2" &&
|
|
119
|
+
record.operation === "inventory_artifact" &&
|
|
120
|
+
isDeepStrictEqual(record.parameters, parameters) &&
|
|
121
|
+
record.raw_result === null &&
|
|
122
|
+
record.confidence === "observed" &&
|
|
123
|
+
record.authority === "shipped-artifact" &&
|
|
124
|
+
record.environment === null &&
|
|
125
|
+
isDeepStrictEqual(record.limitations, page.limitations) &&
|
|
126
|
+
isDeepStrictEqual(record.locations, locations) &&
|
|
127
|
+
record.evidence_links.length === 0 &&
|
|
128
|
+
isDeepStrictEqual(page.limits, traversalLimits(run)) &&
|
|
129
|
+
pageMatches(page.nodes, offset, run.options.page_size, page.manifest.node_count) &&
|
|
130
|
+
pageMatches(page.occurrences, offset, run.options.page_size, page.manifest.occurrence_count) &&
|
|
131
|
+
pageMatches(page.edges, offset, run.options.page_size, page.manifest.edge_count));
|
|
132
|
+
};
|
|
133
|
+
const sameSnapshotMetadata = (pages) => {
|
|
134
|
+
const first = pages[0];
|
|
135
|
+
return (first !== undefined &&
|
|
136
|
+
pages.every((page) => isDeepStrictEqual(page.manifest, first.manifest) &&
|
|
137
|
+
isDeepStrictEqual(page.limits, first.limits) &&
|
|
138
|
+
isDeepStrictEqual(page.provenance, first.provenance) &&
|
|
139
|
+
isDeepStrictEqual(page.integrity_contradictions, first.integrity_contradictions) &&
|
|
140
|
+
isDeepStrictEqual(page.limitations, first.limitations)));
|
|
141
|
+
};
|
|
142
|
+
const pageMatches = (page, offset, limit, total) => page.offset === offset &&
|
|
143
|
+
page.limit === limit &&
|
|
144
|
+
page.total === total &&
|
|
145
|
+
page.items.length === Math.max(0, Math.min(limit, page.total - offset)) &&
|
|
146
|
+
page.next_offset === (offset + limit < page.total ? offset + limit : null);
|
|
147
|
+
const traversalLimits = (run) => ({
|
|
148
|
+
max_entries: run.options.max_entries,
|
|
149
|
+
max_total_bytes: run.options.max_total_bytes,
|
|
150
|
+
max_entry_bytes: run.options.max_entry_bytes,
|
|
151
|
+
max_compression_ratio: run.options.max_compression_ratio,
|
|
152
|
+
max_depth: run.options.max_depth,
|
|
153
|
+
max_path_bytes: run.options.max_path_bytes,
|
|
154
|
+
});
|
|
155
|
+
const targetMatches = (inventory, target) => inventory.manifest.root_sha256 === target.root_sha256 &&
|
|
156
|
+
inventory.manifest.graph_sha256 === target.graph_sha256 &&
|
|
157
|
+
inventory.manifest.manifest_id === target.manifest_id &&
|
|
158
|
+
inventory.manifest.root_format === target.format;
|
|
159
|
+
const runLimitations = (left, right) => [
|
|
160
|
+
...new Set([
|
|
161
|
+
...left.result.inventory.limitations.map((item) => `Left: ${item}`),
|
|
162
|
+
...right.result.inventory.limitations.map((item) => `Right: ${item}`),
|
|
163
|
+
AUTOMATIC_RUN_LIMITATION,
|
|
164
|
+
]),
|
|
165
|
+
].sort((first, second) => first.localeCompare(second));
|
|
166
|
+
const evidenceFor = (workspace, evidenceId) => evidenceId === null
|
|
167
|
+
? undefined
|
|
168
|
+
: workspace.bundle.records.find(({ evidence_id: current }) => current === evidenceId);
|
|
169
|
+
const replayConflict = () => err(new InvestigationWorkspaceError("read", "revision-conflict"));
|
|
170
|
+
const replayIntegrity = (message, cause) => err(new EvidenceIntegrityError(message, cause === undefined ? undefined : { cause }));
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AnalysisProtocolError, PermissionRequiredError, } from "../domain/errors.js";
|
|
2
|
+
import { err, ok } from "../domain/result.js";
|
|
3
|
+
import { PermissionPathError, } from "./PermissionAuthority.js";
|
|
4
|
+
/** Authorize one root-scoped operation and project policy path failures. */
|
|
5
|
+
export const authorizeRootPermission = async (authority, request) => {
|
|
6
|
+
const authorized = await authority.authorize({
|
|
7
|
+
capability: request.capability,
|
|
8
|
+
roots: request.roots,
|
|
9
|
+
executables: [],
|
|
10
|
+
environment_names: [],
|
|
11
|
+
network: "none",
|
|
12
|
+
mount: false,
|
|
13
|
+
operation_identity: `${request.operation}:${request.capability}:${request.roots.join(":")}`,
|
|
14
|
+
}, request.access);
|
|
15
|
+
return authorized.ok
|
|
16
|
+
? ok(null)
|
|
17
|
+
: err(authorized.error instanceof PermissionRequiredError
|
|
18
|
+
? authorized.error
|
|
19
|
+
: new AnalysisProtocolError(authorized.error.message, {
|
|
20
|
+
cause: authorized.error,
|
|
21
|
+
}));
|
|
22
|
+
};
|
|
23
|
+
/** Authorize an existing file read and defer write authority until requested. */
|
|
24
|
+
export const authorizeFileReadWithDeferredWrite = async (authority, input) => {
|
|
25
|
+
const read = await authority.authorize({
|
|
26
|
+
capability: input.readCapability,
|
|
27
|
+
roots: [input.path],
|
|
28
|
+
executables: [],
|
|
29
|
+
environment_names: [],
|
|
30
|
+
network: "none",
|
|
31
|
+
mount: false,
|
|
32
|
+
operation_identity: `${input.operation}:${input.readCapability}:${input.path}`,
|
|
33
|
+
}, "read");
|
|
34
|
+
let writeAuthorization;
|
|
35
|
+
if (!read.ok) {
|
|
36
|
+
if (!(read.error instanceof PermissionPathError) ||
|
|
37
|
+
read.error.reason !== "not_found")
|
|
38
|
+
return err(read.error instanceof PermissionRequiredError
|
|
39
|
+
? read.error
|
|
40
|
+
: new AnalysisProtocolError(read.error.message, {
|
|
41
|
+
cause: read.error,
|
|
42
|
+
}));
|
|
43
|
+
writeAuthorization = authorizeRootPermission(authority, {
|
|
44
|
+
capability: input.writeCapability,
|
|
45
|
+
roots: [input.path],
|
|
46
|
+
access: "write",
|
|
47
|
+
operation: input.operation,
|
|
48
|
+
});
|
|
49
|
+
const authorized = await writeAuthorization;
|
|
50
|
+
if (!authorized.ok)
|
|
51
|
+
return authorized;
|
|
52
|
+
}
|
|
53
|
+
return ok({
|
|
54
|
+
authorizeWrite: () => (writeAuthorization ??= authorizeRootPermission(authority, {
|
|
55
|
+
capability: input.writeCapability,
|
|
56
|
+
roots: [input.path],
|
|
57
|
+
access: "write",
|
|
58
|
+
operation: input.operation,
|
|
59
|
+
})),
|
|
60
|
+
});
|
|
61
|
+
};
|
|
@@ -12,6 +12,7 @@ import { parseBinaryTarget } from "../domain/binaryTarget.js";
|
|
|
12
12
|
import { snapshotEvidenceForQuery, snapshotMatchesTarget, } from "../domain/analysisSnapshot.js";
|
|
13
13
|
import { err, ok } from "../domain/result.js";
|
|
14
14
|
import { loadConfiguredPermissionAuthority } from "./PermissionConfiguration.js";
|
|
15
|
+
import { authorizeFileReadWithDeferredWrite, } from "./DeferredFileAuthorization.js";
|
|
15
16
|
const WORKFLOW_PROVIDER = {
|
|
16
17
|
id: "rea-workflow",
|
|
17
18
|
name: "REA composed investigation workflow",
|
|
@@ -26,6 +27,9 @@ export const runDirectAnalysis = async (path, tool, arguments_, options = {}) =>
|
|
|
26
27
|
logger: options.logger ?? silentLogger,
|
|
27
28
|
snapshotPath: options.snapshotPath,
|
|
28
29
|
signal,
|
|
30
|
+
...(options.permissionAuthority === undefined
|
|
31
|
+
? {}
|
|
32
|
+
: { permissionAuthority: options.permissionAuthority }),
|
|
29
33
|
}));
|
|
30
34
|
/** Execute one provider-native semantic operation with atomic provenance. */
|
|
31
35
|
export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger, signal) => withProcessCancellation(signal, (operationSignal) => runAnalysis(path, tool, arguments_, {
|
|
@@ -51,19 +55,8 @@ export const runSessionStatus = async (logger = silentLogger) => {
|
|
|
51
55
|
await session.close();
|
|
52
56
|
}
|
|
53
57
|
};
|
|
54
|
-
const authorizeAnalysis = async (authority, tool, arguments_
|
|
58
|
+
const authorizeAnalysis = async (authority, tool, arguments_) => {
|
|
55
59
|
const requests = [];
|
|
56
|
-
if (snapshotPath !== undefined) {
|
|
57
|
-
requests.push({
|
|
58
|
-
capability: "snapshot_read",
|
|
59
|
-
path: snapshotPath,
|
|
60
|
-
access: "write",
|
|
61
|
-
}, {
|
|
62
|
-
capability: "snapshot_write",
|
|
63
|
-
path: snapshotPath,
|
|
64
|
-
access: "write",
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
60
|
if (tool === "extract_artifact" && typeof arguments_.output_root === "string")
|
|
68
61
|
requests.push({
|
|
69
62
|
capability: "artifact_extract",
|
|
@@ -107,17 +100,41 @@ const authorizeAnalysis = async (authority, tool, arguments_, snapshotPath) => {
|
|
|
107
100
|
}
|
|
108
101
|
return ok(null);
|
|
109
102
|
};
|
|
103
|
+
const permissionAuthorityFor = (config, supplied) => supplied === undefined
|
|
104
|
+
? loadConfiguredPermissionAuthority(config)
|
|
105
|
+
: Promise.resolve(ok(supplied));
|
|
106
|
+
const authorizeSnapshotAccess = async (authority, tool, snapshotPath) => snapshotPath === undefined
|
|
107
|
+
? ok(undefined)
|
|
108
|
+
: authorizeFileReadWithDeferredWrite(authority, {
|
|
109
|
+
path: snapshotPath,
|
|
110
|
+
readCapability: "snapshot_read",
|
|
111
|
+
writeCapability: "snapshot_write",
|
|
112
|
+
operation: tool,
|
|
113
|
+
});
|
|
114
|
+
const authorizeDeferredWrite = (authorization) => authorization?.authorizeWrite() ?? Promise.resolve(ok(null));
|
|
115
|
+
const authorizeAnalysisRun = async (input) => {
|
|
116
|
+
const authority = await permissionAuthorityFor(input.config, input.suppliedAuthority);
|
|
117
|
+
if (!authority.ok)
|
|
118
|
+
return authority;
|
|
119
|
+
const operation = await authorizeAnalysis(authority.value, input.tool, input.arguments);
|
|
120
|
+
if (!operation.ok)
|
|
121
|
+
return operation;
|
|
122
|
+
return authorizeSnapshotAccess(authority.value, input.tool, input.snapshotPath);
|
|
123
|
+
};
|
|
110
124
|
const runAnalysis = async (path, tool, arguments_, options) => {
|
|
111
125
|
const { logger, signal, snapshotPath } = options;
|
|
112
126
|
const config = parseConfig(process.env);
|
|
113
127
|
if (!config.ok)
|
|
114
128
|
return cliError(config.error);
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
const authorization = await authorizeAnalysisRun({
|
|
130
|
+
config: config.value,
|
|
131
|
+
suppliedAuthority: options.permissionAuthority,
|
|
132
|
+
tool,
|
|
133
|
+
arguments: arguments_,
|
|
134
|
+
snapshotPath,
|
|
135
|
+
});
|
|
136
|
+
if (!authorization.ok)
|
|
137
|
+
return cliError(authorization.error);
|
|
121
138
|
const unavailable = snapshotUnavailable(snapshotPath, config.value.hopperLoaderArgs);
|
|
122
139
|
if (unavailable !== undefined)
|
|
123
140
|
return cliError(unavailable);
|
|
@@ -135,6 +152,9 @@ const runAnalysis = async (path, tool, arguments_, options) => {
|
|
|
135
152
|
return cliError(prepared.error);
|
|
136
153
|
if (prepared.value.evidence !== undefined)
|
|
137
154
|
return prepared.value.evidence;
|
|
155
|
+
const writable = await authorizeDeferredWrite(authorization.value);
|
|
156
|
+
if (!writable.ok)
|
|
157
|
+
return cliError(writable.error);
|
|
138
158
|
const { snapshot } = prepared.value;
|
|
139
159
|
const opened = await session.open(path, snapshot === undefined ? { signal } : { signal, snapshot });
|
|
140
160
|
if (!opened.ok)
|
|
@@ -37,23 +37,28 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
|
|
|
37
37
|
remediation: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",
|
|
38
38
|
classification: "unsupported_host",
|
|
39
39
|
}));
|
|
40
|
-
const candidates =
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
const candidates = host.configuredHopperPath === undefined
|
|
41
|
+
? [
|
|
42
|
+
DEFAULT_HOPPER,
|
|
43
|
+
SYSTEM_LINUX_HOPPER,
|
|
44
|
+
linuxHopperLauncherPath(homedir()),
|
|
45
|
+
...(await host.manualHopperPaths()),
|
|
46
|
+
await host.brewHopperPath(),
|
|
47
|
+
].filter((value) => value !== undefined)
|
|
48
|
+
: [host.configuredHopperPath];
|
|
48
49
|
let hopperPath;
|
|
49
50
|
for (const candidate of new Set(candidates))
|
|
50
51
|
if (await host.executable(candidate)) {
|
|
51
52
|
hopperPath = candidate;
|
|
52
53
|
break;
|
|
53
54
|
}
|
|
54
|
-
checks.push(check("hopper", hopperPath !== undefined, hopperPath, {
|
|
55
|
-
remediation:
|
|
56
|
-
|
|
55
|
+
checks.push(check("hopper", hopperPath !== undefined, hopperPath ?? host.configuredHopperPath, {
|
|
56
|
+
remediation: host.configuredHopperPath === undefined
|
|
57
|
+
? "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH."
|
|
58
|
+
: "Unset or update HOPPER_LAUNCHER_PATH to an executable Hopper launcher.",
|
|
59
|
+
classification: host.configuredHopperPath === undefined
|
|
60
|
+
? "missing_analysis_engine"
|
|
61
|
+
: "config_drift",
|
|
57
62
|
}));
|
|
58
63
|
if (host.platform === "linux" && hopperPath !== undefined)
|
|
59
64
|
checks.push(check("hopper-version", await host.supportedLinuxHopper(hopperPath), hopperPath, {
|