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.
Files changed (110) hide show
  1. package/README.md +55 -12
  2. package/bridge/hopper_bridge.py +12 -3
  3. package/dist/application/AnalysisSnapshotCache.js +8 -6
  4. package/dist/application/ArtifactGraphConstruction.js +12 -3
  5. package/dist/application/ArtifactInventory.js +100 -27
  6. package/dist/application/AuthorizedArtifactInventory.js +4 -3
  7. package/dist/application/BinarySession.js +92 -4
  8. package/dist/application/BoundedJsonFiles.js +6 -29
  9. package/dist/application/BrowserEvidence.js +35 -0
  10. package/dist/application/BrowserObservationPort.js +1 -0
  11. package/dist/application/BrowserObservationService.js +52 -0
  12. package/dist/application/CapabilityInventory.js +134 -0
  13. package/dist/application/ClientRegistrationStatus.js +69 -0
  14. package/dist/application/ConfiguredRoots.js +19 -0
  15. package/dist/application/CrossVersionInventory.js +19 -8
  16. package/dist/application/CrossVersionInvestigation.js +74 -5
  17. package/dist/application/DeferredFileAuthorization.js +61 -0
  18. package/dist/application/DirectAnalysis.js +117 -9
  19. package/dist/application/Doctor.js +74 -2
  20. package/dist/application/EvidenceLedger.js +6 -5
  21. package/dist/application/InvestigationWorkspaceStore.js +2 -26
  22. package/dist/application/PermissionAuthority.js +183 -0
  23. package/dist/application/PermissionConfiguration.js +26 -0
  24. package/dist/application/ProcessCaptureAuthority.js +3 -2
  25. package/dist/application/ProcessCaptureError.js +13 -0
  26. package/dist/application/ProcessCaptureLifecycle.js +21 -9
  27. package/dist/application/ProcessCheckpoints.js +1 -1
  28. package/dist/application/ProcessCli.js +40 -15
  29. package/dist/application/ProcessHarness.js +10 -20
  30. package/dist/application/ProcessNormalization.js +17 -2
  31. package/dist/application/ProcessOwnership.js +4 -3
  32. package/dist/application/ProcessSampling.js +52 -17
  33. package/dist/application/ProgressReporter.js +30 -0
  34. package/dist/application/ProjectPermissionStore.js +213 -0
  35. package/dist/application/ReferenceSourceImportPolicy.js +3 -2
  36. package/dist/application/Setup.js +2 -41
  37. package/dist/application/SupportedClients.js +41 -0
  38. package/dist/application/TerminalRenderer.js +1 -1
  39. package/dist/application/Uninstall.js +1 -1
  40. package/dist/application/Upgrade.js +2 -0
  41. package/dist/application/runtime.js +1 -1
  42. package/dist/artifacts/ArtifactProvider.js +13 -3
  43. package/dist/artifacts/MachOSliceArtifactReader.js +1 -1
  44. package/dist/artifacts/NativeDmgArtifactReader.js +1 -1
  45. package/dist/artifacts/SafeOutputTree.js +3 -3
  46. package/dist/browser/CdpBrowserProvider.js +158 -0
  47. package/dist/browser/CdpCaptureDocuments.js +140 -0
  48. package/dist/browser/CdpCaptureEvents.js +248 -0
  49. package/dist/browser/CdpCaptureStorage.js +53 -0
  50. package/dist/browser/CdpCaptureValues.js +70 -0
  51. package/dist/browser/CdpConnection.js +218 -0
  52. package/dist/browser/CdpEndpoint.js +114 -0
  53. package/dist/browser/CdpOptionalCommand.js +15 -0
  54. package/dist/browser/CdpPageCapture.js +273 -0
  55. package/dist/catalogIdentity.js +123 -0
  56. package/dist/cli.js +48 -3
  57. package/dist/cliBrowserCommands.js +136 -0
  58. package/dist/cliEvidenceCommands.js +42 -1
  59. package/dist/cliInvestigationCommands.js +42 -6
  60. package/dist/cliLogging.js +20 -3
  61. package/dist/cliPolicyCommands.js +118 -0
  62. package/dist/cliProcessCommands.js +4 -14
  63. package/dist/config.js +107 -3
  64. package/dist/contracts/artifactToolContracts.js +14 -1
  65. package/dist/contracts/browserToolContracts.js +80 -0
  66. package/dist/contracts/errorSchemas.js +100 -0
  67. package/dist/contracts/toolContracts.js +13 -6
  68. package/dist/contracts/toolOutputSchemas.js +62 -30
  69. package/dist/domain/artifactComparison.js +38 -11
  70. package/dist/domain/artifactGraph.js +25 -1
  71. package/dist/domain/artifactInventoryEvidence.js +18 -0
  72. package/dist/domain/browserObservation.js +361 -0
  73. package/dist/domain/changedBehavior.js +9 -18
  74. package/dist/domain/errors.js +257 -3
  75. package/dist/domain/investigationWorkspace.js +131 -4
  76. package/dist/domain/jsonLimits.js +26 -0
  77. package/dist/domain/permissionPolicy.js +161 -0
  78. package/dist/domain/processCapture.js +3 -1
  79. package/dist/domain/processComparison.js +23 -34
  80. package/dist/domain/reconstructionVerification.js +3 -16
  81. package/dist/domain/reconstructionVerificationSchemas.js +3 -0
  82. package/dist/domain/staticRuntimeCorrelation.js +7 -1
  83. package/dist/domain/symbolAnalysis.js +1 -1
  84. package/dist/generatedPackageMetadata.js +9 -0
  85. package/dist/hopper/BridgeLauncher.js +7 -3
  86. package/dist/hopper/HopperClient.js +32 -5
  87. package/dist/identity.js +10 -3
  88. package/dist/logger.js +2 -2
  89. package/dist/main.js +101 -7
  90. package/dist/native/CommandRunner.js +11 -4
  91. package/dist/server/createServer.js +59 -5
  92. package/dist/server/mcpProgress.js +23 -0
  93. package/dist/server/registerArtifactComparisonTool.js +6 -2
  94. package/dist/server/registerBrowserTools.js +36 -0
  95. package/dist/server/registerBundleComparisonTool.js +6 -2
  96. package/dist/server/registerEnhancedTools.js +18 -1
  97. package/dist/server/registerEvidenceResources.js +302 -0
  98. package/dist/server/registerEvidenceTools.js +58 -1
  99. package/dist/server/registerFunctionComparisonTool.js +6 -2
  100. package/dist/server/registerInvestigationTools.js +78 -10
  101. package/dist/server/registerOfficialTools.js +19 -1
  102. package/dist/server/registerProcessComparisonTool.js +9 -3
  103. package/dist/server/registerSessionStatusTool.js +44 -3
  104. package/dist/server/registerSessionTools.js +144 -9
  105. package/dist/server/runDerivedOperation.js +43 -0
  106. package/dist/server/toolResult.js +27 -3
  107. package/dist/serverIdentity.js +70 -0
  108. package/package.json +7 -4
  109. package/scripts/rea.mjs +1 -1
  110. 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?.target;
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
- const limits = validateJsonLimits(decoded, policy);
30
- return limits.ok ? ok(decoded) : limits;
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 configuredRoot of roots) {
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 left = await scanAuthorizedArtifactInventory(input.left_path, inputRoots, limits, signal);
17
- const right = await scanAuthorizedArtifactInventory(input.right_path, inputRoots, limits, signal);
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.options);
29
- const right = inventoryPages(input.right_path, snapshots.right, input.options);
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, options) => {
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, options, index * options.page_size));
58
+ return Array.from({ length: count }, (_, index) => inventoryPage(path, snapshot, input, index * options.page_size));
52
59
  };
53
- const inventoryPage = (path, snapshot, options, offset) => {
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,