rea-agents 2.5.0 → 3.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 +14 -4
- package/bridge/ghidra/ReaGhidraBridge.java +477 -4
- package/bridge/hopper_bridge.py +54 -4
- package/dist/application/AnalysisContextQueries.js +130 -0
- package/dist/application/AnalysisSnapshotCache.js +4 -2
- package/dist/application/ArtifactInventory/scanCanonical.js +1 -1
- package/dist/application/BinarySession.js +5 -4
- package/dist/application/BinarySessionComposition.js +21 -0
- package/dist/application/BinarySessionRecords.js +79 -9
- package/dist/application/CapabilityInventory.js +16 -2
- package/dist/application/ClientRegistrationStatus.js +4 -1
- package/dist/application/DirectAnalysis.js +6 -2
- package/dist/application/EnhancedTools.js +37 -7
- package/dist/application/EvidenceLedger.js +45 -16
- package/dist/application/JavaScriptArtifactAnalysis.js +13 -4
- package/dist/application/LazyAnalysisProvider.js +77 -0
- package/dist/application/LoopbackReplay.js +1 -1
- package/dist/application/NativeApiInspection.js +80 -0
- package/dist/application/SessionProviderRouter.js +6 -6
- package/dist/application/SetupClientConfiguration.js +4 -2
- package/dist/application/runtime.js +26 -9
- package/dist/browser/PlaywrightBrowserScenarioProvider.js +8 -2
- package/dist/catalogIdentity.js +3 -3
- package/dist/cli/artifactCommands.js +5 -3
- package/dist/cli/coreAnalysisCommands.js +41 -0
- package/dist/cliCommandNames.js +1 -0
- package/dist/contracts/artifactToolContracts.js +8 -3
- package/dist/contracts/enhancedInputs.js +10 -0
- package/dist/contracts/functionWorkflowToolContracts.js +30 -0
- package/dist/contracts/promptContracts.js +14 -15
- package/dist/contracts/toolContractExamples.js +3 -0
- package/dist/contracts/toolContracts.js +21 -4
- package/dist/contracts/toolEffects.js +4 -0
- package/dist/contracts/toolOutputSchemaGroups.js +50 -10
- package/dist/domain/analysisErrorPresentation.js +8 -2
- package/dist/domain/analysisErrorProjection.js +1 -0
- package/dist/domain/bytecodeProvider.js +179 -0
- package/dist/domain/conformancePackage.js +169 -0
- package/dist/domain/conformanceReplay.js +104 -0
- package/dist/domain/conformanceTrustGate.js +193 -0
- package/dist/domain/customProtocolCapture.js +137 -0
- package/dist/domain/eventProcessTree.js +214 -0
- package/dist/domain/evidenceBundle.js +47 -0
- package/dist/domain/hopperStartupFailure.js +1 -1
- package/dist/domain/hopperValues.js +4 -1
- package/dist/domain/inputIssueProjection.js +7 -1
- package/dist/domain/javascriptSemanticAnalysis.js +8 -12
- package/dist/domain/javascriptSourceParser.js +15 -0
- package/dist/domain/javascriptStaticAnalysis.js +8 -12
- package/dist/domain/mobileApplicationInvestigation.js +159 -0
- package/dist/domain/nativeApiBoundary.js +130 -0
- package/dist/domain/objcSwiftMetadata.js +150 -0
- package/dist/domain/packageAnalysis.js +123 -0
- package/dist/domain/peInspection.js +196 -0
- package/dist/domain/protocolCapture.js +243 -0
- package/dist/dotnet/ManagedStaticProvider.js +34 -5
- package/dist/generatedMcpToolCatalog.js +37 -0
- package/dist/generatedPackageMetadata.js +4 -4
- package/dist/ghidra/GhidraDefaults.js +1 -1
- package/dist/ghidra/GhidraFunctionValues.js +27 -1
- package/dist/ghidra/GhidraProviderCapabilities.js +40 -38
- package/dist/hopper/HopperClient.js +26 -2
- package/dist/hopper/HopperProvider.js +42 -30
- package/dist/hopper/HopperRequestQueue.js +32 -4
- package/dist/hopper/HopperResponseStream.js +2 -2
- package/dist/hopper/protocol.js +63 -21
- package/dist/main/transport.js +2 -5
- package/dist/main.js +14 -12
- package/dist/mcpDoctor.js +2 -2
- package/dist/mcpStartupPolicy.js +14 -0
- package/dist/server/LazyToolCatalog.js +134 -0
- package/dist/server/createServer.js +64 -171
- package/dist/server/hydrateServerTools.js +191 -0
- package/dist/server/registerEnhancedTools.js +13 -3
- package/dist/server/registerEvidenceResources.js +50 -101
- package/dist/server/registerInvestigationTools.js +1 -2
- package/dist/server/registerJavaScriptApplicationGraphResource.js +0 -4
- package/dist/server/registerReconstructionObligationLedgerResource.js +0 -7
- package/dist/server/registerReconstructionReadinessResource.js +0 -7
- package/dist/server/registerSessionRecordTools.js +82 -12
- package/dist/server/registerSessionStatusTool.js +1 -1
- package/dist/server/registerSessionTools.js +21 -1
- package/dist/server/toolRegistrationOptions.js +21 -6
- package/dist/server/toolResult.js +12 -0
- package/package.json +21 -10
- package/scripts/package-runner-bootstrap.mjs +72 -0
- package/scripts/rea.mjs +11 -1
- package/skills/reverse-engineer-anything/SKILL.md +10 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import canonicalize from "canonicalize";
|
|
2
|
-
import { createEvidenceBundle, parseEvidenceBundle, } from "../domain/evidenceBundle.js";
|
|
2
|
+
import { createEvidenceBundle, parseEvidenceBundle, validateResidualUnknownAddition, } from "../domain/evidenceBundle.js";
|
|
3
3
|
import { EvidenceIntegrityError, EvidenceLimitError, UnknownRegistryError, } from "../domain/errors.js";
|
|
4
4
|
import { parseEvidence } from "../domain/evidence.js";
|
|
5
5
|
import { createResidualUnknown, updateResidualUnknown, } from "../domain/residualUnknown.js";
|
|
@@ -142,26 +142,19 @@ export class EvidenceLedger {
|
|
|
142
142
|
catch (cause) {
|
|
143
143
|
return err(new UnknownRegistryError("invalid-transition", { cause }));
|
|
144
144
|
}
|
|
145
|
-
const pendingRecords = new Map(this.#records);
|
|
146
|
-
pendingRecords.set(evidence.evidence_id, evidence);
|
|
147
145
|
const existingUnknown = this.#unknownHeads.get(unknown.unknown_id);
|
|
148
146
|
if (existingUnknown !== undefined) {
|
|
149
147
|
if (existingUnknown.revision !== 1 ||
|
|
150
148
|
existingUnknown.revision_digest !== unknown.revision_digest)
|
|
151
149
|
return err(new UnknownRegistryError("already-exists"));
|
|
152
|
-
const checked = this.#
|
|
150
|
+
const checked = this.#appendIncremental([evidence]);
|
|
153
151
|
if (!checked.ok)
|
|
154
152
|
return checked;
|
|
155
|
-
this.#commit(pendingRecords, this.#unknownRevisions, checked.value);
|
|
156
153
|
return ok(null);
|
|
157
154
|
}
|
|
158
|
-
|
|
159
|
-
const pendingUnknowns = new Map(this.#unknownRevisions);
|
|
160
|
-
pendingUnknowns.set(unknownRevisionKey(unknown), unknown);
|
|
161
|
-
const checked = this.#validateCandidate(pendingRecords, pendingUnknowns);
|
|
155
|
+
const checked = this.#appendIncremental([evidence, mutation], unknown);
|
|
162
156
|
if (!checked.ok)
|
|
163
157
|
return checked;
|
|
164
|
-
this.#commit(pendingRecords, pendingUnknowns, checked.value);
|
|
165
158
|
return ok(structuredClone(unknown));
|
|
166
159
|
}
|
|
167
160
|
/** Apply one approved full-state update using optimistic revision matching. */
|
|
@@ -218,16 +211,52 @@ export class EvidenceLedger {
|
|
|
218
211
|
cause,
|
|
219
212
|
}));
|
|
220
213
|
}
|
|
221
|
-
const
|
|
222
|
-
pendingRecords.set(mutationEvidence.evidence_id, mutationEvidence);
|
|
223
|
-
const pendingUnknowns = new Map(this.#unknownRevisions);
|
|
224
|
-
pendingUnknowns.set(unknownRevisionKey(unknown), unknown);
|
|
225
|
-
const checked = this.#validateCandidate(pendingRecords, pendingUnknowns);
|
|
214
|
+
const checked = this.#appendIncremental([mutationEvidence], unknown);
|
|
226
215
|
if (!checked.ok)
|
|
227
216
|
return checked;
|
|
228
|
-
this.#commit(pendingRecords, pendingUnknowns, checked.value);
|
|
229
217
|
return ok(structuredClone(unknown));
|
|
230
218
|
}
|
|
219
|
+
#appendIncremental(records, unknown) {
|
|
220
|
+
const additions = new Map();
|
|
221
|
+
let addedBytes = 0;
|
|
222
|
+
for (const evidence of records) {
|
|
223
|
+
const existing = additions.get(evidence.evidence_id) ??
|
|
224
|
+
this.#records.get(evidence.evidence_id);
|
|
225
|
+
if (existing !== undefined) {
|
|
226
|
+
if (!recordsAgree(existing, evidence))
|
|
227
|
+
return err(new EvidenceIntegrityError("Conflicting evidence record"));
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
additions.set(evidence.evidence_id, evidence);
|
|
231
|
+
addedBytes += serializedBytes(evidence);
|
|
232
|
+
}
|
|
233
|
+
const unknownKey = unknown === undefined ? undefined : unknownRevisionKey(unknown);
|
|
234
|
+
if (unknownKey !== undefined && this.#unknownRevisions.has(unknownKey))
|
|
235
|
+
return err(new UnknownRegistryError("integrity"));
|
|
236
|
+
const unknownBytes = unknown === undefined ? 0 : serializedBytes(unknown);
|
|
237
|
+
if (this.#exceedsRecordLimit(this.#records.size + additions.size, this.#unknownRevisions.size + (unknown === undefined ? 0 : 1)))
|
|
238
|
+
return err(new EvidenceLimitError("records", this.limits.maxRecords));
|
|
239
|
+
if (this.#bytes + addedBytes + unknownBytes > this.limits.maxBytes)
|
|
240
|
+
return err(new EvidenceLimitError("bytes", this.limits.maxBytes));
|
|
241
|
+
if (unknown !== undefined)
|
|
242
|
+
try {
|
|
243
|
+
validateResidualUnknownAddition(unknown, {
|
|
244
|
+
get: (id) => additions.get(id) ?? this.#records.get(id),
|
|
245
|
+
has: (id) => additions.has(id) || this.#records.has(id),
|
|
246
|
+
}, this.#unknownHeads);
|
|
247
|
+
}
|
|
248
|
+
catch (cause) {
|
|
249
|
+
return err(new UnknownRegistryError("integrity", { cause }));
|
|
250
|
+
}
|
|
251
|
+
for (const [id, evidence] of additions)
|
|
252
|
+
this.#records.set(id, evidence);
|
|
253
|
+
if (unknown !== undefined && unknownKey !== undefined) {
|
|
254
|
+
this.#unknownRevisions.set(unknownKey, unknown);
|
|
255
|
+
this.#unknownHeads.set(unknown.unknown_id, unknown);
|
|
256
|
+
}
|
|
257
|
+
this.#bytes += addedBytes + unknownBytes;
|
|
258
|
+
return ok(undefined);
|
|
259
|
+
}
|
|
231
260
|
#validateCandidate(records, unknowns) {
|
|
232
261
|
if (this.#exceedsRecordLimit(records.size, unknowns.size))
|
|
233
262
|
return err(new EvidenceLimitError("records", this.limits.maxRecords));
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { analyzeParsedJavaScriptStaticSource } from "../domain/javascriptStaticAnalysis.js";
|
|
3
|
+
import { analyzeParsedJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
|
|
4
4
|
import { DEFAULT_JAVASCRIPT_SEMANTIC_LIMITS, } from "../domain/javascriptSemanticIr.js";
|
|
5
|
+
import { parseJavaScriptSource } from "../domain/javascriptSourceParser.js";
|
|
6
|
+
import { failedJavaScriptStaticAnalysis } from "../domain/javascriptStaticAnalysisHelpers.js";
|
|
5
7
|
import { analyzeJavaScriptJsonModule } from "./JavaScriptJsonModules.js";
|
|
6
8
|
/** Analyze selected text files under one shared AST, finding, module, and time budget. */
|
|
7
9
|
export const analyzeJavaScriptArtifactFiles = (fileSet, input, now) => {
|
|
@@ -70,7 +72,14 @@ const analyzeArtifactFile = (file, context) => {
|
|
|
70
72
|
state.truncatedScopes += 1;
|
|
71
73
|
return;
|
|
72
74
|
}
|
|
73
|
-
const
|
|
75
|
+
const parsed = parseJavaScriptSource(file.text.value);
|
|
76
|
+
if (parsed === null) {
|
|
77
|
+
const analysis = failedJavaScriptStaticAnalysis();
|
|
78
|
+
state.files.push({ file, javascript: analysis, semantic: null });
|
|
79
|
+
state.parseFailures += 1;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const analysis = analyzeParsedJavaScriptStaticSource(file.text.value, parsed, {
|
|
74
83
|
maxAstNodes: remainingNodes,
|
|
75
84
|
maxFindings: remainingFindings,
|
|
76
85
|
maxModules: remainingModules,
|
|
@@ -88,7 +97,7 @@ const analyzeArtifactFile = (file, context) => {
|
|
|
88
97
|
maxModuleLinks: semanticFindingBudget,
|
|
89
98
|
};
|
|
90
99
|
const semantics = analysis.parse_status === "complete" || analysis.parse_status === "partial"
|
|
91
|
-
?
|
|
100
|
+
? analyzeParsedJavaScriptSemantics(parsed, semanticLimits)
|
|
92
101
|
: null;
|
|
93
102
|
state.files.push({
|
|
94
103
|
file,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preserve synchronous provider discovery while loading implementation modules
|
|
3
|
+
* only when a target-bound client first executes.
|
|
4
|
+
*/
|
|
5
|
+
export class LazyAnalysisProvider {
|
|
6
|
+
#providerIdentity;
|
|
7
|
+
#capabilityDescriptors;
|
|
8
|
+
#loadProvider;
|
|
9
|
+
#provider;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.#providerIdentity = options.identity;
|
|
12
|
+
this.#capabilityDescriptors = options.capabilities;
|
|
13
|
+
this.#loadProvider = options.load;
|
|
14
|
+
}
|
|
15
|
+
/** Return generated provider identity without loading its implementation. */
|
|
16
|
+
identity() {
|
|
17
|
+
return this.#providerIdentity;
|
|
18
|
+
}
|
|
19
|
+
/** Return generated capability metadata without loading its implementation. */
|
|
20
|
+
capabilities() {
|
|
21
|
+
return this.#capabilityDescriptors;
|
|
22
|
+
}
|
|
23
|
+
/** Create a client whose implementation is loaded on first execution. */
|
|
24
|
+
createClient(target, profile, context) {
|
|
25
|
+
return new LazyAnalysisClient(async () => {
|
|
26
|
+
const provider = await this.#load();
|
|
27
|
+
return provider.createClient(target, profile, context);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async #load() {
|
|
31
|
+
this.#provider ??= this.#loadProvider().catch((cause) => {
|
|
32
|
+
this.#provider = undefined;
|
|
33
|
+
throw cause;
|
|
34
|
+
});
|
|
35
|
+
return this.#provider;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
class LazyAnalysisClient {
|
|
39
|
+
#loadClient;
|
|
40
|
+
#client;
|
|
41
|
+
#loading;
|
|
42
|
+
#closed = false;
|
|
43
|
+
constructor(loadClient) {
|
|
44
|
+
this.#loadClient = loadClient;
|
|
45
|
+
}
|
|
46
|
+
execute = async (operation, parameters, options) => {
|
|
47
|
+
const client = await this.#load();
|
|
48
|
+
return client.execute(operation, parameters, options);
|
|
49
|
+
};
|
|
50
|
+
runtimeLineageSnapshots() {
|
|
51
|
+
return this.#client?.runtimeLineageSnapshots?.() ?? [];
|
|
52
|
+
}
|
|
53
|
+
requestActivitySnapshots() {
|
|
54
|
+
return this.#client?.requestActivitySnapshots?.() ?? [];
|
|
55
|
+
}
|
|
56
|
+
async close() {
|
|
57
|
+
this.#closed = true;
|
|
58
|
+
if (this.#loading === undefined)
|
|
59
|
+
return;
|
|
60
|
+
const client = await this.#loading;
|
|
61
|
+
await client.close();
|
|
62
|
+
}
|
|
63
|
+
async #load() {
|
|
64
|
+
if (this.#closed)
|
|
65
|
+
throw new Error("Lazy analysis client is closed");
|
|
66
|
+
this.#loading ??= this.#loadClient()
|
|
67
|
+
.then((client) => {
|
|
68
|
+
this.#client = client;
|
|
69
|
+
return client;
|
|
70
|
+
})
|
|
71
|
+
.catch((cause) => {
|
|
72
|
+
this.#loading = undefined;
|
|
73
|
+
throw cause;
|
|
74
|
+
});
|
|
75
|
+
return this.#loading;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -374,11 +374,11 @@ export const startLoopbackReplay = async (scenario, recordEvent = () => undefine
|
|
|
374
374
|
async close() {
|
|
375
375
|
closePromise ??= (async () => {
|
|
376
376
|
recorder.stopMachineAdmission();
|
|
377
|
+
await recorder.drainMachine();
|
|
377
378
|
for (const client of websocket.clients)
|
|
378
379
|
client.terminate();
|
|
379
380
|
await new Promise((resolveClose) => websocket.close(() => resolveClose()));
|
|
380
381
|
await closeReplayServer(server);
|
|
381
|
-
await recorder.drainMachine();
|
|
382
382
|
})();
|
|
383
383
|
await closePromise;
|
|
384
384
|
},
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { nativeApiInspectionResultSchema, } from "../domain/nativeApiBoundary.js";
|
|
2
|
+
const unavailableBoundary = () => ({
|
|
3
|
+
available: false,
|
|
4
|
+
reason: "The selected provider did not expose a structured native API boundary model.",
|
|
5
|
+
residual_unknowns: [
|
|
6
|
+
"What are the function's return and parameter boundary types?",
|
|
7
|
+
"Does this function dispatch through a jump table with recoverable data addresses?",
|
|
8
|
+
],
|
|
9
|
+
});
|
|
10
|
+
/** Project one analyzed-function dossier into inspectable native API substeps. */
|
|
11
|
+
export const projectNativeApiInspection = (dossier) => {
|
|
12
|
+
const boundary = dossier.native_api ?? unavailableBoundary();
|
|
13
|
+
const unsupportedBranches = boundary.available
|
|
14
|
+
? []
|
|
15
|
+
: ["structured-boundary-types", "jump-table-data-mapping"];
|
|
16
|
+
const residualUnknowns = boundary.available
|
|
17
|
+
? availableResidualUnknowns(boundary)
|
|
18
|
+
: boundary.residual_unknowns;
|
|
19
|
+
return nativeApiInspectionResultSchema.parse({
|
|
20
|
+
schema_version: 1,
|
|
21
|
+
procedure: {
|
|
22
|
+
address: dossier.procedure.address,
|
|
23
|
+
name: dossier.procedure.name,
|
|
24
|
+
},
|
|
25
|
+
boundary,
|
|
26
|
+
substeps: [
|
|
27
|
+
{
|
|
28
|
+
operation: "analyze_function",
|
|
29
|
+
status: "completed",
|
|
30
|
+
observations: [
|
|
31
|
+
"provider function dossier",
|
|
32
|
+
"provider pseudocode classification",
|
|
33
|
+
"provider control-flow observations",
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
operation: "project_native_api_boundary",
|
|
38
|
+
status: boundary.available ? "completed" : "unsupported",
|
|
39
|
+
observations: boundary.available
|
|
40
|
+
? [
|
|
41
|
+
"return and parameter boundary types",
|
|
42
|
+
"confidence and inference evidence",
|
|
43
|
+
"jump-table dispatch, data, and target addresses",
|
|
44
|
+
]
|
|
45
|
+
: [boundary.reason],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
operation: "preserve_residual_unknowns",
|
|
49
|
+
status: "completed",
|
|
50
|
+
observations: residualUnknowns.length === 0
|
|
51
|
+
? ["no additional structured-boundary unknowns"]
|
|
52
|
+
: residualUnknowns,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
unsupported_branches: unsupportedBranches,
|
|
56
|
+
residual_unknowns: residualUnknowns,
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const availableResidualUnknowns = (boundary) => {
|
|
60
|
+
const unknowns = [];
|
|
61
|
+
if (boundary.parameters_truncated)
|
|
62
|
+
unknowns.push("Which additional parameters were omitted by the native API boundary limit?");
|
|
63
|
+
if (boundary.jump_tables_truncated)
|
|
64
|
+
unknowns.push("Which additional jump tables were omitted by the native API boundary limit?");
|
|
65
|
+
for (const table of boundary.jump_tables) {
|
|
66
|
+
if (table.data_sources_truncated || table.mappings_truncated)
|
|
67
|
+
unknowns.push(`Which additional data sources or targets were omitted for the jump table dispatched at ${table.dispatch_address}?`);
|
|
68
|
+
if (table.data_sources.length === 0)
|
|
69
|
+
unknowns.push(`What backing data address produced the jump table dispatched at ${table.dispatch_address}?`);
|
|
70
|
+
if (table.data_sources.some(({ entry_count: count, entry_size_bytes: size }) => count === null || size === null))
|
|
71
|
+
unknowns.push(`What are the exact entry size and count for candidate jump-table data dispatched at ${table.dispatch_address}?`);
|
|
72
|
+
if (table.mappings.some(({ case_value: value }) => value === null))
|
|
73
|
+
unknowns.push(`Which source-level case values correspond to every target dispatched at ${table.dispatch_address}?`);
|
|
74
|
+
}
|
|
75
|
+
for (const value of [boundary.return_type, ...boundary.parameters]) {
|
|
76
|
+
if (value.confidence === "low")
|
|
77
|
+
unknowns.push(`Can the inferred ${value.role} type ${value.data_type} be confirmed with imported metadata or an ABI probe?`);
|
|
78
|
+
}
|
|
79
|
+
return [...new Set(unknowns)];
|
|
80
|
+
};
|
|
@@ -26,7 +26,7 @@ export class SessionProviderRouter {
|
|
|
26
26
|
: (target, resolutionOptions) => provider.resolveAnalysisProfile?.(target, resolutionOptions) ??
|
|
27
27
|
Promise.resolve(ok({ profile: null, compatibility: {} })));
|
|
28
28
|
return new SessionProviderRouter({
|
|
29
|
-
kind: "
|
|
29
|
+
kind: "single",
|
|
30
30
|
provider,
|
|
31
31
|
identity,
|
|
32
32
|
capabilities,
|
|
@@ -61,7 +61,7 @@ export class SessionProviderRouter {
|
|
|
61
61
|
}
|
|
62
62
|
/** Provider identities exposed through the flat 1.x compatibility field. */
|
|
63
63
|
providerIdentities(route) {
|
|
64
|
-
if (this.runtime.kind === "
|
|
64
|
+
if (this.runtime.kind === "single") {
|
|
65
65
|
if (route.capabilities === undefined)
|
|
66
66
|
return [route.identity];
|
|
67
67
|
return uniqueProviderIdentities([...route.capabilities.values()].map(({ provider }) => provider));
|
|
@@ -86,7 +86,7 @@ export class SessionProviderRouter {
|
|
|
86
86
|
compatibility: {},
|
|
87
87
|
binding: null,
|
|
88
88
|
selection: undefined,
|
|
89
|
-
createClient: (target, context) =>
|
|
89
|
+
createClient: (target, context) => createSingleProviderClient(runtime, target, undefined, context),
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
/** Resolve target routes before any provider client is created. */
|
|
@@ -122,12 +122,12 @@ export class SessionProviderRouter {
|
|
|
122
122
|
compatibility: normalized.value.compatibility,
|
|
123
123
|
binding: null,
|
|
124
124
|
selection: undefined,
|
|
125
|
-
createClient: (openedTarget, context) =>
|
|
125
|
+
createClient: (openedTarget, context) => createSingleProviderClient(runtime, openedTarget, normalized.value.profile ?? undefined, context),
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
128
|
/** Candidate status authoritative for the active or target-free route. */
|
|
129
129
|
candidateStatuses(route) {
|
|
130
|
-
if (this.runtime.kind === "
|
|
130
|
+
if (this.runtime.kind === "single")
|
|
131
131
|
return [];
|
|
132
132
|
return route.selection?.candidates ?? this.runtime.registry.candidates();
|
|
133
133
|
}
|
|
@@ -160,7 +160,7 @@ const selectableRoute = (auxiliaryProviders, selection) => {
|
|
|
160
160
|
emptyClient(identity),
|
|
161
161
|
};
|
|
162
162
|
};
|
|
163
|
-
const
|
|
163
|
+
const createSingleProviderClient = (runtime, target, profile, context) => typeof runtime.provider === "function"
|
|
164
164
|
? runtime.provider(target, profile, context)
|
|
165
165
|
: runtime.provider.createClient(target, profile, context);
|
|
166
166
|
const emptyClient = (identity) => ({
|
|
@@ -5,6 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import writeFileAtomic from "write-file-atomic";
|
|
6
6
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7
7
|
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
8
|
+
import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js";
|
|
8
9
|
import { resolveClientConfigTransactionPath } from "./ClientConfigPath.js";
|
|
9
10
|
const defaultCommand = () => [
|
|
10
11
|
"npx",
|
|
@@ -12,7 +13,6 @@ const defaultCommand = () => [
|
|
|
12
13
|
PRODUCT_IDENTITY.registrationPackageSpecifier,
|
|
13
14
|
"mcp",
|
|
14
15
|
];
|
|
15
|
-
const CODEX_STARTUP_TIMEOUT_SECONDS = 30;
|
|
16
16
|
/** Back up, atomically update, and semantically read back one JSON MCP configuration. */
|
|
17
17
|
export const configureJsonClient = async (client, environment = {}, command = defaultCommand()) => {
|
|
18
18
|
const transactionPath = await resolveClientConfigTransactionPath(client.configPath);
|
|
@@ -214,7 +214,9 @@ const clientConfigurationDesired = (client, providerEnvironment, command) => {
|
|
|
214
214
|
command: command[0] ?? PRODUCT_IDENTITY.cliBinary,
|
|
215
215
|
args: command.slice(1),
|
|
216
216
|
...(client.name === "codex"
|
|
217
|
-
? {
|
|
217
|
+
? {
|
|
218
|
+
startup_timeout_sec: MCP_STARTUP_POLICY.codexStartupTimeoutSeconds,
|
|
219
|
+
}
|
|
218
220
|
: {}),
|
|
219
221
|
...(Object.keys(environment).length === 0 ? {} : { env: environment }),
|
|
220
222
|
};
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import { BinarySession } from "./BinarySession.js";
|
|
2
1
|
import { HopperProvider } from "../hopper/HopperProvider.js";
|
|
3
2
|
import { GhidraProvider } from "../ghidra/GhidraProvider.js";
|
|
4
|
-
import { NativeMacOSProvider } from "../native/NativeMacOSProvider.js";
|
|
5
|
-
import { ArtifactProvider } from "../artifacts/ArtifactProvider.js";
|
|
6
|
-
import { ManagedStaticProvider } from "../dotnet/ManagedStaticProvider.js";
|
|
7
3
|
import { silentLogger } from "../logger.js";
|
|
4
|
+
import { GENERATED_AUXILIARY_PROVIDERS } from "../generatedMcpToolCatalog.js";
|
|
8
5
|
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
|
|
9
|
-
import {
|
|
6
|
+
import { composeBinarySession } from "./BinarySessionComposition.js";
|
|
7
|
+
import { LazyAnalysisProvider } from "./LazyAnalysisProvider.js";
|
|
10
8
|
/**
|
|
11
9
|
* Compose the target-switching runtime shared directly by CLI and MCP adapters.
|
|
12
10
|
* This is the sole production wiring point, so both adapters share identical
|
|
@@ -15,9 +13,28 @@ import { SessionProviderRouter } from "./SessionProviderRouter.js";
|
|
|
15
13
|
export const createBinarySession = (config, logger = silentLogger) => {
|
|
16
14
|
const hopper = new HopperProvider(config, logger);
|
|
17
15
|
const ghidra = new GhidraProvider(config, logger);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
new ManagedStaticProvider(),
|
|
16
|
+
const auxiliary = new Map(GENERATED_AUXILIARY_PROVIDERS.map((provider) => [
|
|
17
|
+
provider.identity.id,
|
|
18
|
+
provider,
|
|
22
19
|
]));
|
|
20
|
+
const lazyProvider = (id, load) => {
|
|
21
|
+
const generated = auxiliary.get(id);
|
|
22
|
+
if (generated === undefined)
|
|
23
|
+
throw new TypeError(`Missing generated provider metadata for ${id}`);
|
|
24
|
+
return new LazyAnalysisProvider({ ...generated, load });
|
|
25
|
+
};
|
|
26
|
+
return composeBinarySession(new AnalysisProviderRegistry([hopper, ghidra], config.analysisProvider), [
|
|
27
|
+
lazyProvider("rea-artifact-graph", async () => {
|
|
28
|
+
const { ArtifactProvider } = await import("../artifacts/ArtifactProvider.js");
|
|
29
|
+
return new ArtifactProvider(config.artifactNativeMountEnabled, config.artifactIntegrityContinueEnabled);
|
|
30
|
+
}),
|
|
31
|
+
lazyProvider("native-macos", async () => {
|
|
32
|
+
const { NativeMacOSProvider } = await import("../native/NativeMacOSProvider.js");
|
|
33
|
+
return new NativeMacOSProvider();
|
|
34
|
+
}),
|
|
35
|
+
lazyProvider("rea-dotnet-static", async () => {
|
|
36
|
+
const { ManagedStaticProvider } = await import("../dotnet/ManagedStaticProvider.js");
|
|
37
|
+
return new ManagedStaticProvider();
|
|
38
|
+
}),
|
|
39
|
+
]);
|
|
23
40
|
};
|
|
@@ -3,8 +3,14 @@ import { browserScenarioCaptureSchema, browserScenarioCompletenessSchema, browse
|
|
|
3
3
|
import { AnalysisError, BrowserObservationError, ProviderAdapterError, } from "../domain/errors.js";
|
|
4
4
|
import { err, ok } from "../domain/result.js";
|
|
5
5
|
import { BrowserScenarioCaptureBudget } from "./PlaywrightScenarioArtifacts.js";
|
|
6
|
-
import { PlaywrightScenarioSessionFactory } from "./PlaywrightScenarioSession.js";
|
|
7
6
|
const OPERATION = "capture_browser_scenario";
|
|
7
|
+
let defaultFactory;
|
|
8
|
+
const lazyPlaywrightFactory = {
|
|
9
|
+
async open(scenario, options) {
|
|
10
|
+
defaultFactory ??= import("./PlaywrightScenarioSession.js").then(({ PlaywrightScenarioSessionFactory }) => new PlaywrightScenarioSessionFactory());
|
|
11
|
+
return (await defaultFactory).open(scenario, options);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
8
14
|
export const PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY = Object.freeze({
|
|
9
15
|
id: "rea-playwright-browser-scenario",
|
|
10
16
|
name: "REA Playwright browser scenario capture provider",
|
|
@@ -234,7 +240,7 @@ const runScenario = async (factory, scenario, options) => {
|
|
|
234
240
|
/** Controlled Playwright/CDP scenario driver with exact process ownership. */
|
|
235
241
|
export class PlaywrightBrowserScenarioProvider {
|
|
236
242
|
factory;
|
|
237
|
-
constructor(factory =
|
|
243
|
+
constructor(factory = lazyPlaywrightFactory) {
|
|
238
244
|
this.factory = factory;
|
|
239
245
|
}
|
|
240
246
|
identity() {
|
package/dist/catalogIdentity.js
CHANGED
|
@@ -29,7 +29,7 @@ const promptCatalog = PROMPT_CONTRACTS.map((contract) => ({
|
|
|
29
29
|
/** Canonical fixed resources exposed by every target-free MCP session. */
|
|
30
30
|
export const MCP_RESOURCE_CATALOG = [
|
|
31
31
|
{ name: "server-identity", uri: "rea://server/identity" },
|
|
32
|
-
{ name: "
|
|
32
|
+
{ name: "analysis-snapshot", uri: "rea://snapshot/current" },
|
|
33
33
|
];
|
|
34
34
|
/** Canonical resource templates exposed by every target-free MCP session. */
|
|
35
35
|
export const MCP_RESOURCE_TEMPLATE_CATALOG = [
|
|
@@ -40,8 +40,8 @@ export const MCP_RESOURCE_TEMPLATE_CATALOG = [
|
|
|
40
40
|
},
|
|
41
41
|
{ name: "residual-unknown", uri_template: "rea://unknown/{unknownId}" },
|
|
42
42
|
{
|
|
43
|
-
name: "
|
|
44
|
-
uri_template: "rea://
|
|
43
|
+
name: "evidence-bundle",
|
|
44
|
+
uri_template: "rea://evidence-bundle/{bundleDigest}",
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
name: "artifact-page",
|
|
@@ -73,17 +73,19 @@ const registerExtractionCommand = (cli, logger) => {
|
|
|
73
73
|
args: z.object({
|
|
74
74
|
path: z.string().describe("Application or package path"),
|
|
75
75
|
outputRoot: z.string().describe("Absent absolute output root"),
|
|
76
|
+
}),
|
|
77
|
+
options: z.object({
|
|
76
78
|
occurrenceIds: z
|
|
77
79
|
.array(z.string().regex(/^occ_[a-f0-9]{64}$/u))
|
|
78
80
|
.min(1)
|
|
79
81
|
.max(500)
|
|
80
82
|
.describe("Exact artifact occurrence IDs selected for extraction"),
|
|
81
83
|
}),
|
|
82
|
-
alias: {
|
|
83
|
-
run: ({ args }) => logCliCommand(logger, "extract-artifact", () => runProviderAnalysis(args.path, "extract_artifact", {
|
|
84
|
+
alias: { occurrenceIds: "occurrence-ids" },
|
|
85
|
+
run: ({ args, options }) => logCliCommand(logger, "extract-artifact", () => runProviderAnalysis(args.path, "extract_artifact", {
|
|
84
86
|
approved: true,
|
|
85
87
|
output_root: args.outputRoot,
|
|
86
|
-
occurrence_ids:
|
|
88
|
+
occurrence_ids: options.occurrenceIds,
|
|
87
89
|
}, logger)),
|
|
88
90
|
});
|
|
89
91
|
};
|
|
@@ -9,6 +9,7 @@ import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnal
|
|
|
9
9
|
export const registerCoreAnalysisCommands = (cli, logger) => {
|
|
10
10
|
registerCoreCommands(cli, logger);
|
|
11
11
|
registerFunctionCommand(cli, logger);
|
|
12
|
+
registerNativeApiCommand(cli, logger);
|
|
12
13
|
registerInstructionsCommand(cli, logger);
|
|
13
14
|
registerSearchCommand(cli, logger);
|
|
14
15
|
registerXrefsCommand(cli, logger);
|
|
@@ -213,6 +214,46 @@ const registerFunctionCommand = (cli, logger) => {
|
|
|
213
214
|
}, directAnalysisOptions(logger, options.snapshot, options.provider))),
|
|
214
215
|
});
|
|
215
216
|
};
|
|
217
|
+
const registerNativeApiCommand = (cli, logger) => {
|
|
218
|
+
cli.command(CLI_COMMANDS.inspectNativeApi, {
|
|
219
|
+
description: "Reconstruct one native function API boundary with evidence",
|
|
220
|
+
args: z.object({
|
|
221
|
+
path: z.string().describe("App or program path"),
|
|
222
|
+
address: z.string().describe("Procedure name or address"),
|
|
223
|
+
}),
|
|
224
|
+
options: z.object({
|
|
225
|
+
maxPseudocodeChars: z
|
|
226
|
+
.number()
|
|
227
|
+
.int()
|
|
228
|
+
.min(1)
|
|
229
|
+
.max(100_000)
|
|
230
|
+
.default(20_000)
|
|
231
|
+
.describe("Maximum pseudocode characters to inspect"),
|
|
232
|
+
maxInstructions: z
|
|
233
|
+
.number()
|
|
234
|
+
.int()
|
|
235
|
+
.min(1)
|
|
236
|
+
.max(5_000)
|
|
237
|
+
.default(500)
|
|
238
|
+
.describe("Maximum assembly instructions to inspect"),
|
|
239
|
+
snapshot: z
|
|
240
|
+
.string()
|
|
241
|
+
.min(1)
|
|
242
|
+
.optional()
|
|
243
|
+
.describe("Load and update a local analysis snapshot"),
|
|
244
|
+
provider: providerSelectionOption,
|
|
245
|
+
}),
|
|
246
|
+
alias: {
|
|
247
|
+
maxPseudocodeChars: "max-pseudocode-chars",
|
|
248
|
+
maxInstructions: "max-instructions",
|
|
249
|
+
},
|
|
250
|
+
run: ({ args, options }) => logCliCommand(logger, "inspect-native-api", () => runDirectAnalysis(args.path, "inspect_native_api", {
|
|
251
|
+
procedure: args.address,
|
|
252
|
+
max_pseudocode_chars: options.maxPseudocodeChars,
|
|
253
|
+
max_instructions: options.maxInstructions,
|
|
254
|
+
}, directAnalysisOptions(logger, options.snapshot, options.provider))),
|
|
255
|
+
});
|
|
256
|
+
};
|
|
216
257
|
const registerInstructionsCommand = (cli, logger) => {
|
|
217
258
|
cli.command(CLI_COMMANDS.instructions, {
|
|
218
259
|
description: "Read one bounded raw-instruction window without decompiling",
|
package/dist/cliCommandNames.js
CHANGED
|
@@ -12,6 +12,7 @@ export const CLI_COMMANDS = Object.freeze({
|
|
|
12
12
|
capabilities: "capabilities",
|
|
13
13
|
providers: "providers",
|
|
14
14
|
function: "function",
|
|
15
|
+
inspectNativeApi: "inspect-native-api",
|
|
15
16
|
instructions: "instructions",
|
|
16
17
|
search: "search",
|
|
17
18
|
importReferenceSource: "import-reference-source",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { artifactOutputSchemas } from "./toolOutputSchemas.js";
|
|
3
4
|
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
@@ -51,7 +52,11 @@ export const artifactInventoryInputSchema = z
|
|
|
51
52
|
/** Exact caller boundary for approved artifact extraction. */
|
|
52
53
|
export const artifactExtractionInputSchema = z.object({
|
|
53
54
|
approved: z.literal(true),
|
|
54
|
-
output_root: z
|
|
55
|
+
output_root: z
|
|
56
|
+
.string()
|
|
57
|
+
.min(1)
|
|
58
|
+
.max(4_096)
|
|
59
|
+
.refine(isAbsolute, "Extraction output root must be absolute"),
|
|
55
60
|
occurrence_ids: z
|
|
56
61
|
.array(z.string().regex(/^occ_[a-f0-9]{64}$/u))
|
|
57
62
|
.min(1)
|
|
@@ -110,7 +115,7 @@ const artifact = (name, description, inputSchema) => {
|
|
|
110
115
|
};
|
|
111
116
|
/** Artifact-graph inventory and explicitly approved safe extraction contracts. */
|
|
112
117
|
export const ARTIFACT_TOOL_CONTRACTS = [
|
|
113
|
-
artifact("inventory_artifact", "
|
|
114
|
-
artifact("inspect_artifact", "
|
|
118
|
+
artifact("inventory_artifact", "After open_binary binds a local archive, application package, or other artifact—or when one is already active—inventory it as a deterministic, content-addressed artifact graph. This tool accepts no path; in a target-free session open the target first. Returns bounded node and edge pages without extracting or mounting by default.", artifactInventoryInputSchema),
|
|
119
|
+
artifact("inspect_artifact", "After open_binary binds a local archive, application package, or other artifact—or when one is already active—inspect it through one bounded, cancellable inventory substep. This tool accepts no path; in a target-free session open the target first. Returns the full substep Evidence and its ID, observations, derived relationships, hypotheses, contradictions, unexplored branches, limitations, and format-specific next probes. Any substep failure fails the whole call.", artifactInspectionInputSchema),
|
|
115
120
|
artifact("extract_artifact", "Extract selected graph artifacts beneath an explicit output root. Requires approval, rejects traversal and symlink escapes, never overwrites, enforces bomb limits, and verifies cleanup.", artifactExtractionInputSchema),
|
|
116
121
|
];
|
|
@@ -61,6 +61,15 @@ export const enhancedInputSchemas = {
|
|
|
61
61
|
basic_blocks: 0,
|
|
62
62
|
}),
|
|
63
63
|
}),
|
|
64
|
+
inspect_native_api: z.object({
|
|
65
|
+
procedure: z.string().describe("A procedure name or address"),
|
|
66
|
+
max_pseudocode_chars: z.number().int().min(1).max(100_000).default(20_000),
|
|
67
|
+
max_instructions: z.number().int().min(1).max(5_000).default(500),
|
|
68
|
+
unknown_registry_approved: z
|
|
69
|
+
.literal(true)
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("Explicit approval to record unsupported native API branches durably"),
|
|
72
|
+
}),
|
|
64
73
|
trace_feature: traceLiteralInputSchema,
|
|
65
74
|
find_code_for_string: traceLiteralInputSchema,
|
|
66
75
|
trace_call_path: z.object({
|
|
@@ -90,6 +99,7 @@ export const enhancedToolNameSchema = z.enum([
|
|
|
90
99
|
"find_xrefs_to_name",
|
|
91
100
|
"binary_overview",
|
|
92
101
|
"analyze_function",
|
|
102
|
+
"inspect_native_api",
|
|
93
103
|
"trace_feature",
|
|
94
104
|
"find_code_for_string",
|
|
95
105
|
"trace_call_path",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsonObjectSchema } from "../domain/jsonValue.js";
|
|
2
|
+
import { enhancedInputSchemas } from "./enhancedInputs.js";
|
|
3
|
+
import { TOOL_EXAMPLE_OVERRIDES } from "./toolContractExamples.js";
|
|
4
|
+
import { toolContractMetadata } from "./toolEffects.js";
|
|
5
|
+
import { enhancedOutputSchemas } from "./toolOutputSchemas.js";
|
|
6
|
+
const functionWorkflow = (name, description) => {
|
|
7
|
+
const inputSchema = enhancedInputSchemas[name];
|
|
8
|
+
const outputSchema = enhancedOutputSchemas[name];
|
|
9
|
+
if (outputSchema === undefined)
|
|
10
|
+
throw new Error(`Missing function workflow output schema for ${name}`);
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
...toolContractMetadata(name),
|
|
14
|
+
description,
|
|
15
|
+
kind: "enhanced",
|
|
16
|
+
inputSchema,
|
|
17
|
+
outputSchema,
|
|
18
|
+
examples: [
|
|
19
|
+
{
|
|
20
|
+
title: `Example ${name.replaceAll("_", " ")} request`,
|
|
21
|
+
input: jsonObjectSchema.parse(inputSchema.parse(TOOL_EXAMPLE_OVERRIDES[name] ?? {})),
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
/** Function dossier and native API reconstruction workflow contracts. */
|
|
27
|
+
export const FUNCTION_WORKFLOW_TOOL_CONTRACTS = [
|
|
28
|
+
functionWorkflow("analyze_function", "Preferred bounded analysis for one procedure symbol or address. Returns identity, provider-specific pseudocode, optional assembly, comments, calls, typed-or-explicitly-unavailable references, referenced strings/names, and local CFG blocks with exact truncation metadata. Providers with a structured decompiler model also expose native API boundary types, confidence, evidence, jump-table data/target addresses, and explicit decompiler-artifact labels."),
|
|
29
|
+
functionWorkflow("inspect_native_api", "Reconstruct one native function boundary through inspectable substeps. Returns structured inferred return/parameter types with confidence and evidence, jump-table dispatch/data/target mappings, explicit decompiler-artifact labels, unsupported branches, and residual unknowns."),
|
|
30
|
+
];
|