rea-agents 0.3.0 → 0.4.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 +124 -20
- package/bridge/hopper_bridge.py +141 -13
- package/dist/application/AnalysisProvider.js +12 -1
- package/dist/application/ArtifactExtraction.js +166 -0
- package/dist/application/ArtifactGraphConstruction.js +257 -0
- package/dist/application/ArtifactInventory.js +253 -0
- package/dist/application/BinarySession.js +164 -14
- package/dist/application/CompositeProvider.js +73 -0
- package/dist/application/DirectAnalysis.js +27 -3
- package/dist/application/Doctor.js +35 -6
- package/dist/application/EnhancedTools.js +8 -7
- package/dist/application/EvidenceBundleCommands.js +29 -0
- package/dist/application/EvidenceBundleFiles.js +127 -0
- package/dist/application/EvidenceLedger.js +225 -18
- package/dist/application/FilesystemSnapshot.js +124 -0
- package/dist/application/LinuxHopper.js +186 -0
- package/dist/application/LoopbackReplay.js +195 -37
- package/dist/application/ProcessHarness.js +200 -191
- package/dist/application/ProcessNormalization.js +44 -0
- package/dist/application/ProcessOwnership.js +84 -0
- package/dist/application/ProcessSampling.js +284 -0
- package/dist/application/RealHopperAssertions.js +105 -0
- package/dist/application/ReferenceSourceImport.js +182 -0
- package/dist/application/ReferenceSourceImportEntries.js +122 -0
- package/dist/application/ReferenceSourceImportPolicy.js +73 -0
- package/dist/application/ReferenceSourceImportTypes.js +18 -0
- package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
- package/dist/application/Setup.js +192 -41
- package/dist/application/Uninstall.js +130 -0
- package/dist/application/runtime.js +8 -1
- package/dist/artifacts/ArtifactPaths.js +51 -0
- package/dist/artifacts/ArtifactProvider.js +130 -0
- package/dist/artifacts/ArtifactReader.js +9 -0
- package/dist/artifacts/AsarArtifactReader.js +62 -0
- package/dist/artifacts/DirectoryArtifactReader.js +107 -0
- package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
- package/dist/artifacts/SafeOutputTree.js +199 -0
- package/dist/artifacts/StreamBytes.js +10 -0
- package/dist/artifacts/ZipArtifactReader.js +109 -0
- package/dist/cli.js +149 -2
- package/dist/config.js +35 -1
- package/dist/contracts/artifactComparisonExample.js +95 -0
- package/dist/contracts/artifactToolContracts.js +84 -0
- package/dist/contracts/enhancedInputs.js +4 -0
- package/dist/contracts/functionComparisonExample.js +57 -0
- package/dist/contracts/investigationExamples.js +118 -0
- package/dist/contracts/nativeToolContracts.js +52 -0
- package/dist/contracts/processCaptureExample.js +25 -0
- package/dist/contracts/toolContractExamples.js +69 -0
- package/dist/contracts/toolContracts.js +99 -36
- package/dist/contracts/toolOutputSchemas.js +154 -82
- package/dist/contracts/unknownContractExamples.js +33 -0
- package/dist/domain/artifactComparison.js +273 -0
- package/dist/domain/artifactGraph.js +194 -0
- package/dist/domain/artifactInventoryEvidence.js +150 -0
- package/dist/domain/binaryTarget.js +55 -0
- package/dist/domain/bundleComparison.js +266 -0
- package/dist/domain/callPath.js +346 -0
- package/dist/domain/changedBehavior.js +294 -0
- package/dist/domain/errors.js +192 -4
- package/dist/domain/evidence.js +41 -10
- package/dist/domain/evidenceBundle.js +187 -6
- package/dist/domain/functionComparison.js +201 -0
- package/dist/domain/functionComparisonNormalization.js +112 -0
- package/dist/domain/functionComparisonResults.js +54 -0
- package/dist/domain/functionComparisonSchemas.js +82 -0
- package/dist/domain/functionDossierEvidence.js +171 -0
- package/dist/domain/hopperValues.js +14 -7
- package/dist/domain/nativeInspection.js +142 -0
- package/dist/domain/processCapture.js +152 -54
- package/dist/domain/processComparison.js +106 -0
- package/dist/domain/reconstructionUnknowns.js +90 -0
- package/dist/domain/reconstructionVerification.js +285 -0
- package/dist/domain/reconstructionVerificationSchemas.js +126 -0
- package/dist/domain/referenceSourceClassification.js +496 -0
- package/dist/domain/referenceSourceGraph.js +376 -0
- package/dist/domain/referenceSourceImportParsing.js +235 -0
- package/dist/domain/referenceSourcePolicy.js +1 -0
- package/dist/domain/residualUnknown.js +239 -0
- package/dist/domain/staticRuntimeCorrelation.js +375 -0
- package/dist/hopper/BridgeLauncher.js +40 -3
- package/dist/hopper/HopperClient.js +14 -5
- package/dist/hopper/HopperProvider.js +57 -22
- package/dist/identity.js +1 -0
- package/dist/main.js +5 -1
- package/dist/native/CommandRunner.js +156 -0
- package/dist/native/NativeMacOSProvider.js +306 -0
- package/dist/native/NativeMachoInspection.js +135 -0
- package/dist/native/parsers/codesign.js +55 -0
- package/dist/native/parsers/demangle.js +26 -0
- package/dist/native/parsers/dyldInfo.js +25 -0
- package/dist/native/parsers/lipo.js +67 -0
- package/dist/native/parsers/otool.js +193 -0
- package/dist/native/parsers/plist.js +23 -0
- package/dist/reference/ReferenceSourceReader.js +73 -0
- package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
- package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
- package/dist/reference/ReferenceSourceReaderFile.js +119 -0
- package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
- package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
- package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
- package/dist/server/createServer.js +31 -5
- package/dist/server/recordDerivedEvidence.js +10 -0
- package/dist/server/registerArtifactComparisonTool.js +62 -0
- package/dist/server/registerArtifactTools.js +6 -0
- package/dist/server/registerBundleComparisonTool.js +47 -0
- package/dist/server/registerEnhancedTools.js +64 -12
- package/dist/server/registerEvidenceTools.js +36 -0
- package/dist/server/registerFunctionComparisonTool.js +68 -0
- package/dist/server/registerInvestigationTools.js +224 -0
- package/dist/server/registerNativeTools.js +6 -0
- package/dist/server/registerOfficialTools.js +47 -14
- package/dist/server/registerProcessComparisonTool.js +106 -0
- package/dist/server/registerSessionTools.js +179 -70
- package/dist/server/sessionEvidence.js +28 -0
- package/dist/server/sessionToolPolicies.js +64 -0
- package/dist/server/toolRegistrationOptions.js +7 -0
- package/dist/server/toolResult.js +8 -5
- package/install.sh +198 -0
- package/package.json +18 -1
- package/scripts/rea.mjs +5 -1
- package/skills/rea-analysis/SKILL.md +77 -2
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { parseBinaryTarget } from "../domain/binaryTarget.js";
|
|
2
|
-
import {
|
|
2
|
+
import { AnalysisCapabilityUnavailableError, EvidenceIntegrityError, EvidenceLimitError, AnalysisCancelledError, NoBinaryOpenError, } from "../domain/errors.js";
|
|
3
3
|
import { err, ok } from "../domain/result.js";
|
|
4
|
+
import { createEvidence } from "../domain/evidence.js";
|
|
4
5
|
import { EvidenceLedger } from "./EvidenceLedger.js";
|
|
6
|
+
import { UnknownRegistryError } from "../domain/errors.js";
|
|
7
|
+
import { enhancedToolNameSchema } from "../contracts/enhancedInputs.js";
|
|
8
|
+
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
|
9
|
+
const REGISTRY_PROVIDER = {
|
|
10
|
+
id: "rea-unknown-registry",
|
|
11
|
+
name: "REA residual unknown registry",
|
|
12
|
+
version: "1",
|
|
13
|
+
};
|
|
14
|
+
const OFFICIAL_OPERATIONS = new Set(OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name));
|
|
5
15
|
/**
|
|
6
16
|
* Owns the single active target shared by CLI and MCP adapters.
|
|
7
17
|
*
|
|
@@ -17,7 +27,11 @@ export class BinarySession {
|
|
|
17
27
|
#calls = new Set();
|
|
18
28
|
#createClient;
|
|
19
29
|
#providerIdentity;
|
|
20
|
-
#
|
|
30
|
+
#capabilities;
|
|
31
|
+
#evidence = new EvidenceLedger({
|
|
32
|
+
maxRecords: 10_000,
|
|
33
|
+
maxBytes: 64 * 1024 * 1024,
|
|
34
|
+
});
|
|
21
35
|
constructor(provider) {
|
|
22
36
|
this.provider = provider;
|
|
23
37
|
this.#createClient =
|
|
@@ -28,14 +42,45 @@ export class BinarySession {
|
|
|
28
42
|
typeof provider === "function"
|
|
29
43
|
? { id: "unidentified", name: "Unidentified provider", version: null }
|
|
30
44
|
: provider.identity();
|
|
45
|
+
this.#capabilities =
|
|
46
|
+
typeof provider === "function"
|
|
47
|
+
? undefined
|
|
48
|
+
: new Map(provider
|
|
49
|
+
.capabilities()
|
|
50
|
+
.map((descriptor) => [descriptor.operation, descriptor]));
|
|
31
51
|
}
|
|
32
52
|
/** Identify the provider producing evidence for this session. */
|
|
33
|
-
providerIdentity() {
|
|
53
|
+
providerIdentity(operation) {
|
|
54
|
+
if (operation !== undefined) {
|
|
55
|
+
const exact = this.#capabilities?.get(operation)?.provider;
|
|
56
|
+
if (exact !== undefined)
|
|
57
|
+
return exact;
|
|
58
|
+
if (enhancedToolNameSchema.safeParse(operation).success) {
|
|
59
|
+
const providers = new Map();
|
|
60
|
+
for (const descriptor of this.#capabilities?.values() ?? [])
|
|
61
|
+
if (descriptor.available &&
|
|
62
|
+
OFFICIAL_OPERATIONS.has(descriptor.operation))
|
|
63
|
+
providers.set(descriptor.provider.id, descriptor.provider);
|
|
64
|
+
if (providers.size === 1) {
|
|
65
|
+
const provider = providers.values().next().value;
|
|
66
|
+
if (provider !== undefined)
|
|
67
|
+
return provider;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
34
71
|
return this.#providerIdentity;
|
|
35
72
|
}
|
|
36
73
|
/** Add one successful public observation to the session ledger. */
|
|
37
74
|
recordEvidence(evidence) {
|
|
38
|
-
this.#evidence.record(evidence);
|
|
75
|
+
return this.#evidence.record(evidence);
|
|
76
|
+
}
|
|
77
|
+
/** Check that comparison input Evidence is already session-owned. */
|
|
78
|
+
hasEvidence(evidenceId) {
|
|
79
|
+
return this.#evidence.has(evidenceId);
|
|
80
|
+
}
|
|
81
|
+
/** Read one detached session-owned Evidence record by semantic ID. */
|
|
82
|
+
evidenceById(evidenceId) {
|
|
83
|
+
return this.#evidence.get(evidenceId);
|
|
39
84
|
}
|
|
40
85
|
/** Return a deterministic snapshot without clearing session evidence. */
|
|
41
86
|
exportEvidenceBundle() {
|
|
@@ -45,6 +90,42 @@ export class BinarySession {
|
|
|
45
90
|
importEvidenceBundle(bundle) {
|
|
46
91
|
return this.#evidence.import(bundle);
|
|
47
92
|
}
|
|
93
|
+
/** Create an approved residual unknown and immutable mutation evidence. */
|
|
94
|
+
recordUnknown(input) {
|
|
95
|
+
const evidence = unknownMutationEvidence(this.#active?.target, input);
|
|
96
|
+
return this.#evidence.recordUnknown(input, evidence);
|
|
97
|
+
}
|
|
98
|
+
/** Atomically record derived Evidence and its approved residual unknown. */
|
|
99
|
+
recordEvidenceWithUnknown(evidence, input) {
|
|
100
|
+
return this.#evidence.recordWithUnknown(evidence, input, unknownMutationEvidence(undefined, input));
|
|
101
|
+
}
|
|
102
|
+
/** Update one unknown using compare-and-swap revision semantics. */
|
|
103
|
+
updateUnknown(input) {
|
|
104
|
+
const evidence = createEvidence(this.#active?.target, REGISTRY_PROVIDER, {
|
|
105
|
+
predicateType: "rea.residual-unknown-mutation/v1",
|
|
106
|
+
operation: "update_unknown",
|
|
107
|
+
parameters: {
|
|
108
|
+
unknown_id: input.unknown_id,
|
|
109
|
+
expected_revision: input.expected_revision,
|
|
110
|
+
},
|
|
111
|
+
result: { action: "update", status: input.status },
|
|
112
|
+
confidence: "derived",
|
|
113
|
+
authority: "analyst-inference",
|
|
114
|
+
evidenceLinks: unknownEvidenceLinks(input),
|
|
115
|
+
limitations: [
|
|
116
|
+
"Registry mutation evidence records analyst intent, not proof of the answer.",
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
return this.#evidence.updateUnknown(input, evidence);
|
|
120
|
+
}
|
|
121
|
+
/** Query stable current residual-unknown heads. */
|
|
122
|
+
listUnknowns(filters = {}) {
|
|
123
|
+
return this.#evidence.listUnknowns(filters);
|
|
124
|
+
}
|
|
125
|
+
/** Check whether current head is a bundle-valid resolved state. */
|
|
126
|
+
verifyUnknownResolution(unknownId) {
|
|
127
|
+
return this.#evidence.verifyUnknownResolution(unknownId);
|
|
128
|
+
}
|
|
48
129
|
/**
|
|
49
130
|
* Open or switch targets after draining calls against the current target.
|
|
50
131
|
* Returns the switch failure even if best-effort restoration also fails.
|
|
@@ -52,12 +133,12 @@ export class BinarySession {
|
|
|
52
133
|
open(path, options = {}) {
|
|
53
134
|
return this.#serialize(async () => {
|
|
54
135
|
if (isAborted(options.signal))
|
|
55
|
-
return err(new
|
|
136
|
+
return err(new AnalysisCancelledError("open_binary"));
|
|
56
137
|
const parsed = await parseBinaryTarget(path, process.cwd(), process.arch, options.targetKind);
|
|
57
138
|
if (!parsed.ok)
|
|
58
139
|
return parsed;
|
|
59
140
|
if (isAborted(options.signal))
|
|
60
|
-
return err(new
|
|
141
|
+
return err(new AnalysisCancelledError("open_binary"));
|
|
61
142
|
if (this.#active?.target.path === parsed.value.path)
|
|
62
143
|
return ok(parsed.value);
|
|
63
144
|
await this.#drainCalls();
|
|
@@ -74,7 +155,7 @@ export class BinarySession {
|
|
|
74
155
|
if (isAborted(options.signal)) {
|
|
75
156
|
await client.close();
|
|
76
157
|
await this.#restore(previous);
|
|
77
|
-
return err(new
|
|
158
|
+
return err(new AnalysisCancelledError("open_binary"));
|
|
78
159
|
}
|
|
79
160
|
this.#active = { target: parsed.value, client };
|
|
80
161
|
return ok(parsed.value);
|
|
@@ -94,10 +175,45 @@ export class BinarySession {
|
|
|
94
175
|
/** Describe the active binary session. */
|
|
95
176
|
status() {
|
|
96
177
|
const target = this.#active?.target;
|
|
178
|
+
const provider = {
|
|
179
|
+
id: this.#providerIdentity.id,
|
|
180
|
+
name: this.#providerIdentity.name,
|
|
181
|
+
version: this.#providerIdentity.version,
|
|
182
|
+
};
|
|
183
|
+
const capabilities = this.#capabilities === undefined
|
|
184
|
+
? []
|
|
185
|
+
: [...this.#capabilities.values()]
|
|
186
|
+
.sort((left, right) => left.operation.localeCompare(right.operation))
|
|
187
|
+
.map((descriptor) => ({
|
|
188
|
+
operation: descriptor.operation,
|
|
189
|
+
available: descriptor.available,
|
|
190
|
+
reason: descriptor.reason,
|
|
191
|
+
input_contract_version: descriptor.inputContractVersion,
|
|
192
|
+
output_contract_version: descriptor.outputContractVersion,
|
|
193
|
+
pagination: descriptor.pagination,
|
|
194
|
+
exhaustive: descriptor.exhaustive,
|
|
195
|
+
effects: {
|
|
196
|
+
mutates_artifact: descriptor.effects.mutatesArtifact,
|
|
197
|
+
launches_process: descriptor.effects.launchesProcess,
|
|
198
|
+
may_show_ui: descriptor.effects.mayShowUi,
|
|
199
|
+
may_access_network: descriptor.effects.mayAccessNetwork,
|
|
200
|
+
may_write_filesystem: descriptor.effects.mayWriteFilesystem,
|
|
201
|
+
changes_permissions: descriptor.effects.changesPermissions,
|
|
202
|
+
requires_root: descriptor.effects.requiresRoot,
|
|
203
|
+
},
|
|
204
|
+
limits: {
|
|
205
|
+
max_results: descriptor.limits.maxResults,
|
|
206
|
+
max_payload_bytes: descriptor.limits.maxPayloadBytes,
|
|
207
|
+
timeout_ms: descriptor.limits.timeoutMs,
|
|
208
|
+
},
|
|
209
|
+
limitations: [...descriptor.limitations],
|
|
210
|
+
}));
|
|
97
211
|
return target === undefined
|
|
98
|
-
? { open: false }
|
|
212
|
+
? { open: false, provider, capabilities }
|
|
99
213
|
: {
|
|
100
214
|
open: true,
|
|
215
|
+
provider,
|
|
216
|
+
capabilities,
|
|
101
217
|
path: target.path,
|
|
102
218
|
sha256: target.sha256,
|
|
103
219
|
format: target.format,
|
|
@@ -115,9 +231,12 @@ export class BinarySession {
|
|
|
115
231
|
* entering until the transition has settled.
|
|
116
232
|
*/
|
|
117
233
|
async execute(name, arguments_, options) {
|
|
118
|
-
const transitioned = await this.#waitForTransition(options?.signal);
|
|
234
|
+
const transitioned = await this.#waitForTransition(name, options?.signal);
|
|
119
235
|
if (!transitioned.ok)
|
|
120
236
|
return transitioned;
|
|
237
|
+
const capability = this.#capabilities?.get(name);
|
|
238
|
+
if (this.#capabilities !== undefined && capability?.available !== true)
|
|
239
|
+
return err(new AnalysisCapabilityUnavailableError(this.#providerIdentity.id, name, capability?.reason ?? "operation is not declared by this provider"));
|
|
121
240
|
const active = this.#active;
|
|
122
241
|
if (active === undefined)
|
|
123
242
|
return err(new NoBinaryOpenError());
|
|
@@ -148,26 +267,57 @@ export class BinarySession {
|
|
|
148
267
|
else
|
|
149
268
|
await client.close();
|
|
150
269
|
}
|
|
151
|
-
async #waitForTransition(signal) {
|
|
270
|
+
async #waitForTransition(operation, signal) {
|
|
152
271
|
if (signal?.aborted === true)
|
|
153
|
-
return err(new
|
|
272
|
+
return err(new AnalysisCancelledError(operation));
|
|
154
273
|
if (signal === undefined) {
|
|
155
274
|
await this.#transition;
|
|
156
275
|
return ok(undefined);
|
|
157
276
|
}
|
|
158
277
|
return new Promise((resolve) => {
|
|
159
278
|
const onAbort = () => {
|
|
160
|
-
resolve(err(new
|
|
279
|
+
resolve(err(new AnalysisCancelledError(operation)));
|
|
161
280
|
};
|
|
162
281
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
163
282
|
this.#transition.then(() => {
|
|
164
283
|
signal.removeEventListener("abort", onAbort);
|
|
165
|
-
resolve(signal.aborted
|
|
284
|
+
resolve(signal.aborted
|
|
285
|
+
? err(new AnalysisCancelledError(operation))
|
|
286
|
+
: ok(undefined));
|
|
166
287
|
}, () => {
|
|
167
288
|
signal.removeEventListener("abort", onAbort);
|
|
168
|
-
resolve(signal.aborted
|
|
289
|
+
resolve(signal.aborted
|
|
290
|
+
? err(new AnalysisCancelledError(operation))
|
|
291
|
+
: ok(undefined));
|
|
169
292
|
});
|
|
170
293
|
});
|
|
171
294
|
}
|
|
172
295
|
}
|
|
296
|
+
const unknownMutationEvidence = (target, input) => createEvidence(target, REGISTRY_PROVIDER, {
|
|
297
|
+
predicateType: "rea.residual-unknown-mutation/v1",
|
|
298
|
+
operation: "record_unknown",
|
|
299
|
+
parameters: {
|
|
300
|
+
domain: input.domain,
|
|
301
|
+
severity: input.severity,
|
|
302
|
+
},
|
|
303
|
+
result: {
|
|
304
|
+
action: "record",
|
|
305
|
+
question: input.question,
|
|
306
|
+
required_authority: input.required_authority,
|
|
307
|
+
required_confidence: input.required_confidence,
|
|
308
|
+
},
|
|
309
|
+
confidence: "derived",
|
|
310
|
+
authority: "analyst-inference",
|
|
311
|
+
evidenceLinks: unknownEvidenceLinks(input),
|
|
312
|
+
limitations: [
|
|
313
|
+
"Registry mutation evidence records analyst intent, not proof of the answer.",
|
|
314
|
+
],
|
|
315
|
+
});
|
|
316
|
+
const unknownEvidenceLinks = (input) => [
|
|
317
|
+
...input.supporting_evidence_ids,
|
|
318
|
+
...input.contradicting_evidence_ids,
|
|
319
|
+
...("resolution" in input && input.resolution !== null
|
|
320
|
+
? input.resolution.evidence_ids
|
|
321
|
+
: []),
|
|
322
|
+
].filter((id, index, values) => values.indexOf(id) === index);
|
|
173
323
|
const isAborted = (signal) => signal?.aborted === true;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ProviderSelectionError } from "../domain/errors.js";
|
|
2
|
+
import { err, ok } from "../domain/result.js";
|
|
3
|
+
const compositeIdentity = (providers) => ({
|
|
4
|
+
id: `composite:${providers
|
|
5
|
+
.map((provider) => provider.identity().id)
|
|
6
|
+
.sort()
|
|
7
|
+
.join("+")}`,
|
|
8
|
+
name: "REA composite analysis provider",
|
|
9
|
+
version: null,
|
|
10
|
+
});
|
|
11
|
+
/** Deterministically route disjoint operations without eagerly starting children. */
|
|
12
|
+
export class CompositeProvider {
|
|
13
|
+
providers;
|
|
14
|
+
#identity;
|
|
15
|
+
#capabilities;
|
|
16
|
+
#providerByOperation;
|
|
17
|
+
constructor(providers) {
|
|
18
|
+
this.providers = providers;
|
|
19
|
+
if (providers.length === 0)
|
|
20
|
+
throw new RangeError("CompositeProvider requires at least one provider");
|
|
21
|
+
this.#identity = Object.freeze(compositeIdentity(providers));
|
|
22
|
+
const routes = new Map();
|
|
23
|
+
const capabilities = [];
|
|
24
|
+
for (const provider of providers) {
|
|
25
|
+
for (const descriptor of provider.capabilities()) {
|
|
26
|
+
if (routes.has(descriptor.operation))
|
|
27
|
+
throw new TypeError(`Multiple providers declare operation ${descriptor.operation}`);
|
|
28
|
+
routes.set(descriptor.operation, provider);
|
|
29
|
+
capabilities.push(descriptor);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
this.#providerByOperation = routes;
|
|
33
|
+
this.#capabilities = Object.freeze(capabilities.sort((left, right) => left.operation.localeCompare(right.operation) ||
|
|
34
|
+
left.provider.id.localeCompare(right.provider.id)));
|
|
35
|
+
}
|
|
36
|
+
identity() {
|
|
37
|
+
return this.#identity;
|
|
38
|
+
}
|
|
39
|
+
capabilities() {
|
|
40
|
+
return this.#capabilities;
|
|
41
|
+
}
|
|
42
|
+
createClient(target) {
|
|
43
|
+
const clients = new Map();
|
|
44
|
+
const clientFor = (provider) => {
|
|
45
|
+
const existing = clients.get(provider);
|
|
46
|
+
if (existing !== undefined)
|
|
47
|
+
return existing;
|
|
48
|
+
const created = provider.createClient(target);
|
|
49
|
+
clients.set(provider, created);
|
|
50
|
+
return created;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
execute: (operation, parameters, options) => {
|
|
54
|
+
if (operation === "health")
|
|
55
|
+
return Promise.resolve(ok({
|
|
56
|
+
result: null,
|
|
57
|
+
rawResult: null,
|
|
58
|
+
provider: this.#identity,
|
|
59
|
+
limitations: [],
|
|
60
|
+
locations: [],
|
|
61
|
+
subject: null,
|
|
62
|
+
}));
|
|
63
|
+
const provider = this.#providerByOperation.get(operation);
|
|
64
|
+
return provider === undefined
|
|
65
|
+
? Promise.resolve(err(new ProviderSelectionError(operation)))
|
|
66
|
+
: clientFor(provider).execute(operation, parameters, options);
|
|
67
|
+
},
|
|
68
|
+
close: async () => {
|
|
69
|
+
await Promise.allSettled([...clients.values()].map(async (client) => client.close()));
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { parseConfig } from "../config.js";
|
|
2
|
+
import { EnhancedTools } from "./EnhancedTools.js";
|
|
2
3
|
import { createBinarySession } from "./runtime.js";
|
|
3
4
|
import { silentLogger } from "../logger.js";
|
|
4
5
|
import { createEvidence } from "../domain/evidence.js";
|
|
6
|
+
const WORKFLOW_PROVIDER = {
|
|
7
|
+
id: "rea-workflow",
|
|
8
|
+
name: "REA composed investigation workflow",
|
|
9
|
+
version: "1",
|
|
10
|
+
};
|
|
5
11
|
/**
|
|
6
12
|
* Open one binary, execute one tool, and always release the bridge session.
|
|
7
13
|
* Unlike MCP mode, every CLI invocation is intentionally isolated and does not
|
|
8
14
|
* retain a target or bridge resources for a subsequent command.
|
|
9
15
|
*/
|
|
10
|
-
export const runDirectAnalysis = async (path, tool, arguments_, logger = silentLogger) =>
|
|
16
|
+
export const runDirectAnalysis = async (path, tool, arguments_, logger = silentLogger) => runAnalysis(path, tool, arguments_, logger);
|
|
17
|
+
/** Execute one provider-native semantic operation with atomic provenance. */
|
|
18
|
+
export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger) => runAnalysis(path, tool, arguments_, logger);
|
|
19
|
+
const runAnalysis = async (path, tool, arguments_, logger) => {
|
|
11
20
|
const config = parseConfig(process.env);
|
|
12
21
|
if (!config.ok)
|
|
13
22
|
return { error: config.error._tag, message: config.error.message };
|
|
@@ -16,12 +25,27 @@ export const runDirectAnalysis = async (path, tool, arguments_, logger = silentL
|
|
|
16
25
|
const opened = await session.open(path);
|
|
17
26
|
if (!opened.ok)
|
|
18
27
|
return { error: opened.error._tag, message: opened.error.message };
|
|
28
|
+
if (tool === "binary_overview") {
|
|
29
|
+
const result = await new EnhancedTools(session).execute(tool, arguments_);
|
|
30
|
+
return result.ok
|
|
31
|
+
? createEvidence(opened.value, WORKFLOW_PROVIDER, {
|
|
32
|
+
operation: tool,
|
|
33
|
+
parameters: arguments_,
|
|
34
|
+
result: result.value,
|
|
35
|
+
confidence: "derived",
|
|
36
|
+
limitations: ["Derived by an REA composed workflow."],
|
|
37
|
+
})
|
|
38
|
+
: { error: result.error._tag, message: result.error.message };
|
|
39
|
+
}
|
|
19
40
|
const result = await session.execute(tool, arguments_);
|
|
20
41
|
return result.ok
|
|
21
|
-
? createEvidence(opened.value,
|
|
42
|
+
? createEvidence(result.value.subject ?? opened.value, result.value.provider, {
|
|
22
43
|
operation: tool,
|
|
23
44
|
parameters: arguments_,
|
|
24
|
-
result: result.value,
|
|
45
|
+
result: result.value.result,
|
|
46
|
+
rawResult: result.value.rawResult,
|
|
47
|
+
limitations: result.value.limitations,
|
|
48
|
+
locations: result.value.locations,
|
|
25
49
|
})
|
|
26
50
|
: { error: result.error._tag, message: result.error.message };
|
|
27
51
|
}
|
|
@@ -7,8 +7,10 @@ import { execFile } from "node:child_process";
|
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
8
|
import { parseBinaryTarget } from "../domain/binaryTarget.js";
|
|
9
9
|
import { probeHomebrew } from "./homebrew.js";
|
|
10
|
+
import { linuxHopperLauncherPath, linuxSharedLibrariesAvailable, readLinuxDistribution, } from "./LinuxHopper.js";
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
const DEFAULT_HOPPER = "/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
|
|
13
|
+
const SYSTEM_LINUX_HOPPER = "/opt/hopper/bin/Hopper";
|
|
12
14
|
/**
|
|
13
15
|
* Check requirements and an optional target without mutating the host.
|
|
14
16
|
* Every failed check includes remediation suitable for either the one-shot CLI
|
|
@@ -17,13 +19,27 @@ const DEFAULT_HOPPER = "/Applications/Hopper Disassembler.app/Contents/MacOS/hop
|
|
|
17
19
|
export const runDoctor = async (target, host = systemDoctorHost()) => {
|
|
18
20
|
const checks = [];
|
|
19
21
|
const nodeMajor = parseMajor(host.nodeVersion);
|
|
20
|
-
checks.push(check("node", nodeMajor >=
|
|
22
|
+
checks.push(check("node", nodeMajor >= 24, host.nodeVersion, {
|
|
23
|
+
remediation: "Install Node.js 24.18 or newer.",
|
|
24
|
+
classification: "missing_dependency",
|
|
25
|
+
}));
|
|
21
26
|
const macosVersion = host.platform === "darwin" ? await host.macosVersion() : undefined;
|
|
22
27
|
const macosMajor = macosVersion === undefined ? 0 : parseMajor(macosVersion);
|
|
23
|
-
|
|
28
|
+
const linuxDistribution = host.platform === "linux" ? await host.linuxDistribution() : undefined;
|
|
29
|
+
const supportedHost = (host.platform === "darwin" && macosMajor >= 12) ||
|
|
30
|
+
(host.platform === "linux" && linuxDistribution?.supported === true);
|
|
31
|
+
checks.push(check("host", supportedHost, macosVersion ??
|
|
32
|
+
(linuxDistribution === undefined
|
|
33
|
+
? host.platform
|
|
34
|
+
: `${linuxDistribution.id} ${linuxDistribution.versionId ?? "unknown"}`), {
|
|
35
|
+
remediation: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",
|
|
36
|
+
classification: "unsupported_host",
|
|
37
|
+
}));
|
|
24
38
|
const candidates = [
|
|
25
39
|
host.configuredHopperPath,
|
|
26
40
|
DEFAULT_HOPPER,
|
|
41
|
+
SYSTEM_LINUX_HOPPER,
|
|
42
|
+
linuxHopperLauncherPath(homedir()),
|
|
27
43
|
...(await host.manualHopperPaths()),
|
|
28
44
|
await host.brewHopperPath(),
|
|
29
45
|
].filter((value) => value !== undefined);
|
|
@@ -33,9 +49,15 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
|
|
|
33
49
|
hopperPath = candidate;
|
|
34
50
|
break;
|
|
35
51
|
}
|
|
36
|
-
checks.push(check("hopper", hopperPath !== undefined, hopperPath,
|
|
52
|
+
checks.push(check("hopper", hopperPath !== undefined, hopperPath, {
|
|
53
|
+
remediation: "Run rea setup --yes to install Hopper, or set HOPPER_LAUNCHER_PATH.",
|
|
54
|
+
classification: "missing_analysis_engine",
|
|
55
|
+
}));
|
|
37
56
|
if (target !== undefined)
|
|
38
|
-
checks.push(check("target", await host.validTarget(target), target,
|
|
57
|
+
checks.push(check("target", await host.validTarget(target), target, {
|
|
58
|
+
remediation: "Supply a readable local app or program path.",
|
|
59
|
+
classification: "config_drift",
|
|
60
|
+
}));
|
|
39
61
|
return {
|
|
40
62
|
healthy: checks.every(({ ok }) => ok),
|
|
41
63
|
...(hopperPath === undefined ? {} : { hopperPath }),
|
|
@@ -57,12 +79,18 @@ export const systemDoctorHost = () => ({
|
|
|
57
79
|
return undefined;
|
|
58
80
|
}
|
|
59
81
|
},
|
|
82
|
+
linuxDistribution: readLinuxDistribution,
|
|
60
83
|
async validTarget(path) {
|
|
61
84
|
return (await parseBinaryTarget(path)).ok;
|
|
62
85
|
},
|
|
63
86
|
async executable(path) {
|
|
64
87
|
try {
|
|
65
88
|
await access(path, constants.X_OK);
|
|
89
|
+
if (process.platform === "linux") {
|
|
90
|
+
const linked = await execFileAsync("ldd", [path]);
|
|
91
|
+
if (!linuxSharedLibrariesAvailable(`${linked.stdout}\n${linked.stderr}`))
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
66
94
|
return true;
|
|
67
95
|
}
|
|
68
96
|
catch {
|
|
@@ -102,9 +130,10 @@ export const systemDoctorHost = () => ({
|
|
|
102
130
|
},
|
|
103
131
|
});
|
|
104
132
|
const parseMajor = (version) => Number.parseInt(version.split(".")[0] ?? "0", 10);
|
|
105
|
-
const check = (name, ok, detail,
|
|
133
|
+
const check = (name, ok, detail, failure) => ({
|
|
106
134
|
name,
|
|
107
135
|
ok,
|
|
136
|
+
classification: ok ? "healthy" : failure.classification,
|
|
108
137
|
...(detail === undefined ? {} : { detail }),
|
|
109
|
-
...(ok ? {} : { remediation }),
|
|
138
|
+
...(ok ? {} : { remediation: failure.remediation }),
|
|
110
139
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
|
|
2
|
-
import {
|
|
2
|
+
import { AnalysisInputError, AnalysisOutputError, } from "../domain/errors.js";
|
|
3
3
|
import { parseDocuments, parseFunctionDossier, parseAddressedPage, parseListCount, parseRelatedAddresses, parseSegments, } from "../domain/hopperValues.js";
|
|
4
4
|
import { err, ok } from "../domain/result.js";
|
|
5
5
|
import { categorizeSwiftTypes, discoverObjcClasses, discoverObjcProtocols, discoverSwiftClasses, } from "../domain/symbolAnalysis.js";
|
|
@@ -282,10 +282,10 @@ export class EnhancedTools {
|
|
|
282
282
|
if (!xrefs.ok)
|
|
283
283
|
return xrefs;
|
|
284
284
|
if (!Array.isArray(xrefs.value))
|
|
285
|
-
return err(new
|
|
285
|
+
return err(new AnalysisOutputError("xrefs", "provider returned a non-array result"));
|
|
286
286
|
for (const source of xrefs.value) {
|
|
287
287
|
if (typeof source !== "string")
|
|
288
|
-
return err(new
|
|
288
|
+
return err(new AnalysisOutputError("xrefs", "provider returned a non-address value"));
|
|
289
289
|
if (operations >= operationBudget) {
|
|
290
290
|
residual.add("Containing-procedure resolution stopped at the operation budget.");
|
|
291
291
|
break;
|
|
@@ -331,16 +331,17 @@ export class EnhancedTools {
|
|
|
331
331
|
return ok(entries);
|
|
332
332
|
}
|
|
333
333
|
if (page.value.nextOffset <= offset) {
|
|
334
|
-
return err(new
|
|
334
|
+
return err(new AnalysisOutputError(tool, "provider returned a non-advancing pagination offset"));
|
|
335
335
|
}
|
|
336
336
|
offset = page.value.nextOffset;
|
|
337
337
|
}
|
|
338
338
|
}
|
|
339
|
-
#call(name, arguments_, signal) {
|
|
340
|
-
|
|
339
|
+
async #call(name, arguments_, signal) {
|
|
340
|
+
const execution = await this.analysis.execute(name, arguments_, signal === undefined ? {} : { signal });
|
|
341
|
+
return execution.ok ? ok(execution.value.result) : execution;
|
|
341
342
|
}
|
|
342
343
|
}
|
|
343
|
-
const invalidInput = (name, cause) => Promise.resolve(err(new
|
|
344
|
+
const invalidInput = (name, cause) => Promise.resolve(err(new AnalysisInputError(name, {
|
|
344
345
|
cause,
|
|
345
346
|
})));
|
|
346
347
|
const resolveAddress = (value) => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { err, ok } from "../domain/result.js";
|
|
2
|
+
import { EvidenceLedger } from "./EvidenceLedger.js";
|
|
3
|
+
import { readEvidenceBundle, writeEvidenceBundle, } from "./EvidenceBundleFiles.js";
|
|
4
|
+
/** Validate and merge one bundle using the same bounded ledger as MCP. */
|
|
5
|
+
export const importEvidenceBundleCommand = async (path, policy) => {
|
|
6
|
+
const loaded = await readEvidenceBundle(path, policy);
|
|
7
|
+
if (!loaded.ok)
|
|
8
|
+
return loaded;
|
|
9
|
+
const ledger = createLedger();
|
|
10
|
+
const imported = ledger.import(loaded.value);
|
|
11
|
+
return imported.ok
|
|
12
|
+
? ok({ imported: imported.value, total: ledger.export().records.length })
|
|
13
|
+
: imported;
|
|
14
|
+
};
|
|
15
|
+
/** Validate a source bundle and atomically export canonical bytes. */
|
|
16
|
+
export const exportEvidenceBundleCommand = async (sourcePath, outputPath, overwrite, policy) => {
|
|
17
|
+
const loaded = await readEvidenceBundle(sourcePath, policy);
|
|
18
|
+
if (!loaded.ok)
|
|
19
|
+
return loaded;
|
|
20
|
+
return projectWrite(loaded.value, await writeEvidenceBundle(loaded.value, outputPath, overwrite, policy));
|
|
21
|
+
};
|
|
22
|
+
const createLedger = () => new EvidenceLedger({ maxRecords: 10_000, maxBytes: 64 * 1024 * 1024 });
|
|
23
|
+
const projectWrite = (bundle, written) => written.ok
|
|
24
|
+
? ok({
|
|
25
|
+
path: written.value.path,
|
|
26
|
+
bytes: written.value.bytes,
|
|
27
|
+
records: bundle.records.length,
|
|
28
|
+
})
|
|
29
|
+
: err(written.error);
|