rea-agents 1.4.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.
Files changed (123) hide show
  1. package/README.md +37 -13
  2. package/dist/application/AnalysisSnapshotCache.js +8 -6
  3. package/dist/application/ArtifactGraphConstruction.js +11 -2
  4. package/dist/application/ArtifactInventory.js +3 -2
  5. package/dist/application/AuthorizedArtifactInventory.js +2 -1
  6. package/dist/application/BinarySession.js +26 -11
  7. package/dist/application/BoundedJsonFiles.js +6 -29
  8. package/dist/application/BrowserEvidence.js +107 -0
  9. package/dist/application/BrowserObservationPort.js +1 -0
  10. package/dist/application/BrowserObservationService.js +116 -0
  11. package/dist/application/CapabilityInventory.js +14 -0
  12. package/dist/application/ClientConfigPath.js +33 -0
  13. package/dist/application/ConfiguredRoots.js +19 -0
  14. package/dist/application/CrossVersionInventory.js +16 -3
  15. package/dist/application/CrossVersionInvestigation.js +23 -62
  16. package/dist/application/CrossVersionInvestigationEvidence.js +64 -0
  17. package/dist/application/CrossVersionInvestigationReplay.js +170 -0
  18. package/dist/application/DeferredFileAuthorization.js +61 -0
  19. package/dist/application/DirectAnalysis.js +38 -18
  20. package/dist/application/Doctor.js +16 -11
  21. package/dist/application/ElectronEvidence.js +35 -0
  22. package/dist/application/ElectronObservationPort.js +1 -0
  23. package/dist/application/ElectronObservationService.js +46 -0
  24. package/dist/application/EvidenceLedger.js +11 -7
  25. package/dist/application/InvestigationWorkspaceStore.js +2 -26
  26. package/dist/application/PermissionAuthority.js +30 -0
  27. package/dist/application/ProcessCaptureAuthority.js +3 -2
  28. package/dist/application/ProcessCaptureLifecycle.js +19 -13
  29. package/dist/application/ProcessCheckpoints.js +1 -1
  30. package/dist/application/ProcessCli.js +17 -5
  31. package/dist/application/ProcessHarness.js +19 -26
  32. package/dist/application/ProcessNormalization.js +17 -2
  33. package/dist/application/ProcessOwnership.js +44 -23
  34. package/dist/application/ProcessSampling.js +111 -25
  35. package/dist/application/ProjectPermissionStore.js +107 -3
  36. package/dist/application/ReferenceSourceImport.js +13 -16
  37. package/dist/application/ReferenceSourceImportPolicy.js +3 -2
  38. package/dist/application/Setup.js +89 -30
  39. package/dist/application/SetupClients.js +7 -4
  40. package/dist/application/SetupInstallFailure.js +24 -0
  41. package/dist/application/SetupPlan.js +11 -7
  42. package/dist/application/SetupSkill.js +20 -3
  43. package/dist/application/TerminalRenderer.js +1 -1
  44. package/dist/application/Uninstall.js +20 -7
  45. package/dist/application/Upgrade.js +14 -3
  46. package/dist/artifacts/MachOSliceArtifactReader.js +1 -1
  47. package/dist/artifacts/NativeDmgArtifactReader.js +1 -1
  48. package/dist/artifacts/SafeOutputTree.js +3 -3
  49. package/dist/browser/CdpBrowserProvider.js +261 -0
  50. package/dist/browser/CdpCaptureCompleteness.js +90 -0
  51. package/dist/browser/CdpCaptureDocuments.js +299 -0
  52. package/dist/browser/CdpCaptureEvents.js +547 -0
  53. package/dist/browser/CdpCaptureStorage.js +53 -0
  54. package/dist/browser/CdpCaptureValues.js +68 -0
  55. package/dist/browser/CdpConnection.js +241 -0
  56. package/dist/browser/CdpElectronInspection.js +330 -0
  57. package/dist/browser/CdpElectronProvider.js +134 -0
  58. package/dist/browser/CdpEndpoint.js +173 -0
  59. package/dist/browser/CdpObservationSession.js +301 -0
  60. package/dist/browser/CdpOptionalCommand.js +15 -0
  61. package/dist/browser/CdpPageCapture.js +376 -0
  62. package/dist/browser/CdpSafeMetadata.js +203 -0
  63. package/dist/browser/CdpScreenshot.js +101 -0
  64. package/dist/browser/CdpTargetSession.js +47 -0
  65. package/dist/browser/CdpWebMcpDiscovery.js +242 -0
  66. package/dist/browser/ElectronFileScope.js +50 -0
  67. package/dist/browser/PngVisualDiff.js +177 -0
  68. package/dist/browser/SensitiveTextCapture.js +25 -0
  69. package/dist/browser/WebSourceMapFetcher.js +357 -0
  70. package/dist/catalogIdentity.js +10 -0
  71. package/dist/cli.js +6 -0
  72. package/dist/cliBrowserAdvancedCommands.js +169 -0
  73. package/dist/cliBrowserCommands.js +312 -0
  74. package/dist/cliElectronCommands.js +114 -0
  75. package/dist/cliInvestigationCommands.js +26 -34
  76. package/dist/cliPolicyCommands.js +60 -12
  77. package/dist/config.js +80 -1
  78. package/dist/contracts/browserToolContracts.js +381 -0
  79. package/dist/contracts/electronToolContracts.js +73 -0
  80. package/dist/contracts/errorSchemas.js +2 -0
  81. package/dist/contracts/toolContracts.js +5 -5
  82. package/dist/domain/browserCompleteness.js +101 -0
  83. package/dist/domain/browserObservation.js +587 -0
  84. package/dist/domain/browserSession.js +63 -0
  85. package/dist/domain/changedBehavior.js +4 -15
  86. package/dist/domain/electronObservation.js +162 -0
  87. package/dist/domain/errors.js +41 -0
  88. package/dist/domain/investigationWorkspace.js +4 -0
  89. package/dist/domain/jsonLimits.js +26 -0
  90. package/dist/domain/jsonShape.js +125 -0
  91. package/dist/domain/permissionPolicy.js +6 -2
  92. package/dist/domain/processCapture.js +3 -1
  93. package/dist/domain/processComparison.js +22 -34
  94. package/dist/domain/reconstructionVerification.js +2 -15
  95. package/dist/domain/reconstructionVerificationSchemas.js +2 -0
  96. package/dist/domain/referenceSourceGraph.js +3 -1
  97. package/dist/domain/staticRuntimeCorrelation.js +2 -0
  98. package/dist/domain/symbolAnalysis.js +1 -1
  99. package/dist/domain/webBundleAnalysis.js +195 -0
  100. package/dist/domain/webBundleAnalyzer.js +423 -0
  101. package/dist/domain/webCaptureDiff.js +184 -0
  102. package/dist/domain/webContentArtifact.js +47 -0
  103. package/dist/domain/webInventory.js +61 -0
  104. package/dist/domain/webMcpDiscovery.js +68 -0
  105. package/dist/domain/webScreenshot.js +114 -0
  106. package/dist/generatedPackageMetadata.js +2 -2
  107. package/dist/hopper/BridgeLauncher.js +2 -3
  108. package/dist/hopper/HopperClient.js +51 -11
  109. package/dist/hopper/HopperProvider.js +6 -1
  110. package/dist/main.js +42 -31
  111. package/dist/native/CommandRunner.js +11 -4
  112. package/dist/native/NativeMacOSProvider.js +3 -2
  113. package/dist/native/NativeMachoInspection.js +21 -6
  114. package/dist/server/createServer.js +14 -0
  115. package/dist/server/registerBrowserTools.js +95 -0
  116. package/dist/server/registerElectronTools.js +36 -0
  117. package/dist/server/registerInvestigationTools.js +31 -37
  118. package/dist/server/registerProcessComparisonTool.js +2 -0
  119. package/dist/server/registerSessionTools.js +16 -2
  120. package/dist/server/runDerivedOperation.js +10 -2
  121. package/package.json +6 -1
  122. package/scripts/rea.mjs +2 -2
  123. package/skills/rea-analysis/SKILL.md +28 -3
