rea-agents 1.3.0 → 1.5.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 +55 -12
- package/bridge/hopper_bridge.py +12 -3
- package/dist/application/AnalysisSnapshotCache.js +8 -6
- package/dist/application/ArtifactGraphConstruction.js +12 -3
- package/dist/application/ArtifactInventory.js +100 -27
- package/dist/application/AuthorizedArtifactInventory.js +4 -3
- package/dist/application/BinarySession.js +92 -4
- package/dist/application/BoundedJsonFiles.js +6 -29
- package/dist/application/BrowserEvidence.js +35 -0
- package/dist/application/BrowserObservationPort.js +1 -0
- package/dist/application/BrowserObservationService.js +52 -0
- package/dist/application/CapabilityInventory.js +134 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/ConfiguredRoots.js +19 -0
- package/dist/application/CrossVersionInventory.js +19 -8
- package/dist/application/CrossVersionInvestigation.js +74 -5
- package/dist/application/DeferredFileAuthorization.js +61 -0
- package/dist/application/DirectAnalysis.js +117 -9
- package/dist/application/Doctor.js +74 -2
- package/dist/application/EvidenceLedger.js +6 -5
- package/dist/application/InvestigationWorkspaceStore.js +2 -26
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureAuthority.js +3 -2
- package/dist/application/ProcessCaptureError.js +13 -0
- package/dist/application/ProcessCaptureLifecycle.js +21 -9
- package/dist/application/ProcessCheckpoints.js +1 -1
- package/dist/application/ProcessCli.js +40 -15
- package/dist/application/ProcessHarness.js +10 -20
- package/dist/application/ProcessNormalization.js +17 -2
- package/dist/application/ProcessOwnership.js +4 -3
- package/dist/application/ProcessSampling.js +52 -17
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +213 -0
- package/dist/application/ReferenceSourceImportPolicy.js +3 -2
- package/dist/application/Setup.js +2 -41
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/TerminalRenderer.js +1 -1
- package/dist/application/Uninstall.js +1 -1
- package/dist/application/Upgrade.js +2 -0
- package/dist/application/runtime.js +1 -1
- package/dist/artifacts/ArtifactProvider.js +13 -3
- package/dist/artifacts/MachOSliceArtifactReader.js +1 -1
- package/dist/artifacts/NativeDmgArtifactReader.js +1 -1
- package/dist/artifacts/SafeOutputTree.js +3 -3
- package/dist/browser/CdpBrowserProvider.js +158 -0
- package/dist/browser/CdpCaptureDocuments.js +140 -0
- package/dist/browser/CdpCaptureEvents.js +248 -0
- package/dist/browser/CdpCaptureStorage.js +53 -0
- package/dist/browser/CdpCaptureValues.js +70 -0
- package/dist/browser/CdpConnection.js +218 -0
- package/dist/browser/CdpEndpoint.js +114 -0
- package/dist/browser/CdpOptionalCommand.js +15 -0
- package/dist/browser/CdpPageCapture.js +273 -0
- package/dist/catalogIdentity.js +123 -0
- package/dist/cli.js +48 -3
- package/dist/cliBrowserCommands.js +136 -0
- package/dist/cliEvidenceCommands.js +42 -1
- package/dist/cliInvestigationCommands.js +42 -6
- package/dist/cliLogging.js +20 -3
- package/dist/cliPolicyCommands.js +118 -0
- package/dist/cliProcessCommands.js +4 -14
- package/dist/config.js +107 -3
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/browserToolContracts.js +80 -0
- package/dist/contracts/errorSchemas.js +100 -0
- package/dist/contracts/toolContracts.js +13 -6
- package/dist/contracts/toolOutputSchemas.js +62 -30
- package/dist/domain/artifactComparison.js +38 -11
- package/dist/domain/artifactGraph.js +25 -1
- package/dist/domain/artifactInventoryEvidence.js +18 -0
- package/dist/domain/browserObservation.js +361 -0
- package/dist/domain/changedBehavior.js +9 -18
- package/dist/domain/errors.js +257 -3
- package/dist/domain/investigationWorkspace.js +131 -4
- package/dist/domain/jsonLimits.js +26 -0
- package/dist/domain/permissionPolicy.js +161 -0
- package/dist/domain/processCapture.js +3 -1
- package/dist/domain/processComparison.js +23 -34
- package/dist/domain/reconstructionVerification.js +3 -16
- package/dist/domain/reconstructionVerificationSchemas.js +3 -0
- package/dist/domain/staticRuntimeCorrelation.js +7 -1
- package/dist/domain/symbolAnalysis.js +1 -1
- package/dist/generatedPackageMetadata.js +9 -0
- package/dist/hopper/BridgeLauncher.js +7 -3
- package/dist/hopper/HopperClient.js +32 -5
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +101 -7
- package/dist/native/CommandRunner.js +11 -4
- package/dist/server/createServer.js +59 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBrowserTools.js +36 -0
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +18 -1
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +58 -1
- package/dist/server/registerFunctionComparisonTool.js +6 -2
- package/dist/server/registerInvestigationTools.js +78 -10
- package/dist/server/registerOfficialTools.js +19 -1
- package/dist/server/registerProcessComparisonTool.js +9 -3
- package/dist/server/registerSessionStatusTool.js +44 -3
- package/dist/server/registerSessionTools.js +144 -9
- package/dist/server/runDerivedOperation.js +43 -0
- package/dist/server/toolResult.js +27 -3
- package/dist/serverIdentity.js +70 -0
- package/package.json +7 -4
- package/scripts/rea.mjs +1 -1
- package/skills/rea-analysis/SKILL.md +18 -3
|
@@ -10,6 +10,7 @@ import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
|
|
10
10
|
import { snapshotMatchesTarget } from "../domain/analysisSnapshot.js";
|
|
11
11
|
import { AnalysisSnapshotCache, isSnapshotCacheable, } from "./AnalysisSnapshotCache.js";
|
|
12
12
|
import { UNKNOWN_REGISTRY_PROVIDER, unknownEvidenceLinks, unknownMutationEvidence, } from "./UnknownEvidence.js";
|
|
13
|
+
import { parseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
|
|
13
14
|
const OFFICIAL_OPERATIONS = new Set(OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name));
|
|
14
15
|
/**
|
|
15
16
|
* Owns the single active target shared by CLI and MCP adapters.
|
|
@@ -32,6 +33,9 @@ export class BinarySession {
|
|
|
32
33
|
maxBytes: 64 * 1024 * 1024,
|
|
33
34
|
});
|
|
34
35
|
#snapshot = new AnalysisSnapshotCache();
|
|
36
|
+
#investigationWorkspaces = new Map();
|
|
37
|
+
#runtimeAvailability = new Map();
|
|
38
|
+
#availabilityListeners = new Set();
|
|
35
39
|
#snapshotsEnabled;
|
|
36
40
|
#snapshotInvalidated = false;
|
|
37
41
|
constructor(provider, options = {}) {
|
|
@@ -57,7 +61,7 @@ export class BinarySession {
|
|
|
57
61
|
if (operation !== undefined) {
|
|
58
62
|
const exact = this.#capabilities?.get(operation)?.provider;
|
|
59
63
|
if (exact !== undefined)
|
|
60
|
-
return exact;
|
|
64
|
+
return structuredClone(exact);
|
|
61
65
|
if (enhancedToolNameSchema.safeParse(operation).success) {
|
|
62
66
|
const providers = new Map();
|
|
63
67
|
for (const descriptor of this.#capabilities?.values() ?? [])
|
|
@@ -67,11 +71,16 @@ export class BinarySession {
|
|
|
67
71
|
if (providers.size === 1) {
|
|
68
72
|
const provider = providers.values().next().value;
|
|
69
73
|
if (provider !== undefined)
|
|
70
|
-
return provider;
|
|
74
|
+
return structuredClone(provider);
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
|
-
return this.#providerIdentity;
|
|
78
|
+
return structuredClone(this.#providerIdentity);
|
|
79
|
+
}
|
|
80
|
+
/** Observe runtime provider-health changes that affect discovery metadata. */
|
|
81
|
+
onAvailabilityChanged(listener) {
|
|
82
|
+
this.#availabilityListeners.add(listener);
|
|
83
|
+
return () => this.#availabilityListeners.delete(listener);
|
|
75
84
|
}
|
|
76
85
|
/** Add one successful public observation to the session ledger. */
|
|
77
86
|
recordEvidence(evidence) {
|
|
@@ -110,6 +119,27 @@ export class BinarySession {
|
|
|
110
119
|
return err(new EvidenceIntegrityError("Analysis snapshots are unavailable with custom Hopper loader arguments"));
|
|
111
120
|
return this.#snapshot.import(snapshot, this.#active?.target, (bundle) => this.#evidence.import(bundle));
|
|
112
121
|
}
|
|
122
|
+
/** Retain one validated immutable workspace revision for session resources. */
|
|
123
|
+
retainInvestigationWorkspace(workspace) {
|
|
124
|
+
const parsed = parseInvestigationWorkspace(workspace);
|
|
125
|
+
const key = `${parsed.workspace_id}:${String(parsed.revision)}`;
|
|
126
|
+
if (this.#investigationWorkspaces.has(key))
|
|
127
|
+
return "duplicate";
|
|
128
|
+
this.#investigationWorkspaces.set(key, parsed);
|
|
129
|
+
return "added";
|
|
130
|
+
}
|
|
131
|
+
/** Read one session-retained immutable workspace revision. */
|
|
132
|
+
investigationWorkspace(workspaceId, revision) {
|
|
133
|
+
const workspace = this.#investigationWorkspaces.get(`${workspaceId}:${String(revision)}`);
|
|
134
|
+
return workspace === undefined ? undefined : structuredClone(workspace);
|
|
135
|
+
}
|
|
136
|
+
/** List retained workspace revisions in canonical identity order. */
|
|
137
|
+
investigationWorkspaces() {
|
|
138
|
+
return [...this.#investigationWorkspaces.values()]
|
|
139
|
+
.sort((left, right) => left.workspace_id.localeCompare(right.workspace_id) ||
|
|
140
|
+
left.revision - right.revision)
|
|
141
|
+
.map((workspace) => structuredClone(workspace));
|
|
142
|
+
}
|
|
113
143
|
/** Create an approved residual unknown and immutable mutation evidence. */
|
|
114
144
|
recordUnknown(input) {
|
|
115
145
|
const evidence = unknownMutationEvidence(this.#active?.target, input);
|
|
@@ -189,6 +219,7 @@ export class BinarySession {
|
|
|
189
219
|
return err(new AnalysisCancelledError("open_binary"));
|
|
190
220
|
}
|
|
191
221
|
this.#active = { target: parsed.value, client };
|
|
222
|
+
this.#clearRuntimeAvailability();
|
|
192
223
|
this.#snapshot.select(parsed.value);
|
|
193
224
|
if (options.snapshot !== undefined) {
|
|
194
225
|
const imported = this.importAnalysisSnapshot(options.snapshot);
|
|
@@ -213,6 +244,7 @@ export class BinarySession {
|
|
|
213
244
|
this.#evidence.clear();
|
|
214
245
|
this.#snapshot.clear();
|
|
215
246
|
this.#snapshotInvalidated = false;
|
|
247
|
+
this.#clearRuntimeAvailability();
|
|
216
248
|
return ok(null);
|
|
217
249
|
});
|
|
218
250
|
}
|
|
@@ -237,6 +269,10 @@ export class BinarySession {
|
|
|
237
269
|
? []
|
|
238
270
|
: [...this.#capabilities.values()]
|
|
239
271
|
.sort((left, right) => left.operation.localeCompare(right.operation))
|
|
272
|
+
.map((descriptor) => ({
|
|
273
|
+
...descriptor,
|
|
274
|
+
...this.#runtimeAvailability.get(descriptor.operation),
|
|
275
|
+
}))
|
|
240
276
|
.map((descriptor) => ({
|
|
241
277
|
operation: descriptor.operation,
|
|
242
278
|
available: descriptor.available,
|
|
@@ -277,7 +313,9 @@ export class BinarySession {
|
|
|
277
313
|
}
|
|
278
314
|
/** Return the immutable artifact identity captured before Hopper launched. */
|
|
279
315
|
activeTarget() {
|
|
280
|
-
return this.#active
|
|
316
|
+
return this.#active === undefined
|
|
317
|
+
? undefined
|
|
318
|
+
: structuredClone(this.#active.target);
|
|
281
319
|
}
|
|
282
320
|
/**
|
|
283
321
|
* Invoke a Hopper tool against the active target.
|
|
@@ -304,6 +342,7 @@ export class BinarySession {
|
|
|
304
342
|
this.#calls.add(call);
|
|
305
343
|
try {
|
|
306
344
|
const result = await call;
|
|
345
|
+
this.#observeRuntimeAvailability(name, result);
|
|
307
346
|
if (result.ok && cacheable)
|
|
308
347
|
this.#snapshot.record(active.target, name, arguments_, result.value);
|
|
309
348
|
else if (result.ok && capability?.effects.mutatesArtifact === true) {
|
|
@@ -316,6 +355,55 @@ export class BinarySession {
|
|
|
316
355
|
this.#calls.delete(call);
|
|
317
356
|
}
|
|
318
357
|
}
|
|
358
|
+
#observeRuntimeAvailability(operation, result) {
|
|
359
|
+
if (result.ok) {
|
|
360
|
+
if (this.#setRuntimeAvailability(operation, true, null))
|
|
361
|
+
this.#emitAvailabilityChanged();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (result.error._tag === "AnalysisCapabilityUnavailableError") {
|
|
365
|
+
if (this.#setRuntimeAvailability(operation, false, result.error.message))
|
|
366
|
+
this.#emitAvailabilityChanged();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if ([
|
|
370
|
+
"ProviderAdapterError",
|
|
371
|
+
"HopperProcessError",
|
|
372
|
+
"HopperStartError",
|
|
373
|
+
"HopperRemoteError",
|
|
374
|
+
].includes(result.error._tag)) {
|
|
375
|
+
const providerId = this.#capabilities?.get(operation)?.provider.id;
|
|
376
|
+
let changed = false;
|
|
377
|
+
for (const descriptor of this.#capabilities?.values() ?? [])
|
|
378
|
+
if (providerId !== undefined && descriptor.provider.id === providerId)
|
|
379
|
+
changed =
|
|
380
|
+
this.#setRuntimeAvailability(descriptor.operation, false, "Provider became unavailable during this session.") || changed;
|
|
381
|
+
if (changed)
|
|
382
|
+
this.#emitAvailabilityChanged();
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
#setRuntimeAvailability(operation, available, reason) {
|
|
386
|
+
const current = this.#runtimeAvailability.get(operation);
|
|
387
|
+
if (current?.available === available && current.reason === reason)
|
|
388
|
+
return false;
|
|
389
|
+
if (available && current === undefined)
|
|
390
|
+
return false;
|
|
391
|
+
if (available)
|
|
392
|
+
this.#runtimeAvailability.delete(operation);
|
|
393
|
+
else
|
|
394
|
+
this.#runtimeAvailability.set(operation, { available, reason });
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
#clearRuntimeAvailability() {
|
|
398
|
+
if (this.#runtimeAvailability.size === 0)
|
|
399
|
+
return;
|
|
400
|
+
this.#runtimeAvailability.clear();
|
|
401
|
+
this.#emitAvailabilityChanged();
|
|
402
|
+
}
|
|
403
|
+
#emitAvailabilityChanged() {
|
|
404
|
+
for (const listener of this.#availabilityListeners)
|
|
405
|
+
listener();
|
|
406
|
+
}
|
|
319
407
|
#serialize(operation) {
|
|
320
408
|
const result = this.#transition.then(operation, operation);
|
|
321
409
|
this.#transition = result.then(() => undefined, () => undefined);
|
|
@@ -2,7 +2,9 @@ import { lstat, readFile, realpath } from "node:fs/promises";
|
|
|
2
2
|
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import writeFileAtomic from "write-file-atomic";
|
|
4
4
|
import { EvidenceFileError } from "../domain/errors.js";
|
|
5
|
+
import { isJsonWithinLimits } from "../domain/jsonLimits.js";
|
|
5
6
|
import { err, ok } from "../domain/result.js";
|
|
7
|
+
import { canonicalizeConfiguredRoots } from "./ConfiguredRoots.js";
|
|
6
8
|
/** Read bounded JSON data from a regular file beneath an approved root. */
|
|
7
9
|
export const readBoundedJson = async (path, policy) => {
|
|
8
10
|
if (policy.roots.length === 0)
|
|
@@ -26,8 +28,9 @@ export const readBoundedJson = async (path, policy) => {
|
|
|
26
28
|
catch (cause) {
|
|
27
29
|
return err(new EvidenceFileError("read", "invalid-json", { cause }));
|
|
28
30
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
return isJsonWithinLimits(decoded, policy)
|
|
32
|
+
? ok(decoded)
|
|
33
|
+
: err(new EvidenceFileError("read", "too-large"));
|
|
31
34
|
}
|
|
32
35
|
catch (cause) {
|
|
33
36
|
return err(new EvidenceFileError("read", "io", { cause }));
|
|
@@ -69,8 +72,7 @@ export const writeBoundedText = async (encoded, path, overwrite, policy) => {
|
|
|
69
72
|
}
|
|
70
73
|
};
|
|
71
74
|
const isApproved = async (candidate, roots) => {
|
|
72
|
-
for (const
|
|
73
|
-
const root = await realpath(resolve(configuredRoot));
|
|
75
|
+
for (const root of await canonicalizeConfiguredRoots(roots.map((configuredRoot) => resolve(configuredRoot)))) {
|
|
74
76
|
const relation = relative(root, candidate);
|
|
75
77
|
if (relation === "" ||
|
|
76
78
|
(!relation.startsWith("..") && !isAbsolute(relation)))
|
|
@@ -78,31 +80,6 @@ const isApproved = async (candidate, roots) => {
|
|
|
78
80
|
}
|
|
79
81
|
return false;
|
|
80
82
|
};
|
|
81
|
-
const validateJsonLimits = (root, policy) => {
|
|
82
|
-
const pending = [
|
|
83
|
-
{ value: root, depth: 0 },
|
|
84
|
-
];
|
|
85
|
-
let nodes = 0;
|
|
86
|
-
while (pending.length > 0) {
|
|
87
|
-
const current = pending.pop();
|
|
88
|
-
if (current === undefined)
|
|
89
|
-
break;
|
|
90
|
-
nodes += 1;
|
|
91
|
-
if (nodes > policy.maxNodes || current.depth > policy.maxDepth)
|
|
92
|
-
return err(new EvidenceFileError("read", "too-large"));
|
|
93
|
-
if (typeof current.value === "string" &&
|
|
94
|
-
current.value.length > policy.maxStringLength)
|
|
95
|
-
return err(new EvidenceFileError("read", "too-large"));
|
|
96
|
-
if (typeof current.value !== "object" || current.value === null)
|
|
97
|
-
continue;
|
|
98
|
-
for (const [key, value] of Object.entries(current.value)) {
|
|
99
|
-
if (key.length > policy.maxStringLength)
|
|
100
|
-
return err(new EvidenceFileError("read", "too-large"));
|
|
101
|
-
pending.push({ value, depth: current.depth + 1 });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return ok(null);
|
|
105
|
-
};
|
|
106
83
|
const isFileNotFound = (cause) => typeof cause === "object" &&
|
|
107
84
|
cause !== null &&
|
|
108
85
|
"code" in cause &&
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createEvidence, } from "../domain/evidence.js";
|
|
2
|
+
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
3
|
+
/** Create Evidence v2 for a policy-scoped external browser observation. */
|
|
4
|
+
export const createBrowserEvidence = (operation, input, result, provider) => createEvidence(undefined, provider, {
|
|
5
|
+
predicateType: operation === "list_browser_targets"
|
|
6
|
+
? "rea.browser-target-list/v1"
|
|
7
|
+
: "rea.web-page-inspection/v1",
|
|
8
|
+
operation,
|
|
9
|
+
parameters: browserParameters(input),
|
|
10
|
+
result: jsonValueSchema.parse(result),
|
|
11
|
+
confidence: "observed",
|
|
12
|
+
authority: "external-service",
|
|
13
|
+
environment: "target" in result
|
|
14
|
+
? {
|
|
15
|
+
id: `${result.browser.product}@${result.browser.revision}`,
|
|
16
|
+
platform: process.platform,
|
|
17
|
+
architecture: process.arch,
|
|
18
|
+
isolation: "none",
|
|
19
|
+
}
|
|
20
|
+
: null,
|
|
21
|
+
limitations: result.limitations,
|
|
22
|
+
});
|
|
23
|
+
const browserParameters = (input) => ({
|
|
24
|
+
cdp_endpoint: input.cdp_endpoint,
|
|
25
|
+
allowed_origins: input.allowed_origins,
|
|
26
|
+
...("target_id" in input
|
|
27
|
+
? {
|
|
28
|
+
target_id: input.target_id,
|
|
29
|
+
observation_ms: input.observation_ms,
|
|
30
|
+
include_script_sources: input.include_script_sources,
|
|
31
|
+
include_storage_keys: input.include_storage_keys,
|
|
32
|
+
limits: input.limits,
|
|
33
|
+
}
|
|
34
|
+
: { offset: input.offset, limit: input.limit }),
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AnalysisCapabilityUnavailableError, AnalysisProtocolError, PermissionRequiredError, } from "../domain/errors.js";
|
|
2
|
+
import { err, ok } from "../domain/result.js";
|
|
3
|
+
import { createBrowserEvidence } from "./BrowserEvidence.js";
|
|
4
|
+
/** Authorize and execute one policy-scoped target discovery observation. */
|
|
5
|
+
export const listBrowserTargets = async (browser, authority, input, options = {}) => {
|
|
6
|
+
const ready = await prepare(browser, authority, input, "list_browser_targets");
|
|
7
|
+
if (!ready.ok)
|
|
8
|
+
return ready;
|
|
9
|
+
const result = await ready.value.listTargets(input, options);
|
|
10
|
+
return result.ok
|
|
11
|
+
? ok(createBrowserEvidence("list_browser_targets", input, result.value, ready.value.identity()))
|
|
12
|
+
: result;
|
|
13
|
+
};
|
|
14
|
+
/** Authorize and execute one policy-scoped passive page inspection. */
|
|
15
|
+
export const inspectWebPage = async (browser, authority, input, options = {}) => {
|
|
16
|
+
const ready = await prepare(browser, authority, input, "inspect_web_page");
|
|
17
|
+
if (!ready.ok)
|
|
18
|
+
return ready;
|
|
19
|
+
const result = await ready.value.inspectPage(input, options);
|
|
20
|
+
return result.ok
|
|
21
|
+
? ok(createBrowserEvidence("inspect_web_page", input, result.value, ready.value.identity()))
|
|
22
|
+
: result;
|
|
23
|
+
};
|
|
24
|
+
const prepare = async (browser, authority, input, operation) => {
|
|
25
|
+
if (authority === undefined)
|
|
26
|
+
return err(new AnalysisCapabilityUnavailableError("rea-cdp-browser", operation, "browser observation permission policy is not configured"));
|
|
27
|
+
const authorized = await authority.authorize({
|
|
28
|
+
capability: "browser_observe",
|
|
29
|
+
roots: [],
|
|
30
|
+
executables: [],
|
|
31
|
+
environment_names: [],
|
|
32
|
+
origins: [input.cdp_endpoint, ...input.allowed_origins],
|
|
33
|
+
network: browserNetworkScope(input.allowed_origins),
|
|
34
|
+
mount: false,
|
|
35
|
+
operation_identity: `${operation}:${input.target_id ?? input.cdp_endpoint}`,
|
|
36
|
+
}, "read");
|
|
37
|
+
if (!authorized.ok)
|
|
38
|
+
return err(authorized.error instanceof PermissionRequiredError
|
|
39
|
+
? authorized.error
|
|
40
|
+
: new AnalysisProtocolError(authorized.error.message, {
|
|
41
|
+
cause: authorized.error,
|
|
42
|
+
}));
|
|
43
|
+
return browser === undefined
|
|
44
|
+
? err(new AnalysisCapabilityUnavailableError("rea-cdp-browser", operation, "browser observation provider is not configured"))
|
|
45
|
+
: ok(browser);
|
|
46
|
+
};
|
|
47
|
+
const browserNetworkScope = (allowedOrigins) => allowedOrigins.every((origin) => {
|
|
48
|
+
const hostname = new URL(origin).hostname;
|
|
49
|
+
return hostname === "127.0.0.1" || hostname === "[::1]";
|
|
50
|
+
})
|
|
51
|
+
? "loopback"
|
|
52
|
+
: "external";
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
|
3
|
+
const statusSchema = z.object({
|
|
4
|
+
open: z.boolean(),
|
|
5
|
+
kind: z.enum(["executable", "database", "archive", "artifact"]).optional(),
|
|
6
|
+
format: z.string().optional(),
|
|
7
|
+
capabilities: z.array(z.object({
|
|
8
|
+
operation: z.string(),
|
|
9
|
+
available: z.boolean(),
|
|
10
|
+
reason: z.string().nullable(),
|
|
11
|
+
})),
|
|
12
|
+
});
|
|
13
|
+
const ENHANCED_REQUIREMENTS = {
|
|
14
|
+
swift_classes: ["list_procedures"],
|
|
15
|
+
get_objc_classes: ["list_names"],
|
|
16
|
+
get_objc_protocols: ["list_names"],
|
|
17
|
+
batch_decompile: ["procedure_pseudo_code"],
|
|
18
|
+
get_call_graph: ["procedure_callees", "procedure_callers"],
|
|
19
|
+
analyze_swift_types: ["list_procedures"],
|
|
20
|
+
find_xrefs_to_name: ["list_names", "xrefs"],
|
|
21
|
+
binary_overview: [
|
|
22
|
+
"list_segments",
|
|
23
|
+
"list_documents",
|
|
24
|
+
"list_procedures",
|
|
25
|
+
"list_strings",
|
|
26
|
+
],
|
|
27
|
+
analyze_function: ["analyze_function"],
|
|
28
|
+
trace_feature: [
|
|
29
|
+
"list_strings",
|
|
30
|
+
"list_procedures",
|
|
31
|
+
"xrefs",
|
|
32
|
+
"resolve_containing_procedure",
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
/** Build stable per-operation availability without hiding familiar tools. */
|
|
36
|
+
export const buildCapabilityInventory = (sessionStatus, policy) => {
|
|
37
|
+
const status = statusSchema.parse(sessionStatus);
|
|
38
|
+
const descriptors = new Map(status.capabilities.map((descriptor) => [descriptor.operation, descriptor]));
|
|
39
|
+
return TOOL_CONTRACTS.map((contract) => {
|
|
40
|
+
const availability = availabilityFor(contract.name, contract.kind, status.open, status.kind, descriptors, policy);
|
|
41
|
+
return {
|
|
42
|
+
name: contract.name,
|
|
43
|
+
surface: contract.kind,
|
|
44
|
+
available: availability.reason === "available",
|
|
45
|
+
reason: availability.reason,
|
|
46
|
+
remediation: availability.remediation,
|
|
47
|
+
annotations: {
|
|
48
|
+
read_only: contract.annotations.readOnlyHint ?? false,
|
|
49
|
+
destructive: contract.annotations.destructiveHint ?? false,
|
|
50
|
+
idempotent: contract.annotations.idempotentHint ?? false,
|
|
51
|
+
open_world: contract.annotations.openWorldHint ?? true,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
55
|
+
};
|
|
56
|
+
const availabilityFor = (name, kind, targetOpen, targetKind, descriptors, policy) => {
|
|
57
|
+
if (name === "capture_process_scenario" && !policy.processCaptureEnabled)
|
|
58
|
+
return {
|
|
59
|
+
reason: "policy_disabled",
|
|
60
|
+
remediation: "Enable or grant process_capture within the administrator ceiling.",
|
|
61
|
+
};
|
|
62
|
+
if (name === "import_evidence_bundle" && policy.evidenceFileRoots === 0)
|
|
63
|
+
return {
|
|
64
|
+
reason: "policy_disabled",
|
|
65
|
+
remediation: "Configure or grant an evidence_read root.",
|
|
66
|
+
};
|
|
67
|
+
if (kind === "browser-provider")
|
|
68
|
+
return policy.browserObservationEnabled === true
|
|
69
|
+
? { reason: "available", remediation: null }
|
|
70
|
+
: {
|
|
71
|
+
reason: "policy_disabled",
|
|
72
|
+
remediation: "Enable browser observation and configure exact CDP endpoint and page origins.",
|
|
73
|
+
};
|
|
74
|
+
if (kind === "session")
|
|
75
|
+
return { reason: "available", remediation: null };
|
|
76
|
+
if (!targetOpen)
|
|
77
|
+
return {
|
|
78
|
+
reason: "target_required",
|
|
79
|
+
remediation: "Call open_binary with a supported local target.",
|
|
80
|
+
};
|
|
81
|
+
if (targetKind !== undefined &&
|
|
82
|
+
targetKind !== "executable" &&
|
|
83
|
+
targetKind !== "database" &&
|
|
84
|
+
(kind === "official-proxy" || kind === "enhanced"))
|
|
85
|
+
return {
|
|
86
|
+
reason: "target_unsupported",
|
|
87
|
+
remediation: "Inventory or extract a native executable, then call open_binary on that executable.",
|
|
88
|
+
};
|
|
89
|
+
if (kind === "enhanced")
|
|
90
|
+
return composedAvailability(name, descriptors);
|
|
91
|
+
const descriptor = descriptors.get(name);
|
|
92
|
+
if (descriptor === undefined)
|
|
93
|
+
return {
|
|
94
|
+
reason: "provider_missing",
|
|
95
|
+
remediation: "Install or configure a provider that declares this operation.",
|
|
96
|
+
};
|
|
97
|
+
if (!descriptor.available &&
|
|
98
|
+
descriptor.reason?.toLowerCase().includes("require macos") === true)
|
|
99
|
+
return {
|
|
100
|
+
reason: "unsupported_host",
|
|
101
|
+
remediation: descriptor.reason,
|
|
102
|
+
};
|
|
103
|
+
return descriptor.available
|
|
104
|
+
? { reason: "available", remediation: null }
|
|
105
|
+
: {
|
|
106
|
+
reason: "provider_unavailable",
|
|
107
|
+
remediation: descriptor.reason ?? "Choose another target or configured provider.",
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
const composedAvailability = (name, descriptors) => {
|
|
111
|
+
const requirements = ENHANCED_REQUIREMENTS[name];
|
|
112
|
+
if (requirements === undefined)
|
|
113
|
+
return {
|
|
114
|
+
reason: "provider_missing",
|
|
115
|
+
remediation: "No provider composition is declared for this operation.",
|
|
116
|
+
};
|
|
117
|
+
const missing = requirements.find((operation) => !descriptors.has(operation));
|
|
118
|
+
if (missing !== undefined)
|
|
119
|
+
return {
|
|
120
|
+
reason: "provider_missing",
|
|
121
|
+
remediation: `Configure a provider for required operation ${missing}.`,
|
|
122
|
+
};
|
|
123
|
+
const unavailable = requirements
|
|
124
|
+
.map((operation) => descriptors.get(operation))
|
|
125
|
+
.find((descriptor) => descriptor?.available === false);
|
|
126
|
+
if (unavailable !== undefined)
|
|
127
|
+
return {
|
|
128
|
+
reason: unavailable.reason?.toLowerCase().includes("require macos") === true
|
|
129
|
+
? "unsupported_host"
|
|
130
|
+
: "provider_unavailable",
|
|
131
|
+
remediation: unavailable.reason ?? "Choose another target or configured provider.",
|
|
132
|
+
};
|
|
133
|
+
return { reason: "available", remediation: null };
|
|
134
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
6
|
+
import { supportedClients } from "./SupportedClients.js";
|
|
7
|
+
const objectSchema = z.record(z.string(), z.unknown());
|
|
8
|
+
const registrationSchema = z
|
|
9
|
+
.object({ command: z.string().min(1), args: z.array(z.string()).default([]) })
|
|
10
|
+
.passthrough();
|
|
11
|
+
/** Inspect supported client registrations without reading their environment. */
|
|
12
|
+
export const readClientRegistrationStatuses = async (home, currentCommandPath = resolve(process.argv[1] ?? "unknown")) => {
|
|
13
|
+
const statuses = [];
|
|
14
|
+
for (const client of supportedClients(home)) {
|
|
15
|
+
if (client.format === "unsupported" || !(await exists(client.markerPath)))
|
|
16
|
+
continue;
|
|
17
|
+
try {
|
|
18
|
+
const content = await readFile(client.configPath, "utf8");
|
|
19
|
+
const document = objectSchema.parse(client.format === "toml" ? parseToml(content) : JSON.parse(content));
|
|
20
|
+
const servers = objectSchema.parse(document[client.format === "toml" ? "mcp_servers" : "mcpServers"] ?? {});
|
|
21
|
+
const raw = servers[PRODUCT_IDENTITY.mcpServerKey];
|
|
22
|
+
if (raw === undefined) {
|
|
23
|
+
statuses.push(status(client.name, client.configPath, [], "missing"));
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const registration = registrationSchema.parse(raw);
|
|
27
|
+
const command = [registration.command, ...registration.args];
|
|
28
|
+
statuses.push(status(client.name, client.configPath, command, registrationAligned(command, currentCommandPath)
|
|
29
|
+
? "aligned"
|
|
30
|
+
: "stale"));
|
|
31
|
+
}
|
|
32
|
+
catch (cause) {
|
|
33
|
+
statuses.push(status(client.name, client.configPath, [], isMissing(cause) ? "missing" : "invalid"));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return statuses.sort((left, right) => left.client.localeCompare(right.client));
|
|
37
|
+
};
|
|
38
|
+
const registrationAligned = (command, currentCommandPath) => {
|
|
39
|
+
if (command.length === 4 &&
|
|
40
|
+
command[0] === "npx" &&
|
|
41
|
+
command[1] === "-y" &&
|
|
42
|
+
command[2] === PRODUCT_IDENTITY.packageName &&
|
|
43
|
+
command[3] === "mcp")
|
|
44
|
+
return true;
|
|
45
|
+
return (command.length === 2 &&
|
|
46
|
+
command[1] === "mcp" &&
|
|
47
|
+
resolve(command[0] ?? "") === currentCommandPath);
|
|
48
|
+
};
|
|
49
|
+
const status = (client, configPath, command, state) => ({
|
|
50
|
+
client,
|
|
51
|
+
config_path: configPath,
|
|
52
|
+
command,
|
|
53
|
+
state,
|
|
54
|
+
remediation: state === "aligned"
|
|
55
|
+
? null
|
|
56
|
+
: "Run rea setup to refresh this registration, then restart the client.",
|
|
57
|
+
});
|
|
58
|
+
const exists = async (path) => {
|
|
59
|
+
if (path === undefined)
|
|
60
|
+
return false;
|
|
61
|
+
try {
|
|
62
|
+
await access(path);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const isMissing = (cause) => cause instanceof Error && "code" in cause && cause.code === "ENOENT";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
/** Canonicalize configured policy roots while ignoring stale missing entries. */
|
|
3
|
+
export const canonicalizeConfiguredRoots = async (roots) => {
|
|
4
|
+
const canonical = [];
|
|
5
|
+
for (const root of roots) {
|
|
6
|
+
try {
|
|
7
|
+
canonical.push(await realpath(root));
|
|
8
|
+
}
|
|
9
|
+
catch (cause) {
|
|
10
|
+
if (!isMissingPath(cause))
|
|
11
|
+
throw cause;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return canonical;
|
|
15
|
+
};
|
|
16
|
+
const isMissingPath = (cause) => typeof cause === "object" &&
|
|
17
|
+
cause !== null &&
|
|
18
|
+
"code" in cause &&
|
|
19
|
+
cause.code === "ENOENT";
|
|
@@ -8,13 +8,19 @@ import { scanAuthorizedArtifactInventory } from "./AuthorizedArtifactInventory.j
|
|
|
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
10
|
/** Authorize and scan both inputs once under the same bounded traversal policy. */
|
|
11
|
-
export const scanVersions = async (input, inputRoots, signal) => {
|
|
11
|
+
export const scanVersions = async (input, inputRoots, signal, integrityContinueEnabled = false) => {
|
|
12
12
|
try {
|
|
13
13
|
if (signal?.aborted === true)
|
|
14
14
|
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
15
15
|
const limits = artifactLimits(input.options);
|
|
16
|
-
const
|
|
17
|
-
|
|
16
|
+
const integrity = {
|
|
17
|
+
mode: input.integrity_policy,
|
|
18
|
+
approved: input.integrity_continue_approved,
|
|
19
|
+
enabled: integrityContinueEnabled,
|
|
20
|
+
maxMismatches: input.max_integrity_mismatches,
|
|
21
|
+
};
|
|
22
|
+
const left = await scanAuthorizedArtifactInventory(input.left_path, inputRoots, limits, signal, integrity);
|
|
23
|
+
const right = await scanAuthorizedArtifactInventory(input.right_path, inputRoots, limits, signal, integrity);
|
|
18
24
|
return ok({ left, right });
|
|
19
25
|
}
|
|
20
26
|
catch (cause) {
|
|
@@ -25,8 +31,8 @@ export const scanVersions = async (input, inputRoots, signal) => {
|
|
|
25
31
|
};
|
|
26
32
|
/** Project complete snapshots into bounded, comparable Evidence page sets. */
|
|
27
33
|
export const createInventoryEvidencePages = (input, snapshots) => {
|
|
28
|
-
const left = inventoryPages(input.left_path, snapshots.left, input
|
|
29
|
-
const right = inventoryPages(input.right_path, snapshots.right, input
|
|
34
|
+
const left = inventoryPages(input.left_path, snapshots.left, input);
|
|
35
|
+
const right = inventoryPages(input.right_path, snapshots.right, input);
|
|
30
36
|
if (left.length > 100 || right.length > 100)
|
|
31
37
|
return err(new ArtifactOperationError("inventory_artifact", "limit"));
|
|
32
38
|
return ok({ left, right });
|
|
@@ -46,11 +52,13 @@ export const runLimitations = (snapshots) => [
|
|
|
46
52
|
AUTOMATIC_RUN_LIMITATION,
|
|
47
53
|
]),
|
|
48
54
|
].sort((left, right) => left.localeCompare(right));
|
|
49
|
-
const inventoryPages = (path, snapshot,
|
|
55
|
+
const inventoryPages = (path, snapshot, input) => {
|
|
56
|
+
const options = input.options;
|
|
50
57
|
const count = Math.max(1, Math.ceil(snapshot.nodes.length / options.page_size), Math.ceil(snapshot.occurrences.length / options.page_size), Math.ceil(snapshot.edges.length / options.page_size));
|
|
51
|
-
return Array.from({ length: count }, (_, index) => inventoryPage(path, snapshot,
|
|
58
|
+
return Array.from({ length: count }, (_, index) => inventoryPage(path, snapshot, input, index * options.page_size));
|
|
52
59
|
};
|
|
53
|
-
const inventoryPage = (path, snapshot,
|
|
60
|
+
const inventoryPage = (path, snapshot, input, offset) => {
|
|
61
|
+
const options = input.options;
|
|
54
62
|
const result = paginateArtifactInventory(snapshot, {
|
|
55
63
|
nodeOffset: offset,
|
|
56
64
|
nodeLimit: options.page_size,
|
|
@@ -78,6 +86,9 @@ const inventoryPage = (path, snapshot, options, offset) => {
|
|
|
78
86
|
max_compression_ratio: options.max_compression_ratio,
|
|
79
87
|
max_depth: options.max_depth,
|
|
80
88
|
max_path_bytes: options.max_path_bytes,
|
|
89
|
+
integrity_policy: input.integrity_policy,
|
|
90
|
+
integrity_continue_approved: input.integrity_continue_approved,
|
|
91
|
+
max_integrity_mismatches: input.max_integrity_mismatches,
|
|
81
92
|
},
|
|
82
93
|
result: jsonValueSchema.parse(result),
|
|
83
94
|
rawResult: null,
|