rea-agents 1.5.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -15
- package/dist/application/BinarySession.js +14 -4
- package/dist/application/BrowserEvidence.js +87 -15
- package/dist/application/BrowserObservationService.js +65 -1
- package/dist/application/CapabilityInventory.js +7 -0
- package/dist/application/ClientConfigPath.js +33 -0
- package/dist/application/CrossVersionInventory.js +16 -3
- package/dist/application/CrossVersionInvestigation.js +17 -62
- package/dist/application/CrossVersionInvestigationEvidence.js +64 -0
- package/dist/application/CrossVersionInvestigationReplay.js +170 -0
- package/dist/application/Doctor.js +16 -11
- package/dist/application/ElectronEvidence.js +35 -0
- package/dist/application/ElectronObservationPort.js +1 -0
- package/dist/application/ElectronObservationService.js +46 -0
- package/dist/application/EvidenceLedger.js +5 -2
- package/dist/application/PermissionAuthority.js +30 -0
- package/dist/application/ProcessCaptureLifecycle.js +1 -5
- package/dist/application/ProcessHarness.js +13 -7
- package/dist/application/ProcessOwnership.js +40 -20
- package/dist/application/ProcessSampling.js +59 -8
- package/dist/application/ProjectPermissionStore.js +1 -0
- package/dist/application/ReferenceSourceImport.js +13 -16
- package/dist/application/Setup.js +89 -30
- package/dist/application/SetupClients.js +7 -4
- package/dist/application/SetupInstallFailure.js +24 -0
- package/dist/application/SetupPlan.js +11 -7
- package/dist/application/SetupSkill.js +20 -3
- package/dist/application/Uninstall.js +20 -7
- package/dist/application/Upgrade.js +14 -3
- package/dist/browser/CdpBrowserProvider.js +154 -51
- package/dist/browser/CdpCaptureCompleteness.js +90 -0
- package/dist/browser/CdpCaptureDocuments.js +180 -21
- package/dist/browser/CdpCaptureEvents.js +342 -43
- package/dist/browser/CdpCaptureValues.js +5 -7
- package/dist/browser/CdpConnection.js +47 -24
- package/dist/browser/CdpElectronInspection.js +330 -0
- package/dist/browser/CdpElectronProvider.js +134 -0
- package/dist/browser/CdpEndpoint.js +64 -5
- package/dist/browser/CdpObservationSession.js +301 -0
- package/dist/browser/CdpPageCapture.js +159 -56
- package/dist/browser/CdpSafeMetadata.js +203 -0
- package/dist/browser/CdpScreenshot.js +101 -0
- package/dist/browser/CdpTargetSession.js +47 -0
- package/dist/browser/CdpWebMcpDiscovery.js +242 -0
- package/dist/browser/ElectronFileScope.js +50 -0
- package/dist/browser/PngVisualDiff.js +177 -0
- package/dist/browser/SensitiveTextCapture.js +25 -0
- package/dist/browser/WebSourceMapFetcher.js +357 -0
- package/dist/catalogIdentity.js +8 -0
- package/dist/cli.js +4 -0
- package/dist/cliBrowserAdvancedCommands.js +169 -0
- package/dist/cliBrowserCommands.js +177 -1
- package/dist/cliElectronCommands.js +114 -0
- package/dist/cliInvestigationCommands.js +15 -8
- package/dist/cliPolicyCommands.js +47 -2
- package/dist/config.js +40 -5
- package/dist/contracts/browserToolContracts.js +301 -0
- package/dist/contracts/electronToolContracts.js +73 -0
- package/dist/contracts/toolContracts.js +2 -0
- package/dist/domain/browserCompleteness.js +101 -0
- package/dist/domain/browserObservation.js +240 -14
- package/dist/domain/browserSession.js +63 -0
- package/dist/domain/electronObservation.js +162 -0
- package/dist/domain/investigationWorkspace.js +4 -0
- package/dist/domain/jsonShape.js +125 -0
- package/dist/domain/referenceSourceGraph.js +3 -1
- package/dist/domain/webBundleAnalysis.js +195 -0
- package/dist/domain/webBundleAnalyzer.js +423 -0
- package/dist/domain/webCaptureDiff.js +184 -0
- package/dist/domain/webContentArtifact.js +47 -0
- package/dist/domain/webInventory.js +61 -0
- package/dist/domain/webMcpDiscovery.js +68 -0
- package/dist/domain/webScreenshot.js +114 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/hopper/BridgeLauncher.js +2 -3
- package/dist/hopper/HopperClient.js +51 -11
- package/dist/hopper/HopperProvider.js +6 -1
- package/dist/main.js +36 -31
- package/dist/native/NativeMacOSProvider.js +3 -2
- package/dist/native/NativeMachoInspection.js +21 -6
- package/dist/server/createServer.js +7 -0
- package/dist/server/registerBrowserTools.js +61 -2
- package/dist/server/registerElectronTools.js +36 -0
- package/dist/server/registerInvestigationTools.js +11 -12
- package/dist/server/registerSessionTools.js +1 -0
- package/package.json +5 -1
- package/scripts/rea.mjs +2 -2
- package/skills/rea-analysis/SKILL.md +19 -9
package/README.md
CHANGED
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
|
|
11
11
|
[](https://www.npmjs.com/package/rea-agents)
|
|
12
12
|
[](https://github.com/morluto/rea/actions/workflows/ci.yml)
|
|
13
|
-
[](#78-tools-for-investigation)
|
|
14
14
|
[](https://nodejs.org/)
|
|
15
15
|
[](LICENSE)
|
|
16
16
|
|
|
17
|
-
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [
|
|
17
|
+
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [78 tools](#78-tools-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
|
|
18
18
|
|
|
19
19
|
<br />
|
|
20
20
|
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
See a feature in an app that you want in your own product? Give the app to your agent—even without its source code. With REA, the agent can investigate the feature, explain how it works, show its evidence, and build a version adapted to your stack and requirements.
|
|
32
32
|
|
|
33
|
-
REA gives agents one consistent way to investigate software. Today that includes deep native analysis through Hopper, complete function dossiers, reproducible Evidence v2 records,
|
|
33
|
+
REA gives agents one consistent way to investigate software. Today that includes deep native analysis through Hopper, complete function dossiers, reproducible Evidence v2 records, controlled process capture, passive website and Electron observation, and bounded JavaScript/source-map reconstruction. The longer-term toolkit extends the same agent workflow to APIs, protocols, mobile artifacts, firmware, richer runtime behavior, and differences between versions.
|
|
34
34
|
|
|
35
35
|
Reverse engineering normally makes the operator choose a tool, learn its API, move evidence between programs, and decide what to inspect next. REA gives that work to the agent through commands, skills, structured results, and repeatable investigation workflows.
|
|
36
36
|
|
|
@@ -294,16 +294,17 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
|
|
|
294
294
|
- Analyze Swift and Objective-C metadata without manually untangling every mangled symbol.
|
|
295
295
|
- Leave names, comments, and bookmarks in Hopper so human and agent analysis reinforce each other.
|
|
296
296
|
|
|
297
|
-
##
|
|
297
|
+
## 78 tools for investigation
|
|
298
298
|
|
|
299
|
-
| Tool family | Count | Examples
|
|
300
|
-
| ------------------------- | ----: |
|
|
301
|
-
| Native inspection | 33 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations
|
|
302
|
-
| Investigation workflows | 10 | `binary_overview`, `analyze_function`, `batch_decompile`, `trace_feature`, call graphs, Swift and Objective-C discovery
|
|
303
|
-
| Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing
|
|
304
|
-
| Artifact graph | 2 | deterministic directory, ZIP/APK/IPA, and ASAR inventory; explicitly selected extraction into an absent owned tree
|
|
305
|
-
| Browser observation |
|
|
306
|
-
|
|
|
299
|
+
| Tool family | Count | Examples |
|
|
300
|
+
| ------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
301
|
+
| Native inspection | 33 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations |
|
|
302
|
+
| Investigation workflows | 10 | `binary_overview`, `analyze_function`, `batch_decompile`, `trace_feature`, call graphs, Swift and Objective-C discovery |
|
|
303
|
+
| Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
|
|
304
|
+
| Artifact graph | 2 | deterministic directory, ZIP/APK/IPA, and ASAR inventory; explicitly selected extraction into an absent owned tree |
|
|
305
|
+
| Browser observation | 8 | exact-origin CDP capture, bundle and source-map analysis, WebMCP discovery, session timelines, capture diff, and visual evidence |
|
|
306
|
+
| Electron observation | 2 | canonical-root-confined file-page discovery and passive DOM, resource, and optionally approved script-source inspection |
|
|
307
|
+
| Workspace and observation | 18 | target lifecycle, Evidence v2 bundles, process/artifact/function comparison, evidence-linked residual-unknown lifecycle |
|
|
307
308
|
|
|
308
309
|
The public interface describes what the agent is trying to learn. Providers decide how to answer. macOS utilities handle common semantic inspection without launching Hopper; Hopper handles deeper native analysis; the process harness implements controlled behavioral capture.
|
|
309
310
|
|
|
@@ -312,7 +313,8 @@ The public interface describes what the agent is trying to learn. Providers deci
|
|
|
312
313
|
REA is already useful for native application investigation on macOS:
|
|
313
314
|
|
|
314
315
|
- Open Mach-O, ELF, PE, `.app`, ZIP, APK, IPA, ASAR, plist, JavaScript, source-map, and Hopper database targets.
|
|
315
|
-
- Attach to a user-owned Chrome-family browser over a configured loopback CDP endpoint
|
|
316
|
+
- Attach to a user-owned Chrome-family browser over a configured loopback CDP endpoint; capture exact-origin web structure, safe metadata, approved value-free payload shapes, bundle/source-map evidence, WebMCP declarations, user-action timelines, capture diffs, and explicitly approved screenshots without navigation or JavaScript evaluation.
|
|
317
|
+
- Inspect Electron `file://` renderer pages through a separate canonical-root permission boundary without invoking Electron APIs; script contents remain separately approved and byte bounded.
|
|
316
318
|
- Traverse content-addressed artifact graphs without extraction; on macOS, read-only DMG traversal additionally requires `native_mount_approved: true` and `REA_ARTIFACT_NATIVE_MOUNT_ENABLED=true`. Materialize only approved occurrences into absent output roots.
|
|
317
319
|
- Build bounded function dossiers with pseudocode, assembly, CFG edges, comments, calls, references, strings, and names.
|
|
318
320
|
- Search and trace features across symbols, strings, metadata, references, and call paths.
|
|
@@ -346,14 +348,14 @@ rea list-browser-targets http://127.0.0.1:9222 --approved --json
|
|
|
346
348
|
rea inspect-web-page http://127.0.0.1:9222 TARGET_ID --approved --json
|
|
347
349
|
```
|
|
348
350
|
|
|
349
|
-
|
|
351
|
+
All eight browser tools expose the same Evidence v2 contracts over CLI and MCP. Inspection is passive: REA does not evaluate page JavaScript, navigate, click, close the page, or close the browser. Query values, credentials, cookies, authorization headers, storage values, and raw JSON or WebSocket values are never retained. Separately approved captures can retain bounded redacted console primitives, value-free JSON/WebSocket shapes, script sources, accessibility text, or screenshot pixels. Existing activity before attach is explicitly unavailable. See [Website observation with CDP](docs/browser-observation.md) for browser startup, schemas, limits, and the threat model.
|
|
350
352
|
|
|
351
353
|
## Roadmap
|
|
352
354
|
|
|
353
355
|
REA is growing into a toolkit for understanding software across static artifacts and observed behavior. The next capability families are:
|
|
354
356
|
|
|
355
357
|
1. **Artifact decomposition** — DMG, ASAR, ZIP, packages, universal-binary slices, application resources, embedded frameworks, mobile packages, and artifact graphs.
|
|
356
|
-
2. **Web and Electron investigation** — extend the shipped passive CDP observation with approved interaction,
|
|
358
|
+
2. **Web and Electron investigation** — extend the shipped passive CDP, bundle, visual-diff, and file-page observation with approved interaction, controlled replay, Electron IPC observation, semantic UI differences, and deeper JavaScript reconstruction.
|
|
357
359
|
3. **Deterministic behavior harnesses** — stronger process-tree ownership, protocol fixtures, network policy, filesystem tracing, signals, reconnects, and cross-version comparison.
|
|
358
360
|
4. **JavaScript and source recovery** — bundle indexing, AST/module reconstruction, source-map discovery, historical-source matching, and CodeDB-backed cross-references.
|
|
359
361
|
5. **Runtime observation** — approval-gated LLDB, Frida, system logs, process and filesystem observers, and native API tracing.
|
|
@@ -7,7 +7,7 @@ import { EvidenceLedger } from "./EvidenceLedger.js";
|
|
|
7
7
|
import { UnknownRegistryError } from "../domain/errors.js";
|
|
8
8
|
import { enhancedToolNameSchema } from "../contracts/enhancedInputs.js";
|
|
9
9
|
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
|
10
|
-
import { snapshotMatchesTarget } from "../domain/analysisSnapshot.js";
|
|
10
|
+
import { snapshotMatchesTarget, snapshotTarget, } from "../domain/analysisSnapshot.js";
|
|
11
11
|
import { AnalysisSnapshotCache, isSnapshotCacheable, } from "./AnalysisSnapshotCache.js";
|
|
12
12
|
import { UNKNOWN_REGISTRY_PROVIDER, unknownEvidenceLinks, unknownMutationEvidence, } from "./UnknownEvidence.js";
|
|
13
13
|
import { parseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
|
|
@@ -194,7 +194,8 @@ export class BinarySession {
|
|
|
194
194
|
return err(new EvidenceIntegrityError("Analysis snapshot target does not match the requested binary"));
|
|
195
195
|
if (isAborted(options.signal))
|
|
196
196
|
return err(new AnalysisCancelledError("open_binary"));
|
|
197
|
-
if (this.#active?.target.path === parsed.value.path
|
|
197
|
+
if (this.#active?.target.path === parsed.value.path &&
|
|
198
|
+
snapshotMatchesTarget(snapshotTarget(this.#active.target), parsed.value)) {
|
|
198
199
|
if (options.snapshot !== undefined) {
|
|
199
200
|
const imported = this.importAnalysisSnapshot(options.snapshot);
|
|
200
201
|
if (!imported.ok)
|
|
@@ -401,8 +402,17 @@ export class BinarySession {
|
|
|
401
402
|
this.#emitAvailabilityChanged();
|
|
402
403
|
}
|
|
403
404
|
#emitAvailabilityChanged() {
|
|
404
|
-
for (const listener of this.#availabilityListeners)
|
|
405
|
-
|
|
405
|
+
for (const listener of this.#availabilityListeners) {
|
|
406
|
+
try {
|
|
407
|
+
const notification = listener();
|
|
408
|
+
if (notification !== undefined)
|
|
409
|
+
void notification.catch(() => undefined);
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// External observers are best-effort notifications. Contain only the
|
|
413
|
+
// callback failure so state transitions and other listeners continue.
|
|
414
|
+
}
|
|
415
|
+
}
|
|
406
416
|
}
|
|
407
417
|
#serialize(operation) {
|
|
408
418
|
const result = this.#transition.then(operation, operation);
|
|
@@ -2,15 +2,13 @@ import { createEvidence, } from "../domain/evidence.js";
|
|
|
2
2
|
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
3
3
|
/** Create Evidence v2 for a policy-scoped external browser observation. */
|
|
4
4
|
export const createBrowserEvidence = (operation, input, result, provider) => createEvidence(undefined, provider, {
|
|
5
|
-
predicateType: operation
|
|
6
|
-
? "rea.browser-target-list/v1"
|
|
7
|
-
: "rea.web-page-inspection/v1",
|
|
5
|
+
predicateType: browserPredicate(operation),
|
|
8
6
|
operation,
|
|
9
7
|
parameters: browserParameters(input),
|
|
10
8
|
result: jsonValueSchema.parse(result),
|
|
11
9
|
confidence: "observed",
|
|
12
10
|
authority: "external-service",
|
|
13
|
-
environment: "
|
|
11
|
+
environment: "browser" in result
|
|
14
12
|
? {
|
|
15
13
|
id: `${result.browser.product}@${result.browser.revision}`,
|
|
16
14
|
platform: process.platform,
|
|
@@ -20,16 +18,90 @@ export const createBrowserEvidence = (operation, input, result, provider) => cre
|
|
|
20
18
|
: null,
|
|
21
19
|
limitations: result.limitations,
|
|
22
20
|
});
|
|
23
|
-
const browserParameters = (input) =>
|
|
24
|
-
cdp_endpoint
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
const browserParameters = (input) => {
|
|
22
|
+
if (!("cdp_endpoint" in input)) {
|
|
23
|
+
if ("max_changes" in input)
|
|
24
|
+
return {
|
|
25
|
+
before_target_id: input.before.inspection.target.target_id,
|
|
26
|
+
before_capture_ended_at: input.before.inspection.capture_window.ended_at,
|
|
27
|
+
after_target_id: input.after.inspection.target.target_id,
|
|
28
|
+
after_capture_ended_at: input.after.inspection.capture_window.ended_at,
|
|
29
|
+
max_changes: input.max_changes,
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
before_artifact_uri: input.before.uri,
|
|
33
|
+
after_artifact_uri: input.after.uri,
|
|
34
|
+
channel_threshold: input.channel_threshold,
|
|
35
|
+
maximum_pixels: input.maximum_pixels,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const scope = {
|
|
39
|
+
cdp_endpoint: input.cdp_endpoint,
|
|
40
|
+
allowed_origins: input.allowed_origins,
|
|
41
|
+
};
|
|
42
|
+
if (!("target_id" in input))
|
|
43
|
+
return { ...scope, offset: input.offset, limit: input.limit };
|
|
44
|
+
if ("screenshot_approved" in input)
|
|
45
|
+
return {
|
|
46
|
+
...scope,
|
|
47
|
+
target_id: input.target_id,
|
|
48
|
+
maximum_image_bytes: input.maximum_image_bytes,
|
|
49
|
+
};
|
|
50
|
+
if ("max_tools" in input)
|
|
51
|
+
return {
|
|
52
|
+
...scope,
|
|
28
53
|
target_id: input.target_id,
|
|
29
54
|
observation_ms: input.observation_ms,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
55
|
+
max_tools: input.max_tools,
|
|
56
|
+
max_schema_bytes: input.max_schema_bytes,
|
|
57
|
+
max_schema_nodes: input.max_schema_nodes,
|
|
58
|
+
max_schema_depth: input.max_schema_depth,
|
|
59
|
+
};
|
|
60
|
+
if (!("include_accessibility_text" in input))
|
|
61
|
+
return {
|
|
62
|
+
...scope,
|
|
63
|
+
target_id: input.target_id,
|
|
64
|
+
observation_ms: input.observation_ms,
|
|
65
|
+
max_timeline_events: input.max_timeline_events,
|
|
66
|
+
};
|
|
67
|
+
return {
|
|
68
|
+
...scope,
|
|
69
|
+
target_id: input.target_id,
|
|
70
|
+
observation_ms: input.observation_ms,
|
|
71
|
+
include_accessibility_text: input.include_accessibility_text,
|
|
72
|
+
include_console_text: input.include_console_text,
|
|
73
|
+
include_json_body_shapes: input.include_json_body_shapes,
|
|
74
|
+
include_websocket_shapes: input.include_websocket_shapes,
|
|
75
|
+
include_script_sources: input.include_script_sources,
|
|
76
|
+
include_storage_keys: input.include_storage_keys,
|
|
77
|
+
limits: input.limits,
|
|
78
|
+
...(input.include_script_sources && "source_capture_approved" in input
|
|
79
|
+
? {
|
|
80
|
+
source_capture_approved: input.source_capture_approved,
|
|
81
|
+
fetch_source_maps: input.fetch_source_maps,
|
|
82
|
+
source_map_fetch_approved: input.source_map_fetch_approved,
|
|
83
|
+
analysis_limits: input.analysis_limits,
|
|
84
|
+
}
|
|
85
|
+
: {}),
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
const browserPredicate = (operation) => {
|
|
89
|
+
switch (operation) {
|
|
90
|
+
case "list_browser_targets":
|
|
91
|
+
return "rea.browser-target-list/v1";
|
|
92
|
+
case "inspect_web_page":
|
|
93
|
+
return "rea.web-page-inspection/v2";
|
|
94
|
+
case "analyze_web_bundle":
|
|
95
|
+
return "rea.web-bundle-analysis/v1";
|
|
96
|
+
case "observe_web_session":
|
|
97
|
+
return "rea.web-observation-session/v1";
|
|
98
|
+
case "discover_webmcp_tools":
|
|
99
|
+
return "rea.webmcp-discovery/v1";
|
|
100
|
+
case "compare_web_captures":
|
|
101
|
+
return "rea.web-capture-diff/v1";
|
|
102
|
+
case "capture_web_screenshot":
|
|
103
|
+
return "rea.web-screenshot/v1";
|
|
104
|
+
case "compare_web_screenshots":
|
|
105
|
+
return "rea.web-screenshot-diff/v1";
|
|
106
|
+
}
|
|
107
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isLiteralLoopbackHostname, } from "../domain/browserObservation.js";
|
|
1
2
|
import { AnalysisCapabilityUnavailableError, AnalysisProtocolError, PermissionRequiredError, } from "../domain/errors.js";
|
|
2
3
|
import { err, ok } from "../domain/result.js";
|
|
3
4
|
import { createBrowserEvidence } from "./BrowserEvidence.js";
|
|
@@ -21,6 +22,66 @@ export const inspectWebPage = async (browser, authority, input, options = {}) =>
|
|
|
21
22
|
? ok(createBrowserEvidence("inspect_web_page", input, result.value, ready.value.identity()))
|
|
22
23
|
: result;
|
|
23
24
|
};
|
|
25
|
+
/** Capture approved sources and derive bounded static web-bundle evidence. */
|
|
26
|
+
export const analyzeWebBundle = async (browser, authority, input, options = {}) => {
|
|
27
|
+
const ready = await prepare(browser, authority, input, "analyze_web_bundle");
|
|
28
|
+
if (!ready.ok)
|
|
29
|
+
return ready;
|
|
30
|
+
const analyzed = await ready.value.analyzeBundle(input, options);
|
|
31
|
+
if (!analyzed.ok)
|
|
32
|
+
return analyzed;
|
|
33
|
+
return ok(createBrowserEvidence("analyze_web_bundle", input, analyzed.value, ready.value.identity()));
|
|
34
|
+
};
|
|
35
|
+
/** Observe navigation caused by external user actions during one armed window. */
|
|
36
|
+
export const observeWebSession = async (browser, authority, input, options = {}) => {
|
|
37
|
+
const ready = await prepare(browser, authority, input, "observe_web_session");
|
|
38
|
+
if (!ready.ok)
|
|
39
|
+
return ready;
|
|
40
|
+
const observed = await ready.value.observeSession(input, options);
|
|
41
|
+
return observed.ok
|
|
42
|
+
? ok(createBrowserEvidence("observe_web_session", input, observed.value, ready.value.identity()))
|
|
43
|
+
: observed;
|
|
44
|
+
};
|
|
45
|
+
/** Discover WebMCP declarations without exposing an invocation surface. */
|
|
46
|
+
export const discoverWebMcpTools = async (browser, authority, input, options = {}) => {
|
|
47
|
+
const ready = await prepare(browser, authority, input, "discover_webmcp_tools");
|
|
48
|
+
if (!ready.ok)
|
|
49
|
+
return ready;
|
|
50
|
+
const discovered = await ready.value.discoverWebMcpTools(input, options);
|
|
51
|
+
return discovered.ok
|
|
52
|
+
? ok(createBrowserEvidence("discover_webmcp_tools", input, discovered.value, ready.value.identity()))
|
|
53
|
+
: discovered;
|
|
54
|
+
};
|
|
55
|
+
/** Compare two already-normalized web captures without external access. */
|
|
56
|
+
export const compareWebCaptureEvidence = async (browser, input) => {
|
|
57
|
+
const ready = requireBrowser(browser, "compare_web_captures");
|
|
58
|
+
if (!ready.ok)
|
|
59
|
+
return ready;
|
|
60
|
+
const compared = await ready.value.compareCaptures(input);
|
|
61
|
+
return compared.ok
|
|
62
|
+
? ok(createBrowserEvidence("compare_web_captures", input, compared.value, ready.value.identity()))
|
|
63
|
+
: compared;
|
|
64
|
+
};
|
|
65
|
+
/** Capture one explicitly approved visible-viewport screenshot. */
|
|
66
|
+
export const captureWebScreenshot = async (browser, authority, input, options = {}) => {
|
|
67
|
+
const ready = await prepare(browser, authority, input, "capture_web_screenshot");
|
|
68
|
+
if (!ready.ok)
|
|
69
|
+
return ready;
|
|
70
|
+
const captured = await ready.value.captureScreenshot(input, options);
|
|
71
|
+
return captured.ok
|
|
72
|
+
? ok(createBrowserEvidence("capture_web_screenshot", input, captured.value, ready.value.identity()))
|
|
73
|
+
: captured;
|
|
74
|
+
};
|
|
75
|
+
/** Compare two self-verifying PNG artifacts without external access. */
|
|
76
|
+
export const compareWebScreenshotEvidence = async (browser, input) => {
|
|
77
|
+
const ready = requireBrowser(browser, "compare_web_screenshots");
|
|
78
|
+
if (!ready.ok)
|
|
79
|
+
return ready;
|
|
80
|
+
const compared = await ready.value.compareScreenshots(input);
|
|
81
|
+
return compared.ok
|
|
82
|
+
? ok(createBrowserEvidence("compare_web_screenshots", input, compared.value, ready.value.identity()))
|
|
83
|
+
: compared;
|
|
84
|
+
};
|
|
24
85
|
const prepare = async (browser, authority, input, operation) => {
|
|
25
86
|
if (authority === undefined)
|
|
26
87
|
return err(new AnalysisCapabilityUnavailableError("rea-cdp-browser", operation, "browser observation permission policy is not configured"));
|
|
@@ -44,9 +105,12 @@ const prepare = async (browser, authority, input, operation) => {
|
|
|
44
105
|
? err(new AnalysisCapabilityUnavailableError("rea-cdp-browser", operation, "browser observation provider is not configured"))
|
|
45
106
|
: ok(browser);
|
|
46
107
|
};
|
|
108
|
+
const requireBrowser = (browser, operation) => browser === undefined
|
|
109
|
+
? err(new AnalysisCapabilityUnavailableError("rea-cdp-browser", operation, "browser comparison provider is not configured"))
|
|
110
|
+
: ok(browser);
|
|
47
111
|
const browserNetworkScope = (allowedOrigins) => allowedOrigins.every((origin) => {
|
|
48
112
|
const hostname = new URL(origin).hostname;
|
|
49
|
-
return hostname
|
|
113
|
+
return isLiteralLoopbackHostname(hostname);
|
|
50
114
|
})
|
|
51
115
|
? "loopback"
|
|
52
116
|
: "external";
|
|
@@ -71,6 +71,13 @@ const availabilityFor = (name, kind, targetOpen, targetKind, descriptors, policy
|
|
|
71
71
|
reason: "policy_disabled",
|
|
72
72
|
remediation: "Enable browser observation and configure exact CDP endpoint and page origins.",
|
|
73
73
|
};
|
|
74
|
+
if (kind === "electron-provider")
|
|
75
|
+
return policy.electronObservationEnabled === true
|
|
76
|
+
? { reason: "available", remediation: null }
|
|
77
|
+
: {
|
|
78
|
+
reason: "policy_disabled",
|
|
79
|
+
remediation: "Enable Electron observation and configure a loopback CDP endpoint and canonical file roots.",
|
|
80
|
+
};
|
|
74
81
|
if (kind === "session")
|
|
75
82
|
return { reason: "available", remediation: null };
|
|
76
83
|
if (!targetOpen)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
+
const systemFileSystem = { lstat, realpath };
|
|
3
|
+
/**
|
|
4
|
+
* Resolve an existing, user-owned config symlink to one stable transaction
|
|
5
|
+
* path. Missing regular paths remain eligible for first-run creation.
|
|
6
|
+
*/
|
|
7
|
+
export const resolveClientConfigTransactionPath = async (requestedPath, fileSystem = systemFileSystem) => {
|
|
8
|
+
let requestedStats;
|
|
9
|
+
try {
|
|
10
|
+
requestedStats = await fileSystem.lstat(requestedPath);
|
|
11
|
+
}
|
|
12
|
+
catch (cause) {
|
|
13
|
+
return isMissing(cause) ? requestedPath : undefined;
|
|
14
|
+
}
|
|
15
|
+
if (!requestedStats.isSymbolicLink())
|
|
16
|
+
return requestedPath;
|
|
17
|
+
const currentUid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
18
|
+
if (currentUid === undefined ||
|
|
19
|
+
requestedStats.uid !== currentUid ||
|
|
20
|
+
fileSystem.realpath === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
try {
|
|
23
|
+
const canonicalPath = await fileSystem.realpath(requestedPath);
|
|
24
|
+
const targetStats = await fileSystem.lstat(canonicalPath);
|
|
25
|
+
return targetStats.isFile?.() === true && targetStats.uid === currentUid
|
|
26
|
+
? canonicalPath
|
|
27
|
+
: undefined;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const isMissing = (cause) => cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
|
@@ -7,6 +7,14 @@ import { paginateArtifactInventory, } from "./ArtifactInventory.js";
|
|
|
7
7
|
import { scanAuthorizedArtifactInventory } from "./AuthorizedArtifactInventory.js";
|
|
8
8
|
import { ARTIFACT_GRAPH_PROVIDER } from "./InvestigationProviders.js";
|
|
9
9
|
export const AUTOMATIC_RUN_LIMITATION = "Automatic cross-version runs compare shipped artifact structure only; they do not execute either version.";
|
|
10
|
+
/** Start both independent version scans while preserving left/right identity. */
|
|
11
|
+
export const scanVersionInventories = async (options, scanner = scanAuthorizedArtifactInventory) => {
|
|
12
|
+
const [left, right] = await Promise.all([
|
|
13
|
+
scanner(options.leftPath, options.inputRoots, options.limits, options.signal, options.integrity),
|
|
14
|
+
scanner(options.rightPath, options.inputRoots, options.limits, options.signal, options.integrity),
|
|
15
|
+
]);
|
|
16
|
+
return { left, right };
|
|
17
|
+
};
|
|
10
18
|
/** Authorize and scan both inputs once under the same bounded traversal policy. */
|
|
11
19
|
export const scanVersions = async (input, inputRoots, signal, integrityContinueEnabled = false) => {
|
|
12
20
|
try {
|
|
@@ -19,9 +27,14 @@ export const scanVersions = async (input, inputRoots, signal, integrityContinueE
|
|
|
19
27
|
enabled: integrityContinueEnabled,
|
|
20
28
|
maxMismatches: input.max_integrity_mismatches,
|
|
21
29
|
};
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
return ok(await scanVersionInventories({
|
|
31
|
+
leftPath: input.left_path,
|
|
32
|
+
rightPath: input.right_path,
|
|
33
|
+
inputRoots,
|
|
34
|
+
limits,
|
|
35
|
+
...(signal === undefined ? {} : { signal }),
|
|
36
|
+
integrity,
|
|
37
|
+
}));
|
|
25
38
|
}
|
|
26
39
|
catch (cause) {
|
|
27
40
|
if (cause instanceof ArtifactReaderFailure)
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { findChangedBehavior } from "../domain/changedBehavior.js";
|
|
3
|
-
import { createEvidence } from "../domain/evidence.js";
|
|
1
|
+
import {} from "../domain/evidence.js";
|
|
4
2
|
import { createEvidenceBundle, } from "../domain/evidenceBundle.js";
|
|
5
3
|
import { AnalysisCancelledError, AnalysisInputError, EvidenceIntegrityError, InvestigationWorkspaceError, } from "../domain/errors.js";
|
|
6
|
-
import { createLegacyInvestigationRunIdentity, createInvestigationRunIdentity, createInvestigationWorkspace, crossVersionInvestigationInputSchema, investigationRunSchema,
|
|
7
|
-
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
4
|
+
import { createLegacyInvestigationRunIdentity, createInvestigationRunIdentity, createInvestigationWorkspace, crossVersionInvestigationInputSchema, investigationRunSchema, reviseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
|
|
8
5
|
import { err, ok } from "../domain/result.js";
|
|
9
|
-
import {
|
|
6
|
+
import { createInventoryEvidencePages, runLimitations, scanVersions, targetFor, } from "./CrossVersionInventory.js";
|
|
10
7
|
import { readInvestigationWorkspace, writeInvestigationWorkspace, } from "./InvestigationWorkspaceStore.js";
|
|
11
|
-
import {
|
|
8
|
+
import { createArtifactComparisonEvidence, createChangedBehaviorEvidence, } from "./CrossVersionInvestigationEvidence.js";
|
|
9
|
+
import { selectCompletedInvestigationReplay } from "./CrossVersionInvestigationReplay.js";
|
|
12
10
|
/** Run or resume a deterministic cross-version artifact investigation. */
|
|
13
11
|
export const runCrossVersionInvestigation = async (input, policy, execution) => {
|
|
14
12
|
const parsed = crossVersionInvestigationInputSchema.safeParse(input);
|
|
@@ -24,6 +22,17 @@ export const runCrossVersionInvestigation = async (input, policy, execution) =>
|
|
|
24
22
|
return preflight;
|
|
25
23
|
if (isCancelled(execution.signal))
|
|
26
24
|
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
25
|
+
if (parsed.data.replay_run_id !== undefined) {
|
|
26
|
+
const replay = selectCompletedInvestigationReplay(initial.value, parsed.data);
|
|
27
|
+
if (!replay.ok)
|
|
28
|
+
return replay;
|
|
29
|
+
return completeOutcome(replay.value.workspace, replay.value.run, true, execution.session);
|
|
30
|
+
}
|
|
31
|
+
if (execution.authorizeInputRead !== undefined) {
|
|
32
|
+
const authorized = await execution.authorizeInputRead();
|
|
33
|
+
if (!authorized.ok)
|
|
34
|
+
return authorized;
|
|
35
|
+
}
|
|
27
36
|
await execution.progress?.report({
|
|
28
37
|
phase: "scan_versions",
|
|
29
38
|
completed: 0,
|
|
@@ -198,31 +207,11 @@ const ensureComparisonCheckpoint = async (input, state, policy) => {
|
|
|
198
207
|
});
|
|
199
208
|
return saved.ok ? ok({ workspace: saved.value, run }) : saved;
|
|
200
209
|
};
|
|
201
|
-
const createArtifactComparisonEvidence = (inventory, limit) => {
|
|
202
|
-
const comparison = compareArtifacts(inventory.left, inventory.right, 0, limit);
|
|
203
|
-
const leftIds = evidenceIds(inventory.left);
|
|
204
|
-
const rightIds = evidenceIds(inventory.right);
|
|
205
|
-
return createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, {
|
|
206
|
-
predicateType: "rea.artifact-comparison/v1",
|
|
207
|
-
operation: "compare_artifacts",
|
|
208
|
-
parameters: {
|
|
209
|
-
left_evidence_ids: leftIds,
|
|
210
|
-
right_evidence_ids: rightIds,
|
|
211
|
-
offset: 0,
|
|
212
|
-
limit,
|
|
213
|
-
},
|
|
214
|
-
result: jsonValueSchema.parse(comparison),
|
|
215
|
-
confidence: "derived",
|
|
216
|
-
authority: "analyst-inference",
|
|
217
|
-
limitations: comparison.limitations,
|
|
218
|
-
evidenceLinks: [...leftIds, ...rightIds],
|
|
219
|
-
});
|
|
220
|
-
};
|
|
221
210
|
const finalizeInvestigation = async (input, state, policy, session) => {
|
|
222
211
|
const comparison = comparisonEvidenceFor(state.workspace, state.run);
|
|
223
212
|
if (!comparison.ok)
|
|
224
213
|
return comparison;
|
|
225
|
-
const evidence = createChangedBehaviorEvidence(state, comparison.value);
|
|
214
|
+
const evidence = createChangedBehaviorEvidence(state.workspace, state.run, comparison.value);
|
|
226
215
|
const run = investigationRunSchema.parse({
|
|
227
216
|
...state.run,
|
|
228
217
|
status: "complete",
|
|
@@ -244,40 +233,6 @@ const finalizeInvestigation = async (input, state, policy, session) => {
|
|
|
244
233
|
});
|
|
245
234
|
return saved.ok ? completeOutcome(saved.value, run, false, session) : saved;
|
|
246
235
|
};
|
|
247
|
-
const createChangedBehaviorEvidence = (state, comparison) => {
|
|
248
|
-
const limit = Math.min(state.run.options.change_limit, 100);
|
|
249
|
-
const changed = findChangedBehavior([comparison], 0, limit);
|
|
250
|
-
const summary = investigationRunSummarySchema.parse({
|
|
251
|
-
schema_version: 1,
|
|
252
|
-
workspace_id: state.workspace.workspace_id,
|
|
253
|
-
run_id: state.run.run_id,
|
|
254
|
-
left_manifest_id: state.run.left.manifest_id,
|
|
255
|
-
right_manifest_id: state.run.right.manifest_id,
|
|
256
|
-
inventory_evidence_count: state.run.left_inventory_evidence_ids.length +
|
|
257
|
-
state.run.right_inventory_evidence_ids.length,
|
|
258
|
-
comparison_evidence_id: comparison.evidence_id,
|
|
259
|
-
limitations: state.run.limitations,
|
|
260
|
-
});
|
|
261
|
-
return createEvidence(undefined, CHANGED_BEHAVIOR_PROVIDER, {
|
|
262
|
-
predicateType: "rea.changed-behavior/v1",
|
|
263
|
-
operation: "find_changed_behavior",
|
|
264
|
-
parameters: {
|
|
265
|
-
workspace_id: state.workspace.workspace_id,
|
|
266
|
-
run_id: state.run.run_id,
|
|
267
|
-
comparison_evidence_ids: [comparison.evidence_id],
|
|
268
|
-
offset: 0,
|
|
269
|
-
limit,
|
|
270
|
-
},
|
|
271
|
-
result: jsonValueSchema.parse({
|
|
272
|
-
...changed,
|
|
273
|
-
investigation_run: summary,
|
|
274
|
-
}),
|
|
275
|
-
confidence: "derived",
|
|
276
|
-
authority: "analyst-inference",
|
|
277
|
-
limitations: [...changed.limitations, AUTOMATIC_RUN_LIMITATION],
|
|
278
|
-
evidenceLinks: changed.evidence_links,
|
|
279
|
-
});
|
|
280
|
-
};
|
|
281
236
|
const comparisonEvidenceFor = (workspace, run) => {
|
|
282
237
|
const evidence = workspace.bundle.records.find(({ evidence_id: id }) => id === run.comparison_evidence_id);
|
|
283
238
|
return evidence === undefined
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { compareArtifacts } from "../domain/artifactComparison.js";
|
|
2
|
+
import { findChangedBehavior } from "../domain/changedBehavior.js";
|
|
3
|
+
import { createEvidence } from "../domain/evidence.js";
|
|
4
|
+
import { investigationRunSummarySchema, } from "../domain/investigationWorkspace.js";
|
|
5
|
+
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
6
|
+
import { AUTOMATIC_RUN_LIMITATION } from "./CrossVersionInventory.js";
|
|
7
|
+
import { ARTIFACT_COMPARISON_PROVIDER, CHANGED_BEHAVIOR_PROVIDER, } from "./InvestigationProviders.js";
|
|
8
|
+
/** Derive deterministic artifact-comparison Evidence from inventory pages. */
|
|
9
|
+
export const createArtifactComparisonEvidence = (inventory, limit) => {
|
|
10
|
+
const comparison = compareArtifacts(inventory.left, inventory.right, 0, limit);
|
|
11
|
+
const leftIds = evidenceIds(inventory.left);
|
|
12
|
+
const rightIds = evidenceIds(inventory.right);
|
|
13
|
+
return createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, {
|
|
14
|
+
predicateType: "rea.artifact-comparison/v1",
|
|
15
|
+
operation: "compare_artifacts",
|
|
16
|
+
parameters: {
|
|
17
|
+
left_evidence_ids: leftIds,
|
|
18
|
+
right_evidence_ids: rightIds,
|
|
19
|
+
offset: 0,
|
|
20
|
+
limit,
|
|
21
|
+
},
|
|
22
|
+
result: jsonValueSchema.parse(comparison),
|
|
23
|
+
confidence: "derived",
|
|
24
|
+
authority: "analyst-inference",
|
|
25
|
+
limitations: comparison.limitations,
|
|
26
|
+
evidenceLinks: [...leftIds, ...rightIds],
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
/** Derive the deterministic final report for one comparison checkpoint. */
|
|
30
|
+
export const createChangedBehaviorEvidence = (workspace, run, comparison) => {
|
|
31
|
+
const limit = Math.min(run.options.change_limit, 100);
|
|
32
|
+
const changed = findChangedBehavior([comparison], 0, limit);
|
|
33
|
+
const summary = investigationRunSummarySchema.parse({
|
|
34
|
+
schema_version: 1,
|
|
35
|
+
workspace_id: workspace.workspace_id,
|
|
36
|
+
run_id: run.run_id,
|
|
37
|
+
left_manifest_id: run.left.manifest_id,
|
|
38
|
+
right_manifest_id: run.right.manifest_id,
|
|
39
|
+
inventory_evidence_count: run.left_inventory_evidence_ids.length +
|
|
40
|
+
run.right_inventory_evidence_ids.length,
|
|
41
|
+
comparison_evidence_id: comparison.evidence_id,
|
|
42
|
+
limitations: run.limitations,
|
|
43
|
+
});
|
|
44
|
+
return createEvidence(undefined, CHANGED_BEHAVIOR_PROVIDER, {
|
|
45
|
+
predicateType: "rea.changed-behavior/v1",
|
|
46
|
+
operation: "find_changed_behavior",
|
|
47
|
+
parameters: {
|
|
48
|
+
workspace_id: workspace.workspace_id,
|
|
49
|
+
run_id: run.run_id,
|
|
50
|
+
comparison_evidence_ids: [comparison.evidence_id],
|
|
51
|
+
offset: 0,
|
|
52
|
+
limit,
|
|
53
|
+
},
|
|
54
|
+
result: jsonValueSchema.parse({
|
|
55
|
+
...changed,
|
|
56
|
+
investigation_run: summary,
|
|
57
|
+
}),
|
|
58
|
+
confidence: "derived",
|
|
59
|
+
authority: "analyst-inference",
|
|
60
|
+
limitations: [...changed.limitations, AUTOMATIC_RUN_LIMITATION],
|
|
61
|
+
evidenceLinks: changed.evidence_links,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const evidenceIds = (records) => records.map(({ evidence_id: id }) => id);
|