@@ -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";
@@ -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";
@@ -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
- const left = await scanAuthorizedArtifactInventory(input.left_path, inputRoots, limits, signal, integrity);
23
- const right = await scanAuthorizedArtifactInventory(input.right_path, inputRoots, limits, signal, integrity);
24
- return ok({ left, right });
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 { compareArtifacts } from "../domain/artifactComparison.js";
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, investigationRunSummarySchema, reviseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
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 { AUTOMATIC_RUN_LIMITATION, createInventoryEvidencePages, runLimitations, scanVersions, targetFor, } from "./CrossVersionInventory.js";
6
+ import { createInventoryEvidencePages, runLimitations, scanVersions, targetFor, } from "./CrossVersionInventory.js";
10
7
  import { readInvestigationWorkspace, writeInvestigationWorkspace, } from "./InvestigationWorkspaceStore.js";
11
- import { ARTIFACT_COMPARISON_PROVIDER, CHANGED_BEHAVIOR_PROVIDER, } from "./InvestigationProviders.js";
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,
@@ -47,6 +56,7 @@ export const runCrossVersionInvestigation = async (input, policy, execution) =>
47
56
  session: execution.session,
48
57
  signal: execution.signal,
49
58
  progress: execution.progress,
59
+ authorizeWorkspaceWrite: execution.authorizeWorkspaceWrite,
50
60
  });
