rea-agents 1.6.0 → 2.0.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 +157 -66
- package/bridge/ghidra/ReaGhidraBridge.java +2075 -0
- package/bridge/hopper_bridge.py +13 -3
- package/dist/application/AbortablePromise.js +25 -0
- package/dist/application/AnalysisProvider.js +3 -0
- package/dist/application/AnalysisProviderEvaluation.js +60 -0
- package/dist/application/AnalysisProviderRegistry.js +228 -0
- package/dist/application/AnalysisSnapshotCache.js +41 -21
- package/dist/application/AnalysisSnapshotFiles.js +5 -6
- package/dist/application/BinarySession.js +218 -101
- package/dist/application/CapabilityInventory.js +18 -0
- package/dist/application/CompositeProvider.js +20 -8
- package/dist/application/CrossVersionInvestigation.js +10 -6
- package/dist/application/DirectAnalysis.js +49 -35
- package/dist/application/Doctor.js +110 -8
- package/dist/application/ElectronBoundaryAnalysis.js +96 -0
- package/dist/application/ElectronBoundaryGraph.js +9 -0
- package/dist/application/ElectronBoundaryGraphContext.js +62 -0
- package/dist/application/ElectronBoundaryGraphIpc.js +242 -0
- package/dist/application/ElectronBoundaryGraphNative.js +86 -0
- package/dist/application/ElectronBoundaryGraphWindows.js +199 -0
- package/dist/application/EnhancedTools.js +39 -14
- package/dist/application/InvestigationProviders.js +41 -0
- package/dist/application/JavaScriptApplicationEvidence.js +26 -0
- package/dist/application/JavaScriptApplicationEvidenceGraph.js +89 -0
- package/dist/application/JavaScriptApplicationService.js +74 -0
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +29 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +90 -0
- package/dist/application/JavaScriptArtifactAnalysis.js +305 -0
- package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
- package/dist/application/JavaScriptArtifactFiles.js +231 -0
- package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
- package/dist/application/JavaScriptArtifactGraphBuilder.js +144 -0
- package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
- package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
- package/dist/application/JavaScriptArtifactGraphEvidence.js +99 -0
- package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
- package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
- package/dist/application/JavaScriptArtifactReconstruction.js +77 -0
- package/dist/application/JavaScriptArtifactReconstructionInput.js +18 -0
- package/dist/application/JavaScriptReplayPlanning.js +201 -0
- package/dist/application/JavaScriptReplayService.js +266 -0
- package/dist/application/JavaScriptRuntimeReconciliationEvidence.js +25 -0
- package/dist/application/JavaScriptRuntimeReconciliationService.js +27 -0
- package/dist/application/MacHopper.js +20 -4
- package/dist/application/ManagedApplicationGraphService.js +55 -0
- package/dist/application/ManagedMemberComparisonService.js +122 -0
- package/dist/application/ManagedNativeVerificationService.js +39 -0
- package/dist/application/ManagedReconstructionService.js +58 -0
- package/dist/application/ManagedRuntimeCorrelationService.js +103 -0
- package/dist/application/PermissionAuthority.js +1 -1
- package/dist/application/ProcessCaptureLifecycle.js +1 -1
- package/dist/application/ProcessHarness.js +1 -1
- package/dist/application/ProcessSampling.js +16 -12
- package/dist/application/ProjectPermissionStore.js +1 -0
- package/dist/application/SessionProviderRouter.js +219 -0
- package/dist/application/Setup.js +84 -60
- package/dist/application/SetupClients.js +1 -1
- package/dist/application/SetupPlan.js +9 -2
- package/dist/application/SupportedClients.js +40 -16
- package/dist/application/Upgrade.js +1 -1
- package/dist/application/runtime.js +11 -6
- package/dist/artifacts/AsarArtifactReader.js +23 -3
- package/dist/browser/CdpBrowserProvider.js +4 -2
- package/dist/browser/CdpCaptureEvents.js +49 -0
- package/dist/browser/CdpElectronInspection.js +106 -162
- package/dist/browser/CdpElectronProvider.js +9 -3
- package/dist/browser/CdpElectronScriptEvents.js +69 -0
- package/dist/browser/CdpElectronScripts.js +94 -0
- package/dist/browser/CdpElectronWorkers.js +56 -0
- package/dist/browser/CdpEndpoint.js +14 -1
- package/dist/browser/CdpPageCapture.js +8 -4
- package/dist/catalogIdentity.js +6 -42
- package/dist/cli.js +446 -34
- package/dist/cliApplicationCommands.js +62 -0
- package/dist/cliBrowserAdvancedCommands.js +5 -4
- package/dist/cliBrowserCommands.js +5 -4
- package/dist/cliCommandNames.js +56 -0
- package/dist/cliElectronCommands.js +106 -3
- package/dist/cliEvidenceCommands.js +4 -3
- package/dist/cliInvestigationCommands.js +2 -1
- package/dist/cliJsonInput.js +54 -0
- package/dist/cliPolicyCommands.js +2 -1
- package/dist/cliProcessCommands.js +3 -2
- package/dist/config.js +101 -2
- package/dist/contracts/applicationToolContracts.js +91 -0
- package/dist/contracts/artifactToolContracts.js +5 -9
- package/dist/contracts/browserToolContracts.js +14 -62
- package/dist/contracts/electronToolContracts.js +45 -19
- package/dist/contracts/enhancedInputs.js +2 -2
- package/dist/contracts/errorSchemas.js +1 -0
- package/dist/contracts/investigationExamples.js +0 -6
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +46 -0
- package/dist/contracts/javascriptRuntimeReconciliationExample.js +208 -0
- package/dist/contracts/managedToolContracts.js +153 -0
- package/dist/contracts/managedWorkflowExamples.js +346 -0
- package/dist/contracts/managedWorkflowToolContracts.js +203 -0
- package/dist/contracts/nativeToolContracts.js +2 -6
- package/dist/contracts/promptContracts.js +18 -2
- package/dist/contracts/providerSelection.js +28 -0
- package/dist/contracts/sessionLifecycleInputs.js +2 -0
- package/dist/contracts/toolContractExamples.js +10 -25
- package/dist/contracts/toolContracts.js +69 -65
- package/dist/contracts/toolEffects.js +160 -0
- package/dist/contracts/toolOutputSchemas.js +152 -42
- package/dist/doctorRuntime.js +47 -0
- package/dist/domain/analysisProfile.js +78 -0
- package/dist/domain/analysisSnapshot.js +92 -46
- package/dist/domain/artifactComparison.js +3 -4
- package/dist/domain/artifactGraph.js +1 -0
- package/dist/domain/binaryTarget.js +4 -29
- package/dist/domain/browserObservation.js +3 -0
- package/dist/domain/bundleComparison.js +4 -4
- package/dist/domain/electronObservation.js +13 -0
- package/dist/domain/electronStaticAnalysis.js +9 -0
- package/dist/domain/electronStaticAnalysisBrowser.js +194 -0
- package/dist/domain/electronStaticAnalysisIpc.js +171 -0
- package/dist/domain/electronStaticAnalysisNative.js +164 -0
- package/dist/domain/electronStaticAnalysisTypes.js +1 -0
- package/dist/domain/electronStaticAnalysisValues.js +103 -0
- package/dist/domain/errors.js +148 -7
- package/dist/domain/evidence.js +44 -5
- package/dist/domain/evidenceBundle.js +9 -7
- package/dist/domain/functionComparison.js +9 -3
- package/dist/domain/functionComparisonNormalization.js +37 -0
- package/dist/domain/functionComparisonSchemas.js +3 -8
- package/dist/domain/functionDossierEvidence.js +4 -1
- package/dist/domain/hopperValues.js +66 -9
- package/dist/domain/inputIssueProjection.js +52 -0
- package/dist/domain/investigationWorkspace.js +2 -1
- package/dist/domain/javascriptApplicationAnalysis.js +155 -0
- package/dist/domain/javascriptApplicationChangeGraph.js +231 -0
- package/dist/domain/javascriptApplicationEvidenceSchemas.js +379 -0
- package/dist/domain/javascriptApplicationGraph.js +282 -0
- package/dist/domain/javascriptApplicationGraphSchemas.js +139 -0
- package/dist/domain/javascriptApplicationVersionComparison.js +116 -0
- package/dist/domain/javascriptApplicationVersionComparisonSchemas.js +139 -0
- package/dist/domain/javascriptApplicationVersionItems.js +221 -0
- package/dist/domain/javascriptApplicationVersionKeys.js +170 -0
- package/dist/domain/javascriptAstFingerprint.js +143 -0
- package/dist/domain/javascriptFeatureSeed.js +140 -0
- package/dist/domain/javascriptFeatureTrace.js +232 -0
- package/dist/domain/javascriptFeatureTraceSchemas.js +136 -0
- package/dist/domain/javascriptFeatureTraversal.js +88 -0
- package/dist/domain/javascriptNativeHandoff.js +85 -0
- package/dist/domain/javascriptReplay.js +361 -0
- package/dist/domain/javascriptRuntimeLoadState.js +106 -0
- package/dist/domain/javascriptRuntimeReconciliation.js +42 -0
- package/dist/domain/javascriptRuntimeReconciliationGraph.js +169 -0
- package/dist/domain/javascriptRuntimeReconciliationMatching.js +247 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +160 -0
- package/dist/domain/javascriptRuntimeReconciliationResult.js +119 -0
- package/dist/domain/javascriptRuntimeReconciliationRuntime.js +316 -0
- package/dist/domain/javascriptRuntimeReconciliationSchemas.js +245 -0
- package/dist/domain/javascriptRuntimeStaticCandidates.js +191 -0
- package/dist/domain/javascriptStaticAnalysis.js +346 -0
- package/dist/domain/javascriptStaticAnalysisFindings.js +39 -0
- package/dist/domain/javascriptStaticAnalysisHelpers.js +274 -0
- package/dist/domain/javascriptStaticAnalysisState.js +23 -0
- package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
- package/dist/domain/managedApplicationGraph.js +556 -0
- package/dist/domain/managedArtifact.js +444 -0
- package/dist/domain/managedMemberComparison.js +565 -0
- package/dist/domain/managedNativeVerification.js +408 -0
- package/dist/domain/managedReconstruction.js +183 -0
- package/dist/domain/managedRuntimeCorrelation.js +175 -0
- package/dist/domain/reconstructionVerificationSchemas.js +1 -3
- package/dist/dotnet/ManagedArtifactInspector.js +318 -0
- package/dist/dotnet/ManagedMemberInspector.js +887 -0
- package/dist/dotnet/ManagedMetadataHeaps.js +131 -0
- package/dist/dotnet/ManagedMetadataInventory.js +368 -0
- package/dist/dotnet/ManagedMetadataLayout.js +322 -0
- package/dist/dotnet/ManagedNativeBoundaryInspector.js +391 -0
- package/dist/dotnet/ManagedPeReader.js +157 -0
- package/dist/dotnet/ManagedReaderFailure.js +10 -0
- package/dist/dotnet/ManagedStaticProvider.js +203 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
- package/dist/ghidra/GhidraClient.js +400 -0
- package/dist/ghidra/GhidraClientTypes.js +1 -0
- package/dist/ghidra/GhidraDefaults.js +20 -0
- package/dist/ghidra/GhidraDiagnostics.js +43 -0
- package/dist/ghidra/GhidraDoctor.js +59 -0
- package/dist/ghidra/GhidraFunctionValues.js +264 -0
- package/dist/ghidra/GhidraInstallation.js +239 -0
- package/dist/ghidra/GhidraInventoryValues.js +217 -0
- package/dist/ghidra/GhidraLauncher.js +139 -0
- package/dist/ghidra/GhidraProvider.js +366 -0
- package/dist/ghidra/GhidraRequestQueue.js +98 -0
- package/dist/ghidra/GhidraResponseBuffer.js +31 -0
- package/dist/ghidra/GhidraResponseRouter.js +24 -0
- package/dist/ghidra/GhidraSessionError.js +39 -0
- package/dist/ghidra/GhidraSessionValues.js +62 -0
- package/dist/ghidra/GhidraSocketConnection.js +46 -0
- package/dist/ghidra/protocol.js +69 -0
- package/dist/hopper/BridgeLauncher.js +19 -54
- package/dist/hopper/HopperAnalysisProfile.js +106 -0
- package/dist/hopper/HopperClient.js +154 -162
- package/dist/hopper/HopperProvider.js +99 -7
- package/dist/hopper/HopperSessionValues.js +27 -0
- package/dist/hopper/HopperSocketConnection.js +31 -0
- package/dist/main.js +14 -0
- package/dist/native/NativeMacOSProvider.js +3 -1
- package/dist/process/PendingOperations.js +59 -0
- package/dist/process/PrivateRuntimeRoot.js +33 -0
- package/dist/process/ProviderDeadline.js +95 -0
- package/dist/process/ProviderProcess.js +275 -0
- package/dist/replay/JavaScriptReplayWorker.js +286 -0
- package/dist/replay/LinuxJavaScriptReplayRunner.js +434 -0
- package/dist/replay/LinuxRuntimeClosure.js +46 -0
- package/dist/replay/LinuxSeccompPolicy.js +40 -0
- package/dist/replay/ReplayWorkerProtocol.js +55 -0
- package/dist/replay/SystemJavaScriptReplayHost.js +182 -0
- package/dist/server/createServer.js +53 -0
- package/dist/server/promptCompletion.js +9 -3
- package/dist/server/registerApplicationTools.js +115 -0
- package/dist/server/registerArtifactComparisonTool.js +16 -9
- package/dist/server/registerBrowserTools.js +33 -8
- package/dist/server/registerBundleComparisonTool.js +17 -20
- package/dist/server/registerElectronTools.js +45 -3
- package/dist/server/registerEnhancedTools.js +67 -19
- package/dist/server/registerEvidenceTools.js +5 -1
- package/dist/server/registerFunctionComparisonTool.js +14 -7
- package/dist/server/registerInvestigationTools.js +21 -17
- package/dist/server/registerManagedTools.js +6 -0
- package/dist/server/registerManagedWorkflowTools.js +277 -0
- package/dist/server/registerOfficialTools.js +11 -8
- package/dist/server/registerProcessComparisonTool.js +25 -47
- package/dist/server/registerSessionStatusTool.js +7 -7
- package/dist/server/registerSessionTools.js +57 -87
- package/dist/server/sessionEvidence.js +14 -19
- package/dist/server/toolInputValidation.js +77 -0
- package/dist/server/toolRegistrationOptions.js +79 -2
- package/dist/server/toolResult.js +25 -17
- package/package.json +17 -7
- package/scripts/rea.mjs +36 -2
- package/skills/rea-analysis/SKILL.md +59 -3
- /package/dist/{application → process}/ProcessOwnership.js +0 -0
package/bridge/hopper_bridge.py
CHANGED
|
@@ -106,7 +106,11 @@ def _procedure_name(procedure):
|
|
|
106
106
|
|
|
107
107
|
|
|
108
108
|
def _procedure_identity(procedure):
|
|
109
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
"address": _hex(procedure.getEntryPoint()),
|
|
111
|
+
"name": _procedure_name(procedure),
|
|
112
|
+
"classification": None,
|
|
113
|
+
}
|
|
110
114
|
|
|
111
115
|
|
|
112
116
|
def _procedure_locals(procedure):
|
|
@@ -609,7 +613,7 @@ def _analyze_function(document, params):
|
|
|
609
613
|
def collection(name, items, scan_limited=False):
|
|
610
614
|
return _bounded(items, _collection_offset(params, name), limit, None, scan_limited)
|
|
611
615
|
return {
|
|
612
|
-
"procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)},
|
|
616
|
+
"procedure": {"address": _hex(procedure.getEntryPoint()), "name": _procedure_name(procedure), "classification": None, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)},
|
|
613
617
|
"pseudocode": {"text": pseudo_text, "total_chars": len(pseudo), "returned_chars": len(pseudo_text), "truncated": pseudo_next < len(pseudo), "next_offset": pseudo_next if pseudo_next < len(pseudo) else None},
|
|
614
618
|
"assembly": _bounded(assembly_lines, assembly_offset, max_instructions),
|
|
615
619
|
"comments": collection("comments", comments, instruction_scan_truncated),
|
|
@@ -620,6 +624,12 @@ def _analyze_function(document, params):
|
|
|
620
624
|
"referenced_names": collection("referenced_names", referenced_names, instruction_scan_truncated),
|
|
621
625
|
"basic_blocks": collection("basic_blocks", blocks),
|
|
622
626
|
"instruction_scan": {"scanned": len(addresses), "truncated": instruction_scan_truncated},
|
|
627
|
+
"limitations": [
|
|
628
|
+
"Hopper's public Python API does not classify reference kinds.",
|
|
629
|
+
"Hopper's public Python API does not expose equivalent external or thunk classification in this dossier.",
|
|
630
|
+
"Unresolved indirect calls without reported target addresses are not represented as call edges.",
|
|
631
|
+
"Pseudocode and assembly are provider-specific representations, not original source.",
|
|
632
|
+
],
|
|
623
633
|
}
|
|
624
634
|
|
|
625
635
|
|
|
@@ -766,7 +776,7 @@ def _dispatch(method, params):
|
|
|
766
776
|
if method == "procedure_info":
|
|
767
777
|
blocks = list(procedure.basicBlockIterator())
|
|
768
778
|
length = sum(max(0, block.getEndingAddress() - block.getStartingAddress()) for block in blocks)
|
|
769
|
-
return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure)}
|
|
779
|
+
return {"name": _procedure_name(procedure), "entrypoint": _hex(procedure.getEntryPoint()), "basicblock_count": procedure.getBasicBlockCount(), "length": length, "signature": procedure.signatureString(), "locals": _procedure_locals(procedure), "classification": None}
|
|
770
780
|
if method == "set_address_name":
|
|
771
781
|
address = _address(document, params.get("address"))
|
|
772
782
|
result = document.setNameAtAddress(address, params["name"])
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Sentinel returned when an abort signal wins an asynchronous race. */
|
|
2
|
+
export const ABORTED = Symbol("aborted");
|
|
3
|
+
/**
|
|
4
|
+
* Await a promise without depending on the producer to observe cancellation.
|
|
5
|
+
* The producer still receives its signal separately and remains responsible for
|
|
6
|
+
* releasing any resources that it owns.
|
|
7
|
+
*/
|
|
8
|
+
export const waitForAbortable = async (pending, signal) => {
|
|
9
|
+
if (signal === undefined)
|
|
10
|
+
return pending;
|
|
11
|
+
if (signal.aborted)
|
|
12
|
+
return ABORTED;
|
|
13
|
+
let onAbort;
|
|
14
|
+
const cancelled = new Promise((resolve) => {
|
|
15
|
+
onAbort = () => resolve(ABORTED);
|
|
16
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
return await Promise.race([pending, cancelled]);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
if (onAbort !== undefined)
|
|
23
|
+
signal.removeEventListener("abort", onAbort);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -6,6 +6,9 @@ export const createAnalysisExecution = (result, provider, options = {}) => ({
|
|
|
6
6
|
? jsonValueSchema.parse(result)
|
|
7
7
|
: jsonValueSchema.parse(options.rawResult),
|
|
8
8
|
provider,
|
|
9
|
+
...(options.analysisProfile === undefined
|
|
10
|
+
? {}
|
|
11
|
+
: { analysisProfile: structuredClone(options.analysisProfile) }),
|
|
9
12
|
limitations: [...(options.limitations ?? [])],
|
|
10
13
|
locations: [...(options.locations ?? [])],
|
|
11
14
|
subject: options.subject ?? null,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { analysisProfileSchema, } from "../domain/analysisProfile.js";
|
|
2
|
+
import { AnalysisCancelledError } from "../domain/errors.js";
|
|
3
|
+
import { jsonObjectSchema } from "../domain/jsonValue.js";
|
|
4
|
+
import { err, ok } from "../domain/result.js";
|
|
5
|
+
import { ABORTED, waitForAbortable } from "./AbortablePromise.js";
|
|
6
|
+
/** Resolve and validate one provider profile without starting its client. */
|
|
7
|
+
export const evaluateAnalysisProviderCandidate = async (candidate, status, target, signal) => {
|
|
8
|
+
if (signalIsAborted(signal))
|
|
9
|
+
return err(new AnalysisCancelledError("open_binary"));
|
|
10
|
+
let resolved;
|
|
11
|
+
try {
|
|
12
|
+
const resolution = await waitForAbortable(candidate.resolveAnalysisProfile(target, signal === undefined ? undefined : { signal }), signal);
|
|
13
|
+
if (resolution === ABORTED)
|
|
14
|
+
return err(new AnalysisCancelledError("open_binary"));
|
|
15
|
+
resolved = resolution;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return ok(rejectedProfile(candidate, status, "Provider profile resolution threw"));
|
|
19
|
+
}
|
|
20
|
+
if (signalIsAborted(signal))
|
|
21
|
+
return err(new AnalysisCancelledError("open_binary"));
|
|
22
|
+
if (!resolved.ok && resolved.error instanceof AnalysisCancelledError)
|
|
23
|
+
return err(resolved.error);
|
|
24
|
+
if (!resolved.ok)
|
|
25
|
+
return ok(rejectedProfile(candidate, status, resolved.error.message, {
|
|
26
|
+
error_tag: resolved.error._tag,
|
|
27
|
+
}));
|
|
28
|
+
try {
|
|
29
|
+
if (resolved.value.profile === null)
|
|
30
|
+
return ok(rejectedProfile(candidate, status, "Provider version or analysis profile could not be resolved"));
|
|
31
|
+
const profile = analysisProfileSchema.parse(resolved.value.profile);
|
|
32
|
+
const compatibility = jsonObjectSchema.parse(resolved.value.compatibility);
|
|
33
|
+
const identity = candidate.identity();
|
|
34
|
+
if (profile.provider.id !== identity.id ||
|
|
35
|
+
profile.provider.name !== identity.name ||
|
|
36
|
+
(identity.version !== null &&
|
|
37
|
+
profile.provider.version !== identity.version))
|
|
38
|
+
return ok(rejectedProfile(candidate, status, "Resolved analysis profile has mismatched provider identity"));
|
|
39
|
+
return ok({ candidate, status, profile, compatibility });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return ok(rejectedProfile(candidate, status, "Provider returned an invalid analysis profile"));
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const signalIsAborted = (signal) => signal?.aborted === true;
|
|
46
|
+
const rejectedProfile = (candidate, status, reason, diagnostics = {}) => ({
|
|
47
|
+
candidate,
|
|
48
|
+
status: {
|
|
49
|
+
...status,
|
|
50
|
+
availability: {
|
|
51
|
+
status: "unavailable",
|
|
52
|
+
code: "version_unresolved",
|
|
53
|
+
reason,
|
|
54
|
+
diagnostics: jsonObjectSchema.parse({
|
|
55
|
+
...status.availability.diagnostics,
|
|
56
|
+
...diagnostics,
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { analysisProviderIdSchema, analysisProviderSelectorSchema, } from "../contracts/providerSelection.js";
|
|
2
|
+
import { AnalysisCancelledError, ProviderSelectionError, } from "../domain/errors.js";
|
|
3
|
+
import { jsonObjectSchema } from "../domain/jsonValue.js";
|
|
4
|
+
import { err, ok } from "../domain/result.js";
|
|
5
|
+
import { evaluateAnalysisProviderCandidate, } from "./AnalysisProviderEvaluation.js";
|
|
6
|
+
/**
|
|
7
|
+
* Discover and select overlapping deep providers without starting their clients.
|
|
8
|
+
* Registration order never affects selection or caller-visible output.
|
|
9
|
+
*/
|
|
10
|
+
export class AnalysisProviderRegistry {
|
|
11
|
+
#providers;
|
|
12
|
+
#byId;
|
|
13
|
+
#declaredOperations;
|
|
14
|
+
#defaultSelector;
|
|
15
|
+
constructor(providers, defaultSelector = "auto") {
|
|
16
|
+
this.#defaultSelector =
|
|
17
|
+
analysisProviderSelectorSchema.parse(defaultSelector);
|
|
18
|
+
const byId = new Map();
|
|
19
|
+
const declaredOperations = new Set();
|
|
20
|
+
for (const provider of providers) {
|
|
21
|
+
const identity = provider.identity();
|
|
22
|
+
if (!analysisProviderIdSchema.safeParse(identity.id).success)
|
|
23
|
+
throw new TypeError(`Invalid analysis provider ID: ${identity.id}`);
|
|
24
|
+
if (byId.has(identity.id))
|
|
25
|
+
throw new TypeError(`Duplicate analysis provider ID: ${identity.id}`);
|
|
26
|
+
const providerOperations = new Set();
|
|
27
|
+
for (const capability of provider.capabilities()) {
|
|
28
|
+
if (capability.provider.id !== identity.id ||
|
|
29
|
+
capability.provider.name !== identity.name ||
|
|
30
|
+
capability.provider.version !== identity.version)
|
|
31
|
+
throw new TypeError(`Provider ${identity.id} published mismatched capability provenance for ${capability.operation}`);
|
|
32
|
+
if (providerOperations.has(capability.operation))
|
|
33
|
+
throw new TypeError(`Provider ${identity.id} declares operation ${capability.operation} more than once`);
|
|
34
|
+
providerOperations.add(capability.operation);
|
|
35
|
+
declaredOperations.add(capability.operation);
|
|
36
|
+
}
|
|
37
|
+
byId.set(identity.id, provider);
|
|
38
|
+
}
|
|
39
|
+
this.#providers = Object.freeze([...providers].sort((left, right) => left.identity().id.localeCompare(right.identity().id)));
|
|
40
|
+
this.#byId = byId;
|
|
41
|
+
this.#declaredOperations = Object.freeze([...declaredOperations].sort((left, right) => left.localeCompare(right)));
|
|
42
|
+
}
|
|
43
|
+
/** Return the configured default without interpreting it as priority. */
|
|
44
|
+
defaultSelector() {
|
|
45
|
+
return this.#defaultSelector;
|
|
46
|
+
}
|
|
47
|
+
/** List configured candidate identities in canonical provider-ID order. */
|
|
48
|
+
identities() {
|
|
49
|
+
return this.#providers.map((provider) => structuredClone(provider.identity()));
|
|
50
|
+
}
|
|
51
|
+
/** List the unique deep-operation declarations without probing candidates. */
|
|
52
|
+
declaredOperations() {
|
|
53
|
+
return [...this.#declaredOperations];
|
|
54
|
+
}
|
|
55
|
+
/** Discover candidates for status without resolving profiles or starting clients. */
|
|
56
|
+
candidates(target) {
|
|
57
|
+
return this.#providers.map((provider) => candidateStatus(provider, target));
|
|
58
|
+
}
|
|
59
|
+
/** Select one whole deep-operation family for a parsed target. */
|
|
60
|
+
async select(target, requestedSelector, options = {}) {
|
|
61
|
+
if (signalIsAborted(options.signal))
|
|
62
|
+
return err(new AnalysisCancelledError("open_binary"));
|
|
63
|
+
const parsedRequest = requestedSelector === undefined
|
|
64
|
+
? undefined
|
|
65
|
+
: analysisProviderSelectorSchema.safeParse(requestedSelector);
|
|
66
|
+
if (parsedRequest !== undefined && !parsedRequest.success)
|
|
67
|
+
return err(selectionError("invalid_options", typeof requestedSelector === "string"
|
|
68
|
+
? requestedSelector
|
|
69
|
+
: "<invalid>", this.candidates(target)));
|
|
70
|
+
const request = parsedRequest?.data;
|
|
71
|
+
const selector = request ?? this.#defaultSelector;
|
|
72
|
+
const baseline = this.candidates(target);
|
|
73
|
+
if (signalIsAborted(options.signal))
|
|
74
|
+
return err(new AnalysisCancelledError("open_binary"));
|
|
75
|
+
if (request === undefined &&
|
|
76
|
+
selector !== "auto" &&
|
|
77
|
+
baseline.length > 0 &&
|
|
78
|
+
baseline.every(({ targetSupport }) => targetSupport.status === "unsupported"))
|
|
79
|
+
return ok({
|
|
80
|
+
requestedProviderId: selector,
|
|
81
|
+
binding: null,
|
|
82
|
+
candidates: baseline,
|
|
83
|
+
});
|
|
84
|
+
if (selector !== "auto")
|
|
85
|
+
return this.#selectConcrete({ target, baseline, signal: options.signal }, selector, request === undefined ? "environment" : "request");
|
|
86
|
+
return this.#selectAutomatically({
|
|
87
|
+
target,
|
|
88
|
+
baseline,
|
|
89
|
+
signal: options.signal,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Explain why a deep operation is unavailable on an unbound target. */
|
|
93
|
+
unboundOperationError(operation, selection) {
|
|
94
|
+
const reason = selection.candidates.length > 0 &&
|
|
95
|
+
selection.candidates.every(({ targetSupport }) => targetSupport.status === "unsupported")
|
|
96
|
+
? "target_unsupported"
|
|
97
|
+
: "provider_unavailable";
|
|
98
|
+
return selectionError(reason, selection.requestedProviderId, selection.candidates, operation);
|
|
99
|
+
}
|
|
100
|
+
async #selectConcrete(context, providerId, source) {
|
|
101
|
+
const { baseline, signal, target } = context;
|
|
102
|
+
const provider = this.#byId.get(providerId);
|
|
103
|
+
if (provider === undefined)
|
|
104
|
+
return err(selectionError("unknown_provider", providerId, baseline));
|
|
105
|
+
const initial = baseline.find(({ provider: identity }) => identity.id === providerId);
|
|
106
|
+
if (initial === undefined)
|
|
107
|
+
throw new TypeError("Registered provider is absent from discovery output");
|
|
108
|
+
if (initial.targetSupport.status !== "supported")
|
|
109
|
+
return err(selectionError("target_unsupported", providerId, baseline));
|
|
110
|
+
if (initial.availability.status === "unavailable")
|
|
111
|
+
return err(selectionError("provider_unavailable", providerId, baseline));
|
|
112
|
+
const evaluation = await evaluateAnalysisProviderCandidate(provider, initial, target, signal);
|
|
113
|
+
if (!evaluation.ok)
|
|
114
|
+
return evaluation;
|
|
115
|
+
const evaluated = evaluation.value;
|
|
116
|
+
const statuses = replaceCandidate(baseline, evaluated.status);
|
|
117
|
+
if (evaluated.profile === undefined ||
|
|
118
|
+
evaluated.compatibility === undefined)
|
|
119
|
+
return err(selectionError("provider_unavailable", providerId, statuses));
|
|
120
|
+
return ok(selectedResult(providerId, source, {
|
|
121
|
+
...evaluated,
|
|
122
|
+
profile: evaluated.profile,
|
|
123
|
+
compatibility: evaluated.compatibility,
|
|
124
|
+
}, statuses));
|
|
125
|
+
}
|
|
126
|
+
async #selectAutomatically(context) {
|
|
127
|
+
const { baseline, signal, target } = context;
|
|
128
|
+
const results = await Promise.all(this.#providers.map(async (provider) => {
|
|
129
|
+
const status = baseline.find(({ provider: identity }) => identity.id === provider.identity().id);
|
|
130
|
+
if (status === undefined)
|
|
131
|
+
throw new TypeError("Registered provider is absent from discovery output");
|
|
132
|
+
return status.availability.status === "available" &&
|
|
133
|
+
status.targetSupport.status === "supported"
|
|
134
|
+
? evaluateAnalysisProviderCandidate(provider, status, target, signal)
|
|
135
|
+
: ok({
|
|
136
|
+
candidate: provider,
|
|
137
|
+
status,
|
|
138
|
+
});
|
|
139
|
+
}));
|
|
140
|
+
const cancelled = results.find((result) => !result.ok);
|
|
141
|
+
if (cancelled !== undefined && !cancelled.ok)
|
|
142
|
+
return cancelled;
|
|
143
|
+
const evaluations = results.map((result) => {
|
|
144
|
+
if (!result.ok)
|
|
145
|
+
throw new TypeError("Cancelled candidate evaluation disappeared");
|
|
146
|
+
return result.value;
|
|
147
|
+
});
|
|
148
|
+
const statuses = evaluations.map(({ status }) => status);
|
|
149
|
+
const usable = evaluations.filter((evaluation) => evaluation.profile !== undefined &&
|
|
150
|
+
evaluation.compatibility !== undefined);
|
|
151
|
+
if (usable.length === 0)
|
|
152
|
+
return ok({
|
|
153
|
+
requestedProviderId: "auto",
|
|
154
|
+
binding: null,
|
|
155
|
+
candidates: statuses,
|
|
156
|
+
});
|
|
157
|
+
if (usable.length > 1)
|
|
158
|
+
return err(selectionError("ambiguous", "auto", statuses));
|
|
159
|
+
const selected = usable[0];
|
|
160
|
+
if (selected === undefined)
|
|
161
|
+
throw new TypeError("Usable provider selection disappeared");
|
|
162
|
+
return ok(selectedResult("auto", "auto-single-candidate", selected, statuses));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const candidateStatus = (provider, target) => ({
|
|
166
|
+
provider: structuredClone(provider.identity()),
|
|
167
|
+
availability: cloneAvailability(provider.inspectAvailability()),
|
|
168
|
+
targetSupport: target === undefined
|
|
169
|
+
? {
|
|
170
|
+
status: "unknown",
|
|
171
|
+
code: null,
|
|
172
|
+
reason: "No target is open.",
|
|
173
|
+
diagnostics: {},
|
|
174
|
+
}
|
|
175
|
+
: cloneTargetSupport(provider.inspectTargetSupport(target)),
|
|
176
|
+
selected: false,
|
|
177
|
+
capabilities: provider
|
|
178
|
+
.capabilities()
|
|
179
|
+
.map((capability) => structuredClone(capability)),
|
|
180
|
+
});
|
|
181
|
+
const signalIsAborted = (signal) => signal?.aborted === true;
|
|
182
|
+
const selectedResult = (requestedProviderId, source, evaluated, statuses) => {
|
|
183
|
+
const selectedStatus = {
|
|
184
|
+
...evaluated.status,
|
|
185
|
+
provider: structuredClone(evaluated.profile.provider),
|
|
186
|
+
selected: true,
|
|
187
|
+
};
|
|
188
|
+
return {
|
|
189
|
+
requestedProviderId,
|
|
190
|
+
binding: {
|
|
191
|
+
provider: evaluated.candidate,
|
|
192
|
+
identity: structuredClone(evaluated.profile.provider),
|
|
193
|
+
selectionSource: source,
|
|
194
|
+
profile: structuredClone(evaluated.profile),
|
|
195
|
+
compatibility: structuredClone(evaluated.compatibility),
|
|
196
|
+
},
|
|
197
|
+
candidates: replaceCandidate(statuses, selectedStatus),
|
|
198
|
+
};
|
|
199
|
+
};
|
|
200
|
+
const replaceCandidate = (statuses, replacement) => statuses.map((status) => status.provider.id === replacement.provider.id ? replacement : status);
|
|
201
|
+
const selectionError = (reason, requestedProviderId, candidates, operation = "open_binary") => new ProviderSelectionError({
|
|
202
|
+
operation,
|
|
203
|
+
reason,
|
|
204
|
+
requestedProviderId,
|
|
205
|
+
candidateIds: candidates.map(({ provider }) => provider.id),
|
|
206
|
+
rejections: candidateRejections(candidates),
|
|
207
|
+
});
|
|
208
|
+
const candidateRejections = (candidates) => candidates.flatMap((candidate) => [
|
|
209
|
+
...(candidate.availability.status === "unavailable"
|
|
210
|
+
? [candidate.availability]
|
|
211
|
+
: []),
|
|
212
|
+
...(candidate.targetSupport.status === "unsupported"
|
|
213
|
+
? [candidate.targetSupport]
|
|
214
|
+
: []),
|
|
215
|
+
].map((rejection) => ({
|
|
216
|
+
providerId: candidate.provider.id,
|
|
217
|
+
code: rejection.code,
|
|
218
|
+
reason: rejection.reason,
|
|
219
|
+
diagnostics: structuredClone(rejection.diagnostics),
|
|
220
|
+
})));
|
|
221
|
+
const cloneAvailability = (availability) => ({
|
|
222
|
+
...availability,
|
|
223
|
+
diagnostics: jsonObjectSchema.parse(availability.diagnostics),
|
|
224
|
+
});
|
|
225
|
+
const cloneTargetSupport = (support) => ({
|
|
226
|
+
...support,
|
|
227
|
+
diagnostics: jsonObjectSchema.parse(support.diagnostics),
|
|
228
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EvidenceIntegrityError, NoBinaryOpenError, } from "../domain/errors.js";
|
|
2
2
|
import { err, ok } from "../domain/result.js";
|
|
3
|
-
import { analysisQueryId, createAnalysisSnapshotEntry, snapshotMatchesTarget, snapshotTarget, } from "../domain/analysisSnapshot.js";
|
|
3
|
+
import { analysisQueryId, createAnalysisSnapshotEntry, snapshotBinding, snapshotMatchesProfile, snapshotMatchesTarget, snapshotTarget, } from "../domain/analysisSnapshot.js";
|
|
4
4
|
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
|
5
5
|
const STATEFUL_OPERATIONS = new Set([
|
|
6
6
|
"health",
|
|
@@ -35,24 +35,32 @@ export const isSnapshotCacheable = (operation, descriptor, parameters) => !STATE
|
|
|
35
35
|
export class AnalysisSnapshotCache {
|
|
36
36
|
#entries = new Map();
|
|
37
37
|
#target;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
#binding;
|
|
39
|
+
/** Whether staged entries belong to the supplied target and profile. */
|
|
40
|
+
matches(target, profile) {
|
|
41
|
+
return ((this.#target === undefined && this.#binding === undefined) ||
|
|
42
|
+
(this.#target !== undefined &&
|
|
43
|
+
this.#binding !== undefined &&
|
|
44
|
+
profile !== undefined &&
|
|
45
|
+
snapshotMatchesTarget(this.#target, target) &&
|
|
46
|
+
snapshotMatchesProfile(this.#binding, profile)));
|
|
41
47
|
}
|
|
42
|
-
/** Replace cache state when the
|
|
43
|
-
select(target) {
|
|
44
|
-
if (!this.matches(target))
|
|
48
|
+
/** Replace cache state when the target or selected profile changes. */
|
|
49
|
+
select(target, profile) {
|
|
50
|
+
if (!this.matches(target, profile))
|
|
45
51
|
this.#entries.clear();
|
|
46
52
|
this.#target = snapshotTarget(target);
|
|
53
|
+
this.#binding = snapshotBinding(profile);
|
|
47
54
|
}
|
|
48
55
|
/** Merge already-validated entries and return the new-entry count. */
|
|
49
56
|
stage(snapshot) {
|
|
50
|
-
if (this.#target !== undefined &&
|
|
51
|
-
(this.#target
|
|
52
|
-
|
|
53
|
-
this.#
|
|
57
|
+
if ((this.#target !== undefined &&
|
|
58
|
+
JSON.stringify(this.#target) !== JSON.stringify(snapshot.target)) ||
|
|
59
|
+
(this.#binding !== undefined &&
|
|
60
|
+
!snapshotMatchesProfile(this.#binding, snapshot.binding.analysis_profile)))
|
|
54
61
|
this.#entries.clear();
|
|
55
62
|
this.#target = structuredClone(snapshot.target);
|
|
63
|
+
this.#binding = structuredClone(snapshot.binding);
|
|
56
64
|
let imported = 0;
|
|
57
65
|
for (const entry of snapshot.entries) {
|
|
58
66
|
if (!this.#entries.has(entry.query_id))
|
|
@@ -62,20 +70,23 @@ export class AnalysisSnapshotCache {
|
|
|
62
70
|
return imported;
|
|
63
71
|
}
|
|
64
72
|
/** Build a complete snapshot for an active target. */
|
|
65
|
-
export(target, evidenceBundle) {
|
|
66
|
-
if (target === undefined)
|
|
73
|
+
export(target, profile, evidenceBundle) {
|
|
74
|
+
if (target === undefined || profile === undefined)
|
|
67
75
|
return err(new NoBinaryOpenError());
|
|
68
76
|
return ok({
|
|
69
|
-
snapshot_version:
|
|
77
|
+
snapshot_version: 2,
|
|
70
78
|
target: snapshotTarget(target),
|
|
79
|
+
binding: snapshotBinding(profile),
|
|
71
80
|
entries: this.entries(),
|
|
72
81
|
evidence_bundle: structuredClone(evidenceBundle),
|
|
73
82
|
});
|
|
74
83
|
}
|
|
75
84
|
/** Validate target identity, merge evidence atomically, then stage entries. */
|
|
76
85
|
import(snapshot, active, mergeEvidence) {
|
|
77
|
-
if (active !== undefined &&
|
|
78
|
-
|
|
86
|
+
if (active !== undefined &&
|
|
87
|
+
(!snapshotMatchesTarget(snapshot.target, active.target) ||
|
|
88
|
+
!snapshotMatchesProfile(snapshot.binding, active.profile)))
|
|
89
|
+
return err(new EvidenceIntegrityError("Analysis snapshot profile_mismatch: target, provider, or analysis profile does not match the active binary"));
|
|
79
90
|
const importedEvidence = mergeEvidence(snapshot.evidence_bundle);
|
|
80
91
|
return importedEvidence.ok ? ok(this.stage(snapshot)) : importedEvidence;
|
|
81
92
|
}
|
|
@@ -86,8 +97,8 @@ export class AnalysisSnapshotCache {
|
|
|
86
97
|
.map((entry) => structuredClone(entry));
|
|
87
98
|
}
|
|
88
99
|
/** Replay an exact provider-specific query, marking its cached provenance. */
|
|
89
|
-
lookup(target, operation, parameters
|
|
90
|
-
const queryId = analysisQueryId(snapshotTarget(target), operation, parameters
|
|
100
|
+
lookup(target, profile, operation, parameters) {
|
|
101
|
+
const queryId = analysisQueryId(snapshotTarget(target), snapshotBinding(profile), operation, parameters);
|
|
91
102
|
const cached = this.#entries.get(queryId);
|
|
92
103
|
if (cached === undefined)
|
|
93
104
|
return undefined;
|
|
@@ -96,6 +107,7 @@ export class AnalysisSnapshotCache {
|
|
|
96
107
|
result: cached.execution.result,
|
|
97
108
|
rawResult: cached.execution.raw_result,
|
|
98
109
|
provider: cached.execution.provider,
|
|
110
|
+
analysisProfile: structuredClone(profile),
|
|
99
111
|
limitations: [
|
|
100
112
|
...cached.execution.limitations,
|
|
101
113
|
"Loaded from a local REA analysis snapshot; this call did not re-run the provider.",
|
|
@@ -118,16 +130,24 @@ export class AnalysisSnapshotCache {
|
|
|
118
130
|
});
|
|
119
131
|
}
|
|
120
132
|
/** Record one successful immutable call unless the cache is full. */
|
|
121
|
-
record(
|
|
133
|
+
record(input) {
|
|
134
|
+
const { target, profile, operation, parameters, execution } = input;
|
|
122
135
|
if (this.#entries.size >= 10_000)
|
|
123
136
|
return;
|
|
124
|
-
this.select(target);
|
|
125
|
-
const entry = createAnalysisSnapshotEntry(
|
|
137
|
+
this.select(target, profile);
|
|
138
|
+
const entry = createAnalysisSnapshotEntry({
|
|
139
|
+
target: snapshotTarget(target),
|
|
140
|
+
binding: snapshotBinding(profile),
|
|
141
|
+
operation,
|
|
142
|
+
parameters,
|
|
143
|
+
execution,
|
|
144
|
+
});
|
|
126
145
|
this.#entries.set(entry.query_id, entry);
|
|
127
146
|
}
|
|
128
147
|
/** Forget all target-bound entries. */
|
|
129
148
|
clear() {
|
|
130
149
|
this.#entries.clear();
|
|
131
150
|
this.#target = undefined;
|
|
151
|
+
this.#binding = undefined;
|
|
132
152
|
}
|
|
133
153
|
}
|
|
@@ -11,9 +11,7 @@ export const readAnalysisSnapshot = async (path, policy) => {
|
|
|
11
11
|
return ok(parseAnalysisSnapshot(loaded.value));
|
|
12
12
|
}
|
|
13
13
|
catch (cause) {
|
|
14
|
-
return err(new EvidenceIntegrityError(
|
|
15
|
-
cause,
|
|
16
|
-
}));
|
|
14
|
+
return err(new EvidenceIntegrityError(snapshotValidationMessage(cause), { cause }));
|
|
17
15
|
}
|
|
18
16
|
};
|
|
19
17
|
/** Atomically write a deterministic analysis snapshot inside an approved root. */
|
|
@@ -23,9 +21,10 @@ export const writeAnalysisSnapshot = async (snapshot, path, overwrite, policy) =
|
|
|
23
21
|
encoded = serializeAnalysisSnapshot(snapshot);
|
|
24
22
|
}
|
|
25
23
|
catch (cause) {
|
|
26
|
-
return err(new EvidenceIntegrityError(
|
|
27
|
-
cause,
|
|
28
|
-
}));
|
|
24
|
+
return err(new EvidenceIntegrityError(snapshotValidationMessage(cause), { cause }));
|
|
29
25
|
}
|
|
30
26
|
return writeBoundedText(encoded, path, overwrite, policy);
|
|
31
27
|
};
|
|
28
|
+
const snapshotValidationMessage = (cause) => cause instanceof TypeError
|
|
29
|
+
? cause.message
|
|
30
|
+
: "Analysis snapshot validation failed";
|