51
61
  };
52
62
  const continueInvestigation = async (context) => {
@@ -74,6 +84,11 @@ const continueInvestigation = async (context) => {
74
84
  run.run_id === legacyIdentity.runId));
75
85
  if (context.initial !== null && existing?.status === "complete")
76
86
  return completeOutcome(context.initial, existing, true, context.session);
87
+ if (context.authorizeWorkspaceWrite !== undefined) {
88
+ const authorized = await context.authorizeWorkspaceWrite();
89
+ if (!authorized.ok)
90
+ return authorized;
91
+ }
77
92
  const pages = createInventoryEvidencePages(context.input, context.snapshots);
78
93
  if (!pages.ok)
79
94
  return pages;
@@ -192,31 +207,11 @@ const ensureComparisonCheckpoint = async (input, state, policy) => {
192
207
  });
193
208
  return saved.ok ? ok({ workspace: saved.value, run }) : saved;
194
209
  };
195
- const createArtifactComparisonEvidence = (inventory, limit) => {
196
- const comparison = compareArtifacts(inventory.left, inventory.right, 0, limit);
197
- const leftIds = evidenceIds(inventory.left);
198
- const rightIds = evidenceIds(inventory.right);
199
- return createEvidence(undefined, ARTIFACT_COMPARISON_PROVIDER, {
200
- predicateType: "rea.artifact-comparison/v1",
201
- operation: "compare_artifacts",
202
- parameters: {
203
- left_evidence_ids: leftIds,
204
- right_evidence_ids: rightIds,
205
- offset: 0,
206
- limit,
207
- },
208
- result: jsonValueSchema.parse(comparison),
209
- confidence: "derived",
210
- authority: "analyst-inference",
211
- limitations: comparison.limitations,
212
- evidenceLinks: [...leftIds, ...rightIds],
213
- });
214
- };
215
210
  const finalizeInvestigation = async (input, state, policy, session) => {
216
211
  const comparison = comparisonEvidenceFor(state.workspace, state.run);
217
212
  if (!comparison.ok)
218
213
  return comparison;
219
- const evidence = createChangedBehaviorEvidence(state, comparison.value);
214
+ const evidence = createChangedBehaviorEvidence(state.workspace, state.run, comparison.value);
220
215
  const run = investigationRunSchema.parse({
221
216
  ...state.run,
222
217
  status: "complete",
@@ -238,40 +233,6 @@ const finalizeInvestigation = async (input, state, policy, session) => {
238
233
  });
239
234
  return saved.ok ? completeOutcome(saved.value, run, false, session) : saved;
240
235
  };
241
- const createChangedBehaviorEvidence = (state, comparison) => {
242
- const limit = Math.min(state.run.options.change_limit, 100);
243
- const changed = findChangedBehavior([comparison], 0, limit);
244
- const summary = investigationRunSummarySchema.parse({
245
- schema_version: 1,
246
- workspace_id: state.workspace.workspace_id,
247
- run_id: state.run.run_id,
248
- left_manifest_id: state.run.left.manifest_id,
249
- right_manifest_id: state.run.right.manifest_id,
250
- inventory_evidence_count: state.run.left_inventory_evidence_ids.length +
251
- state.run.right_inventory_evidence_ids.length,
252
- comparison_evidence_id: comparison.evidence_id,
253
- limitations: state.run.limitations,
254
- });
255
- return createEvidence(undefined, CHANGED_BEHAVIOR_PROVIDER, {
256
- predicateType: "rea.changed-behavior/v1",
257
- operation: "find_changed_behavior",
258
- parameters: {
259
- workspace_id: state.workspace.workspace_id,
260
- run_id: state.run.run_id,
261
- comparison_evidence_ids: [comparison.evidence_id],
262
- offset: 0,
263
- limit,
264
- },
265
- result: jsonValueSchema.parse({
266
- ...changed,
267
- investigation_run: summary,
268
- }),
269
- confidence: "derived",
270
- authority: "analyst-inference",
271
- limitations: [...changed.limitations, AUTOMATIC_RUN_LIMITATION],
272
- evidenceLinks: changed.evidence_links,
273
- });
274
- };
275
236
  const comparisonEvidenceFor = (workspace, run) => {
276
237
  const evidence = workspace.bundle.records.find(({ evidence_id: id }) => id === run.comparison_evidence_id);
277
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);
@@ -0,0 +1,170 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { artifactInventoryResultSchema, } from "../domain/artifactGraph.js";
3
+ import { parseArtifactInventoryEvidence } from "../domain/artifactInventoryEvidence.js";
4
+ import { EvidenceIntegrityError, InvestigationWorkspaceError, } from "../domain/errors.js";
5
+ import { err, ok } from "../domain/result.js";
6
+ import { createArtifactComparisonEvidence, createChangedBehaviorEvidence, } from "./CrossVersionInvestigationEvidence.js";
7
+ import { AUTOMATIC_RUN_LIMITATION } from "./CrossVersionInventory.js";
8
+ import { ARTIFACT_GRAPH_PROVIDER } from "./InvestigationProviders.js";
9
+ /** Select and verify one caller-identified complete run without input access. */
10
+ export const selectCompletedInvestigationReplay = (workspace, input) => {
11
+ const run = workspace?.runs.find(({ run_id: runId }) => runId === input.replay_run_id);
12
+ if (workspace === null ||
13
+ run === undefined ||
14
+ run.status !== "complete" ||
15
+ !controlsMatch(run, input))
16
+ return replayConflict();
17
+ const records = new Map(workspace.bundle.records.map((record) => [record.evidence_id, record]));
18
+ const left = inventoryRecords(records, run.left_inventory_evidence_ids);
19
+ const right = inventoryRecords(records, run.right_inventory_evidence_ids);
20
+ if (!left.ok)
21
+ return left;
22
+ if (!right.ok)
23
+ return right;
24
+ if (!pathsMatch(left.value, input.left_path) ||
25
+ !pathsMatch(right.value, input.right_path))
26
+ return replayConflict();
27
+ const validated = validateLinkedEvidence(workspace, run, left.value, right.value);
28
+ return validated.ok ? ok({ workspace, run }) : validated;
29
+ };
30
+ const controlsMatch = (run, input) => isDeepStrictEqual(run.options, input.options) &&
31
+ run.integrity_policy === input.integrity_policy &&
32
+ run.integrity_continue_approved === input.integrity_continue_approved &&
33
+ run.max_integrity_mismatches === input.max_integrity_mismatches;
34
+ const inventoryRecords = (records, evidenceIds) => {
35
+ if (new Set(evidenceIds).size !== evidenceIds.length)
36
+ return replayIntegrity("Investigation replay repeats inventory Evidence");
37
+ const selected = evidenceIds.flatMap((evidenceId) => {
38
+ const evidence = records.get(evidenceId);
39
+ return evidence === undefined ? [] : [evidence];
40
+ });
41
+ return selected.length === evidenceIds.length
42
+ ? ok(selected)
43
+ : replayIntegrity("Investigation replay is missing inventory Evidence");
44
+ };
45
+ const pathsMatch = (records, path) => records.every(({ subject }) => subject?.local_path === path);
46
+ const validateLinkedEvidence = (workspace, run, leftRecords, rightRecords) => {
47
+ try {
48
+ const left = validateInventory(leftRecords, run.left, run);
49
+ const right = validateInventory(rightRecords, run.right, run);
50
+ if (!left.ok)
51
+ return left;
52
+ if (!right.ok)
53
+ return right;
54
+ const expectedLimitations = runLimitations(left.value, right.value);
55
+ if (!isDeepStrictEqual(run.limitations, expectedLimitations))
56
+ return replayIntegrity("Investigation replay limitations disagree with inventory Evidence");
57
+ const comparison = createArtifactComparisonEvidence({ left: left.value.records, right: right.value.records }, run.options.change_limit);
58
+ const storedComparison = evidenceFor(workspace, run.comparison_evidence_id);
59
+ if (storedComparison === undefined ||
60
+ !isDeepStrictEqual(storedComparison, comparison))
61
+ return replayIntegrity("Investigation replay comparison Evidence is inconsistent");
62
+ const result = createChangedBehaviorEvidence(workspace, run, comparison);
63
+ const storedResult = evidenceFor(workspace, run.result_evidence_id);
64
+ return storedResult !== undefined && isDeepStrictEqual(storedResult, result)
65
+ ? ok(null)
66
+ : replayIntegrity("Investigation replay result Evidence is inconsistent");
67
+ }
68
+ catch (cause) {
69
+ return replayIntegrity("Investigation replay linked Evidence is invalid", cause);
70
+ }
71
+ };
72
+ const validateInventory = (records, target, run) => {
73
+ const pages = records.map((record) => artifactInventoryResultSchema.parse(record.normalized_result));
74
+ const expectedPageCount = Math.max(1, Math.ceil((pages[0]?.manifest.node_count ?? 0) / run.options.page_size), Math.ceil((pages[0]?.manifest.occurrence_count ?? 0) / run.options.page_size), Math.ceil((pages[0]?.manifest.edge_count ?? 0) / run.options.page_size));
75
+ if (records.length !== expectedPageCount)
76
+ return replayIntegrity("Investigation replay inventory Evidence pagination is inconsistent");
77
+ if (!sameSnapshotMetadata(pages))
78
+ return replayIntegrity("Investigation replay inventory pages disagree on snapshot metadata");
79
+ for (const [index, record] of records.entries()) {
80
+ const page = pages[index];
81
+ if (page === undefined || !inventoryRecordMatches(record, page, run, index))
82
+ return replayIntegrity("Investigation replay inventory Evidence is inconsistent");
83
+ }
84
+ const result = parseArtifactInventoryEvidence(records);
85
+ if (!result.inventory.complete || !targetMatches(result.inventory, target))
86
+ return replayIntegrity("Investigation replay inventory commitment is inconsistent");
87
+ return ok({ records, result });
88
+ };
89
+ const inventoryRecordMatches = (record, page, run, index) => {
90
+ const offset = index * run.options.page_size;
91
+ const parameters = {
92
+ node_offset: offset,
93
+ node_limit: run.options.page_size,
94
+ occurrence_offset: offset,
95
+ occurrence_limit: run.options.page_size,
96
+ edge_offset: offset,
97
+ edge_limit: run.options.page_size,
98
+ max_entries: run.options.max_entries,
99
+ max_total_bytes: run.options.max_total_bytes,
100
+ max_entry_bytes: run.options.max_entry_bytes,
101
+ max_compression_ratio: run.options.max_compression_ratio,
102
+ max_depth: run.options.max_depth,
103
+ max_path_bytes: run.options.max_path_bytes,
104
+ integrity_policy: run.integrity_policy,
105
+ integrity_continue_approved: run.integrity_continue_approved,
106
+ max_integrity_mismatches: run.max_integrity_mismatches,
107
+ };
108
+ const locations = page.occurrences.items.map(({ logical_path: path }) => ({
109
+ kind: "artifact-path",
110
+ path,
111
+ }));
112
+ return (record.subject !== null &&
113
+ record.subject.name === record.subject.local_path.split("/").at(-1) &&
114
+ record.subject.digest.sha256 === page.manifest.root_sha256 &&
115
+ record.subject.format === page.manifest.root_format &&
116
+ record.subject.architecture === null &&
117
+ isDeepStrictEqual(record.provider, ARTIFACT_GRAPH_PROVIDER) &&
118
+ record.predicate_type === "rea.analysis/v2" &&
119
+ record.operation === "inventory_artifact" &&
120
+ isDeepStrictEqual(record.parameters, parameters) &&
121
+ record.raw_result === null &&
122
+ record.confidence === "observed" &&
123
+ record.authority === "shipped-artifact" &&
124
+ record.environment === null &&
125
+ isDeepStrictEqual(record.limitations, page.limitations) &&
126
+ isDeepStrictEqual(record.locations, locations) &&
127
+ record.evidence_links.length === 0 &&
128
+ isDeepStrictEqual(page.limits, traversalLimits(run)) &&
129
+ pageMatches(page.nodes, offset, run.options.page_size, page.manifest.node_count) &&
130
+ pageMatches(page.occurrences, offset, run.options.page_size, page.manifest.occurrence_count) &&
131
+ pageMatches(page.edges, offset, run.options.page_size, page.manifest.edge_count));
132
+ };
133
+ const sameSnapshotMetadata = (pages) => {
134
+ const first = pages[0];
135
+ return (first !== undefined &&
136
+ pages.every((page) => isDeepStrictEqual(page.manifest, first.manifest) &&
137
+ isDeepStrictEqual(page.limits, first.limits) &&
138
+ isDeepStrictEqual(page.provenance, first.provenance) &&
139
+ isDeepStrictEqual(page.integrity_contradictions, first.integrity_contradictions) &&
140
+ isDeepStrictEqual(page.limitations, first.limitations)));
141
+ };
142
+ const pageMatches = (page, offset, limit, total) => page.offset === offset &&
143
+ page.limit === limit &&
144
+ page.total === total &&
145
+ page.items.length === Math.max(0, Math.min(limit, page.total - offset)) &&
146
+ page.next_offset === (offset + limit < page.total ? offset + limit : null);
147
+ const traversalLimits = (run) => ({
148
+ max_entries: run.options.max_entries,
149
+ max_total_bytes: run.options.max_total_bytes,
150
+ max_entry_bytes: run.options.max_entry_bytes,
151
+ max_compression_ratio: run.options.max_compression_ratio,
152
+ max_depth: run.options.max_depth,
153
+ max_path_bytes: run.options.max_path_bytes,
154
+ });
155
+ const targetMatches = (inventory, target) => inventory.manifest.root_sha256 === target.root_sha256 &&
156
+ inventory.manifest.graph_sha256 === target.graph_sha256 &&
157
+ inventory.manifest.manifest_id === target.manifest_id &&
158
+ inventory.manifest.root_format === target.format;
159
+ const runLimitations = (left, right) => [
160
+ ...new Set([
161
+ ...left.result.inventory.limitations.map((item) => `Left: ${item}`),
162
+ ...right.result.inventory.limitations.map((item) => `Right: ${item}`),
163
+ AUTOMATIC_RUN_LIMITATION,
164
+ ]),
165
+ ].sort((first, second) => first.localeCompare(second));
166
+ const evidenceFor = (workspace, evidenceId) => evidenceId === null
167
+ ? undefined
168
+ : workspace.bundle.records.find(({ evidence_id: current }) => current === evidenceId);
169
+ const replayConflict = () => err(new InvestigationWorkspaceError("read", "revision-conflict"));
170
+ const replayIntegrity = (message, cause) => err(new EvidenceIntegrityError(message, cause === undefined ? undefined : { cause }));
@@ -0,0 +1,61 @@
1
+ import { AnalysisProtocolError, PermissionRequiredError, } from "../domain/errors.js";
2
+ import { err, ok } from "../domain/result.js";
3
+ import { PermissionPathError, } from "./PermissionAuthority.js";
4
+ /** Authorize one root-scoped operation and project policy path failures. */
5
+ export const authorizeRootPermission = async (authority, request) => {
6
+ const authorized = await authority.authorize({
7
+ capability: request.capability,
8
+ roots: request.roots,
9
+ executables: [],
10
+ environment_names: [],
11
+ network: "none",
12
+ mount: false,
13
+ operation_identity: `${request.operation}:${request.capability}:${request.roots.join(":")}`,
14
+ }, request.access);
15
+ return authorized.ok
16
+ ? ok(null)
17
+ : err(authorized.error instanceof PermissionRequiredError
18
+ ? authorized.error
19
+ : new AnalysisProtocolError(authorized.error.message, {
20
+ cause: authorized.error,
21
+ }));
22
+ };
23
+ /** Authorize an existing file read and defer write authority until requested. */
24
+ export const authorizeFileReadWithDeferredWrite = async (authority, input) => {
25
+ const read = await authority.authorize({
26
+ capability: input.readCapability,
27
+ roots: [input.path],
28
+ executables: [],
29
+ environment_names: [],
30
+ network: "none",
31
+ mount: false,
32
+ operation_identity: `${input.operation}:${input.readCapability}:${input.path}`,
33
+ }, "read");
34
+ let writeAuthorization;
35
+ if (!read.ok) {
36
+ if (!(read.error instanceof PermissionPathError) ||
37
+ read.error.reason !== "not_found")
38
+ return err(read.error instanceof PermissionRequiredError
39
+ ? read.error
40
+ : new AnalysisProtocolError(read.error.message, {
41
+ cause: read.error,
42
+ }));
43
+ writeAuthorization = authorizeRootPermission(authority, {
44
+ capability: input.writeCapability,
45
+ roots: [input.path],
46
+ access: "write",
47
+ operation: input.operation,
48
+ });
49
+ const authorized = await writeAuthorization;
50
+ if (!authorized.ok)
51
+ return authorized;
52
+ }
53
+ return ok({
54
+ authorizeWrite: () => (writeAuthorization ??= authorizeRootPermission(authority, {
55
+ capability: input.writeCapability,
56
+ roots: [input.path],
57
+ access: "write",
58
+ operation: input.operation,
59
+ })),
60
+ });
61
+ };
@@ -12,6 +12,7 @@ import { parseBinaryTarget } from "../domain/binaryTarget.js";
12
12
  import { snapshotEvidenceForQuery, snapshotMatchesTarget, } from "../domain/analysisSnapshot.js";
13
13
  import { err, ok } from "../domain/result.js";
14
14
  import { loadConfiguredPermissionAuthority } from "./PermissionConfiguration.js";
15
+ import { authorizeFileReadWithDeferredWrite, } from "./DeferredFileAuthorization.js";
15
16
  const WORKFLOW_PROVIDER = {
16
17
  id: "rea-workflow",
17
18
  name: "REA composed investigation workflow",
@@ -26,6 +27,9 @@ export const runDirectAnalysis = async (path, tool, arguments_, options = {}) =>
26
27
  logger: options.logger ?? silentLogger,
27
28
  snapshotPath: options.snapshotPath,
28
29
  signal,
30
+ ...(options.permissionAuthority === undefined
31
+ ? {}
32
+ : { permissionAuthority: options.permissionAuthority }),
29
33
  }));
30
34
  /** Execute one provider-native semantic operation with atomic provenance. */
31
35
  export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger, signal) => withProcessCancellation(signal, (operationSignal) => runAnalysis(path, tool, arguments_, {
@@ -51,19 +55,8 @@ export const runSessionStatus = async (logger = silentLogger) => {
51
55
  await session.close();
52
56
  }
53
57
  };
54
- const authorizeAnalysis = async (authority, tool, arguments_, snapshotPath) => {
58
+ const authorizeAnalysis = async (authority, tool, arguments_) => {
55
59
  const requests = [];
56
- if (snapshotPath !== undefined) {
57
- requests.push({
58
- capability: "snapshot_read",
59
- path: snapshotPath,
60
- access: "write",
61
- }, {
62
- capability: "snapshot_write",
63
- path: snapshotPath,
64
- access: "write",
65
- });
66
- }
67
60
  if (tool === "extract_artifact" && typeof arguments_.output_root === "string")
68
61
  requests.push({
69
62
  capability: "artifact_extract",
@@ -107,17 +100,41 @@ const authorizeAnalysis = async (authority, tool, arguments_, snapshotPath) => {
107
100
  }
108
101
  return ok(null);
109
102
  };
103
+ const permissionAuthorityFor = (config, supplied) => supplied === undefined
104
+ ? loadConfiguredPermissionAuthority(config)
105
+ : Promise.resolve(ok(supplied));
106
+ const authorizeSnapshotAccess = async (authority, tool, snapshotPath) => snapshotPath === undefined
107
+ ? ok(undefined)
108
+ : authorizeFileReadWithDeferredWrite(authority, {
109
+ path: snapshotPath,
110
+ readCapability: "snapshot_read",
111
+ writeCapability: "snapshot_write",
112
+ operation: tool,
113
+ });
114
+ const authorizeDeferredWrite = (authorization) => authorization?.authorizeWrite() ?? Promise.resolve(ok(null));
115
+ const authorizeAnalysisRun = async (input) => {
116
+ const authority = await permissionAuthorityFor(input.config, input.suppliedAuthority);
117
+ if (!authority.ok)
118
+ return authority;
119
+ const operation = await authorizeAnalysis(authority.value, input.tool, input.arguments);
120
+ if (!operation.ok)
121
+ return operation;
122
+ return authorizeSnapshotAccess(authority.value, input.tool, input.snapshotPath);
123
+ };
110
124
  const runAnalysis = async (path, tool, arguments_, options) => {
111
125
  const { logger, signal, snapshotPath } = options;
112
126
  const config = parseConfig(process.env);
113
127
  if (!config.ok)
114
128
  return cliError(config.error);
115
- const permissionAuthority = await loadConfiguredPermissionAuthority(config.value);
116
- if (!permissionAuthority.ok)
117
- return cliError(permissionAuthority.error);
118
- const authorized = await authorizeAnalysis(permissionAuthority.value, tool, arguments_, snapshotPath);
119
- if (!authorized.ok)
120
- return cliError(authorized.error);
129
+ const authorization = await authorizeAnalysisRun({
130
+ config: config.value,
131
+ suppliedAuthority: options.permissionAuthority,
132
+ tool,
133
+ arguments: arguments_,
134
+ snapshotPath,
135
+ });
136
+ if (!authorization.ok)
137
+ return cliError(authorization.error);
121
138
  const unavailable = snapshotUnavailable(snapshotPath, config.value.hopperLoaderArgs);
122
139
  if (unavailable !== undefined)
123
140
  return cliError(unavailable);
@@ -135,6 +152,9 @@ const runAnalysis = async (path, tool, arguments_, options) => {
135
152
  return cliError(prepared.error);
136
153
  if (prepared.value.evidence !== undefined)
137
154
  return prepared.value.evidence;
155
+ const writable = await authorizeDeferredWrite(authorization.value);
156
+ if (!writable.ok)
157
+ return cliError(writable.error);
138
158
  const { snapshot } = prepared.value;
139
159
  const opened = await session.open(path, snapshot === undefined ? { signal } : { signal, snapshot });
140
160
  if (!opened.ok)
@@ -37,23 +37,28 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
37
37
  remediation: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",
38
38
  classification: "unsupported_host",
39
39
  }));
40
- const candidates = [
41
- host.configuredHopperPath,
42
- DEFAULT_HOPPER,
43
- SYSTEM_LINUX_HOPPER,
44
- linuxHopperLauncherPath(homedir()),
45
- ...(await host.manualHopperPaths()),
46
- await host.brewHopperPath(),
47
- ].filter((value) => value !== undefined);
40
+ const candidates = host.configuredHopperPath === undefined
41
+ ? [
42
+ DEFAULT_HOPPER,
43
+ SYSTEM_LINUX_HOPPER,
44
+ linuxHopperLauncherPath(homedir()),
45
+ ...(await host.manualHopperPaths()),
46
+ await host.brewHopperPath(),
47
+ ].filter((value) => value !== undefined)
48
+ : [host.configuredHopperPath];
48
49
  let hopperPath;
49
50
  for (const candidate of new Set(candidates))
50
51
  if (await host.executable(candidate)) {
51
52
  hopperPath = candidate;
52
53
  break;
53
54
  }
54
- checks.push(check("hopper", hopperPath !== undefined, hopperPath, {
55
- remediation: "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",
56
- classification: "missing_analysis_engine",
55
+ checks.push(check("hopper", hopperPath !== undefined, hopperPath ?? host.configuredHopperPath, {
56
+ remediation: host.configuredHopperPath === undefined
57
+ ? "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH."
58
+ : "Unset or update HOPPER_LAUNCHER_PATH to an executable Hopper launcher.",
59
+ classification: host.configuredHopperPath === undefined
60
+ ? "missing_analysis_engine"
61
+ : "config_drift",
57
62
  }));
58
63
  if (host.platform === "linux" && hopperPath !== undefined)
59
64
  checks.push(check("hopper-version", await host.supportedLinuxHopper(hopperPath), hopperPath, {