rea-agents 1.2.0 → 1.4.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 (107) hide show
  1. package/README.md +95 -27
  2. package/bridge/hopper_bridge.py +172 -14
  3. package/dist/application/AnalysisSnapshotCache.js +131 -0
  4. package/dist/application/AnalysisSnapshotFiles.js +31 -0
  5. package/dist/application/ArtifactGraphConstruction.js +1 -1
  6. package/dist/application/ArtifactInventory.js +100 -24
  7. package/dist/application/AuthorizedArtifactInventory.js +16 -0
  8. package/dist/application/BinarySession.js +149 -36
  9. package/dist/application/BinarySessionPort.js +1 -0
  10. package/dist/application/BoundedJsonFiles.js +109 -0
  11. package/dist/application/CapabilityInventory.js +127 -0
  12. package/dist/application/ClientRegistrationStatus.js +69 -0
  13. package/dist/application/CrossVersionInventory.js +109 -0
  14. package/dist/application/CrossVersionInvestigation.js +358 -0
  15. package/dist/application/DirectAnalysis.js +205 -24
  16. package/dist/application/Doctor.js +104 -3
  17. package/dist/application/EvidenceBundleFiles.js +15 -108
  18. package/dist/application/EvidenceLedger.js +3 -2
  19. package/dist/application/InvestigationProviders.js +48 -0
  20. package/dist/application/InvestigationWorkspaceStore.js +214 -0
  21. package/dist/application/LinuxHopper.js +72 -48
  22. package/dist/application/PermissionAuthority.js +183 -0
  23. package/dist/application/PermissionConfiguration.js +26 -0
  24. package/dist/application/ProcessCaptureAuthority.js +2 -2
  25. package/dist/application/ProcessCaptureError.js +25 -0
  26. package/dist/application/ProcessCaptureLifecycle.js +22 -2
  27. package/dist/application/ProcessCli.js +111 -31
  28. package/dist/application/ProcessHarness.js +6 -3
  29. package/dist/application/ProgressReporter.js +30 -0
  30. package/dist/application/ProjectPermissionStore.js +110 -0
  31. package/dist/application/ReferenceSourceImportEntries.js +18 -3
  32. package/dist/application/ReferenceSourceImportTypes.js +32 -0
  33. package/dist/application/Setup.js +35 -101
  34. package/dist/application/SetupClients.js +19 -0
  35. package/dist/application/SetupInstallFailure.js +53 -0
  36. package/dist/application/SetupPlan.js +31 -0
  37. package/dist/application/SupportedClients.js +41 -0
  38. package/dist/application/Uninstall.js +34 -24
  39. package/dist/application/UnknownEvidence.js +33 -0
  40. package/dist/application/Upgrade.js +3 -1
  41. package/dist/application/runtime.js +2 -2
  42. package/dist/artifacts/ArtifactProvider.js +15 -8
  43. package/dist/catalogIdentity.js +121 -0
  44. package/dist/cli.js +76 -13
  45. package/dist/cliEvidenceCommands.js +52 -12
  46. package/dist/cliInvestigationCommands.js +137 -0
  47. package/dist/cliLogging.js +20 -3
  48. package/dist/cliOutput.js +41 -0
  49. package/dist/cliPolicyCommands.js +115 -0
  50. package/dist/config.js +102 -27
  51. package/dist/contracts/artifactToolContracts.js +14 -1
  52. package/dist/contracts/errorSchemas.js +98 -0
  53. package/dist/contracts/promptContracts.js +256 -0
  54. package/dist/contracts/sessionLifecycleInputs.js +11 -0
  55. package/dist/contracts/toolContractTypes.js +1 -0
  56. package/dist/contracts/toolContracts.js +20 -7
  57. package/dist/contracts/toolOutputSchemas.js +70 -9
  58. package/dist/domain/analysisSnapshot.js +149 -0
  59. package/dist/domain/artifactComparison.js +38 -11
  60. package/dist/domain/artifactGraph.js +25 -1
  61. package/dist/domain/artifactInventoryEvidence.js +18 -0
  62. package/dist/domain/changedBehavior.js +35 -10
  63. package/dist/domain/errors.js +384 -32
  64. package/dist/domain/evidence.js +4 -2
  65. package/dist/domain/evidenceBundle.js +28 -0
  66. package/dist/domain/hopperStartupFailure.js +55 -0
  67. package/dist/domain/investigationWorkspace.js +341 -0
  68. package/dist/domain/jsonValue.js +2 -0
  69. package/dist/domain/nativeInspection.js +3 -2
  70. package/dist/domain/permissionPolicy.js +157 -0
  71. package/dist/domain/processCapture.js +5 -4
  72. package/dist/domain/processComparison.js +6 -4
  73. package/dist/domain/reconstructionVerification.js +1 -1
  74. package/dist/domain/reconstructionVerificationSchemas.js +1 -0
  75. package/dist/domain/staticRuntimeCorrelation.js +5 -1
  76. package/dist/generatedPackageMetadata.js +9 -0
  77. package/dist/hopper/BridgeLauncher.js +51 -20
  78. package/dist/hopper/HopperClient.js +45 -3
  79. package/dist/hopper/HopperProvider.js +1 -0
  80. package/dist/identity.js +10 -3
  81. package/dist/logger.js +2 -2
  82. package/dist/main.js +139 -28
  83. package/dist/server/createServer.js +54 -5
  84. package/dist/server/mcpProgress.js +23 -0
  85. package/dist/server/promptCompletion.js +144 -0
  86. package/dist/server/registerArtifactComparisonTool.js +6 -2
  87. package/dist/server/registerBundleComparisonTool.js +6 -2
  88. package/dist/server/registerEnhancedTools.js +20 -7
  89. package/dist/server/registerEvidenceResources.js +302 -0
  90. package/dist/server/registerEvidenceTools.js +59 -8
  91. package/dist/server/registerFunctionComparisonTool.js +7 -3
  92. package/dist/server/registerInvestigationTools.js +111 -18
  93. package/dist/server/registerOfficialTools.js +25 -14
  94. package/dist/server/registerProcessComparisonTool.js +8 -10
  95. package/dist/server/registerPrompts.js +67 -0
  96. package/dist/server/registerSessionStatusTool.js +52 -0
  97. package/dist/server/registerSessionTools.js +178 -22
  98. package/dist/server/runDerivedOperation.js +35 -0
  99. package/dist/server/sessionEvidence.js +2 -8
  100. package/dist/server/sessionToolPolicies.js +1 -42
  101. package/dist/server/toolResult.js +34 -6
  102. package/dist/serverIdentity.js +70 -0
  103. package/install.sh +11 -11
  104. package/package.json +10 -6
  105. package/scripts/hopper-demo-x11.py +349 -0
  106. package/scripts/rea.mjs +6 -3
  107. package/skills/rea-analysis/SKILL.md +9 -2
@@ -0,0 +1,358 @@
1
+ import { compareArtifacts } from "../domain/artifactComparison.js";
2
+ import { findChangedBehavior } from "../domain/changedBehavior.js";
3
+ import { createEvidence } from "../domain/evidence.js";
4
+ import { createEvidenceBundle, } from "../domain/evidenceBundle.js";
5
+ 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";
8
+ import { err, ok } from "../domain/result.js";
9
+ import { AUTOMATIC_RUN_LIMITATION, createInventoryEvidencePages, runLimitations, scanVersions, targetFor, } from "./CrossVersionInventory.js";
10
+ import { readInvestigationWorkspace, writeInvestigationWorkspace, } from "./InvestigationWorkspaceStore.js";
11
+ import { ARTIFACT_COMPARISON_PROVIDER, CHANGED_BEHAVIOR_PROVIDER, } from "./InvestigationProviders.js";
12
+ /** Run or resume a deterministic cross-version artifact investigation. */
13
+ export const runCrossVersionInvestigation = async (input, policy, execution) => {
14
+ const parsed = crossVersionInvestigationInputSchema.safeParse(input);
15
+ if (!parsed.success)
16
+ return err(new AnalysisInputError("find_changed_behavior", {
17
+ cause: parsed.error,
18
+ }));
19
+ const initial = await readInvestigationWorkspace(parsed.data.workspace_path, policy);
20
+ if (!initial.ok)
21
+ return initial;
22
+ const preflight = validateWorkspaceRequest(initial.value, parsed.data);
23
+ if (!preflight.ok)
24
+ return preflight;
25
+ if (isCancelled(execution.signal))
26
+ return err(new AnalysisCancelledError("find_changed_behavior"));
27
+ await execution.progress?.report({
28
+ phase: "scan_versions",
29
+ completed: 0,
30
+ total: 4,
31
+ message: "Scanning both version inputs",
32
+ });
33
+ const snapshots = await scanVersions(parsed.data, execution.inputRoots, execution.signal, execution.integrityContinueEnabled);
34
+ if (!snapshots.ok)
35
+ return snapshots;
36
+ await execution.progress?.report({
37
+ phase: "scan_versions",
38
+ completed: 1,
39
+ total: 4,
40
+ message: "Version inputs scanned",
41
+ });
42
+ return continueInvestigation({
43
+ input: parsed.data,
44
+ initial: initial.value,
45
+ snapshots: snapshots.value,
46
+ policy,
47
+ session: execution.session,
48
+ signal: execution.signal,
49
+ progress: execution.progress,
50
+ });
51
+ };
52
+ const continueInvestigation = async (context) => {
53
+ const left = targetFor(context.snapshots.left);
54
+ const right = targetFor(context.snapshots.right);
55
+ const identity = createInvestigationRunIdentity({
56
+ left,
57
+ right,
58
+ options: context.input.options,
59
+ integrity_policy: context.input.integrity_policy,
60
+ integrity_continue_approved: context.input.integrity_continue_approved,
61
+ max_integrity_mismatches: context.input.max_integrity_mismatches,
62
+ });
63
+ const legacyIdentity = createLegacyInvestigationRunIdentity({
64
+ left,
65
+ right,
66
+ options: context.input.options,
67
+ });
68
+ const existing = context.initial?.runs.find((run) => run.run_id === identity.runId ||
69
+ (context.input.integrity_policy === "fail" &&
70
+ !context.input.integrity_continue_approved &&
71
+ context.input.max_integrity_mismatches === 10 &&
72
+ "legacy_request_identity" in run &&
73
+ run.max_integrity_mismatches === 10 &&
74
+ run.run_id === legacyIdentity.runId));
75
+ if (context.initial !== null && existing?.status === "complete")
76
+ return completeOutcome(context.initial, existing, true, context.session);
77
+ const pages = createInventoryEvidencePages(context.input, context.snapshots);
78
+ if (!pages.ok)
79
+ return pages;
80
+ if (isCancelled(context.signal))
81
+ return err(new AnalysisCancelledError("find_changed_behavior"));
82
+ const inventoried = await ensureInventoryCheckpoint({
83
+ input: context.input,
84
+ current: context.initial,
85
+ existing,
86
+ pages: pages.value,
87
+ snapshots: context.snapshots,
88
+ policy: context.policy,
89
+ });
90
+ if (!inventoried.ok)
91
+ return inventoried;
92
+ await context.progress?.report({
93
+ phase: "inventory_versions",
94
+ completed: 2,
95
+ total: 4,
96
+ message: "Both artifact inventories checkpointed",
97
+ });
98
+ if (isCancelled(context.signal))
99
+ return err(new AnalysisCancelledError("find_changed_behavior"));
100
+ const compared = await ensureComparisonCheckpoint(context.input, inventoried.value, context.policy);
101
+ if (!compared.ok)
102
+ return compared;
103
+ await context.progress?.report({
104
+ phase: "compare_artifacts",
105
+ completed: 3,
106
+ total: 4,
107
+ message: "Artifact comparison checkpointed",
108
+ });
109
+ if (isCancelled(context.signal))
110
+ return err(new AnalysisCancelledError("find_changed_behavior"));
111
+ const finalized = await finalizeInvestigation(context.input, compared.value, context.policy, context.session);
112
+ await context.progress?.report({
113
+ phase: "changed_behavior",
114
+ completed: 4,
115
+ total: 4,
116
+ message: finalized.ok ? "Investigation completed" : "Investigation failed",
117
+ terminal: true,
118
+ });
119
+ return finalized;
120
+ };
121
+ const isCancelled = (signal) => signal?.aborted === true;
122
+ const ensureInventoryCheckpoint = async (input) => {
123
+ if (input.existing !== undefined) {
124
+ const consistent = validateInventoryCheckpoint(input.existing, input.pages);
125
+ if (!consistent.ok)
126
+ return consistent;
127
+ return input.current === null
128
+ ? err(new EvidenceIntegrityError("Investigation workspace is missing"))
129
+ : ok({ workspace: input.current, run: input.existing });
130
+ }
131
+ const left = targetFor(input.snapshots.left);
132
+ const right = targetFor(input.snapshots.right);
133
+ const identity = createInvestigationRunIdentity({
134
+ left,
135
+ right,
136
+ options: input.input.options,
137
+ integrity_policy: input.input.integrity_policy,
138
+ integrity_continue_approved: input.input.integrity_continue_approved,
139
+ max_integrity_mismatches: input.input.max_integrity_mismatches,
140
+ });
141
+ const run = investigationRunSchema.parse({
142
+ schema_version: 1,
143
+ run_id: identity.runId,
144
+ request_sha256: identity.requestSha256,
145
+ left,
146
+ right,
147
+ options: input.input.options,
148
+ integrity_policy: input.input.integrity_policy,
149
+ integrity_continue_approved: input.input.integrity_continue_approved,
150
+ max_integrity_mismatches: input.input.max_integrity_mismatches,
151
+ status: "running",
152
+ completed_stages: ["inventory_left", "inventory_right"],
153
+ left_inventory_evidence_ids: evidenceIds(input.pages.left),
154
+ right_inventory_evidence_ids: evidenceIds(input.pages.right),
155
+ comparison_evidence_id: null,
156
+ result_evidence_id: null,
157
+ limitations: runLimitations(input.snapshots),
158
+ });
159
+ const saved = await checkpoint({
160
+ path: input.input.workspace_path,
161
+ name: input.input.workspace_name,
162
+ current: input.current,
163
+ records: [...input.pages.left, ...input.pages.right],
164
+ run,
165
+ policy: input.policy,
166
+ });
167
+ return saved.ok ? ok({ workspace: saved.value, run }) : saved;
168
+ };
169
+ const ensureComparisonCheckpoint = async (input, state, policy) => {
170
+ if (state.run.comparison_evidence_id !== null)
171
+ return ok(state);
172
+ const inventory = inventoryEvidence(state.workspace, state.run);
173
+ if (!inventory.ok)
174
+ return inventory;
175
+ const evidence = createArtifactComparisonEvidence(inventory.value, input.options.change_limit);
176
+ const run = investigationRunSchema.parse({
177
+ ...state.run,
178
+ completed_stages: [
179
+ "inventory_left",
180
+ "inventory_right",
181
+ "compare_artifacts",
182
+ ],
183
+ comparison_evidence_id: evidence.evidence_id,
184
+ });
185
+ const saved = await checkpoint({
186
+ path: input.workspace_path,
187
+ name: input.workspace_name,
188
+ current: state.workspace,
189
+ records: [evidence],
190
+ run,
191
+ policy,
192
+ });
193
+ return saved.ok ? ok({ workspace: saved.value, run }) : saved;
194
+ };
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
+ const finalizeInvestigation = async (input, state, policy, session) => {
216
+ const comparison = comparisonEvidenceFor(state.workspace, state.run);
217
+ if (!comparison.ok)
218
+ return comparison;
219
+ const evidence = createChangedBehaviorEvidence(state, comparison.value);
220
+ const run = investigationRunSchema.parse({
221
+ ...state.run,
222
+ status: "complete",
223
+ completed_stages: [
224
+ "inventory_left",
225
+ "inventory_right",
226
+ "compare_artifacts",
227
+ "find_changed_behavior",
228
+ ],
229
+ result_evidence_id: evidence.evidence_id,
230
+ });
231
+ const saved = await checkpoint({
232
+ path: input.workspace_path,
233
+ name: input.workspace_name,
234
+ current: state.workspace,
235
+ records: [evidence],
236
+ run,
237
+ policy,
238
+ });
239
+ return saved.ok ? completeOutcome(saved.value, run, false, session) : saved;
240
+ };
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
+ const comparisonEvidenceFor = (workspace, run) => {
276
+ const evidence = workspace.bundle.records.find(({ evidence_id: id }) => id === run.comparison_evidence_id);
277
+ return evidence === undefined
278
+ ? err(new EvidenceIntegrityError("Investigation comparison checkpoint is missing Evidence"))
279
+ : ok(evidence);
280
+ };
281
+ const checkpoint = async (input) => {
282
+ const records = mergeEvidence(input.current?.bundle.records ?? [], input.records);
283
+ const bundle = createEvidenceBundle(records, input.current?.bundle.unknowns ?? []);
284
+ const runs = replaceRun(input.current?.runs ?? [], input.run);
285
+ const next = input.current === null
286
+ ? createInvestigationWorkspace(input.name, bundle, runs)
287
+ : reviseInvestigationWorkspace(input.current, bundle, runs);
288
+ const written = await writeInvestigationWorkspace(next, input.path, input.current?.revision ?? null, input.policy);
289
+ return written.ok ? ok(next) : written;
290
+ };
291
+ const completeOutcome = (workspace, run, reused, session) => {
292
+ if (run.result_evidence_id === null)
293
+ return err(new EvidenceIntegrityError("Completed investigation has no result"));
294
+ const evidence = workspace.bundle.records.find(({ evidence_id: id }) => id === run.result_evidence_id);
295
+ if (evidence === undefined)
296
+ return err(new EvidenceIntegrityError("Completed investigation result Evidence is missing"));
297
+ if (session !== undefined) {
298
+ const records = recordsForRun(workspace, run);
299
+ const imported = session.importEvidenceBundle(createEvidenceBundle(records));
300
+ if (!imported.ok)
301
+ return imported;
302
+ }
303
+ return ok({ evidence, workspace, reused });
304
+ };
305
+ const validateWorkspaceRequest = (workspace, input) => {
306
+ if (workspace !== null && workspace.name !== input.workspace_name)
307
+ return err(new InvestigationWorkspaceError("update", "name-conflict"));
308
+ if (input.expected_workspace_revision !== undefined &&
309
+ workspace?.revision !== input.expected_workspace_revision)
310
+ return err(new InvestigationWorkspaceError("update", "revision-conflict"));
311
+ return ok(null);
312
+ };
313
+ const validateInventoryCheckpoint = (run, pages) => JSON.stringify(run.left_inventory_evidence_ids) ===
314
+ JSON.stringify(evidenceIds(pages.left)) &&
315
+ JSON.stringify(run.right_inventory_evidence_ids) ===
316
+ JSON.stringify(evidenceIds(pages.right))
317
+ ? ok(null)
318
+ : err(new EvidenceIntegrityError("Investigation inventory checkpoint disagrees with current content"));
319
+ const inventoryEvidence = (workspace, run) => {
320
+ if (workspace === null)
321
+ return err(new EvidenceIntegrityError("Investigation workspace is missing"));
322
+ const byId = new Map(workspace.bundle.records.map((record) => [record.evidence_id, record]));
323
+ const left = run.left_inventory_evidence_ids.flatMap((id) => {
324
+ const evidence = byId.get(id);
325
+ return evidence === undefined ? [] : [evidence];
326
+ });
327
+ const right = run.right_inventory_evidence_ids.flatMap((id) => {
328
+ const evidence = byId.get(id);
329
+ return evidence === undefined ? [] : [evidence];
330
+ });
331
+ return left.length === run.left_inventory_evidence_ids.length &&
332
+ right.length === run.right_inventory_evidence_ids.length
333
+ ? ok({ left, right })
334
+ : err(new EvidenceIntegrityError("Investigation inventory checkpoint is incomplete"));
335
+ };
336
+ const recordsForRun = (workspace, run) => {
337
+ const ids = new Set([
338
+ ...run.left_inventory_evidence_ids,
339
+ ...run.right_inventory_evidence_ids,
340
+ ...(run.comparison_evidence_id === null
341
+ ? []
342
+ : [run.comparison_evidence_id]),
343
+ ...(run.result_evidence_id === null ? [] : [run.result_evidence_id]),
344
+ ]);
345
+ return workspace.bundle.records.filter(({ evidence_id: id }) => ids.has(id));
346
+ };
347
+ const mergeEvidence = (current, additions) => {
348
+ const records = new Map(current.map((record) => [record.evidence_id, record]));
349
+ for (const record of additions)
350
+ if (!records.has(record.evidence_id))
351
+ records.set(record.evidence_id, record);
352
+ return [...records.values()];
353
+ };
354
+ const replaceRun = (runs, replacement) => [
355
+ ...runs.filter(({ run_id: id }) => id !== replacement.run_id),
356
+ replacement,
357
+ ];
358
+ const evidenceIds = (records) => records.map(({ evidence_id: id }) => id);
@@ -1,8 +1,17 @@
1
1
  import { parseConfig } from "../config.js";
2
+ import { jsonObjectSchema } from "../domain/jsonValue.js";
3
+ import { createServerIdentity } from "../serverIdentity.js";
2
4
  import { EnhancedTools } from "./EnhancedTools.js";
3
5
  import { createBinarySession } from "./runtime.js";
4
6
  import { silentLogger } from "../logger.js";
5
7
  import { createEvidence } from "../domain/evidence.js";
8
+ import { EvidenceIntegrityError, AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "../domain/errors.js";
9
+ import { access } from "node:fs/promises";
10
+ import { readAnalysisSnapshot, writeAnalysisSnapshot, } from "./AnalysisSnapshotFiles.js";
11
+ import { parseBinaryTarget } from "../domain/binaryTarget.js";
12
+ import { snapshotEvidenceForQuery, snapshotMatchesTarget, } from "../domain/analysisSnapshot.js";
13
+ import { err, ok } from "../domain/result.js";
14
+ import { loadConfiguredPermissionAuthority } from "./PermissionConfiguration.js";
6
15
  const WORKFLOW_PROVIDER = {
7
16
  id: "rea-workflow",
8
17
  name: "REA composed investigation workflow",
@@ -13,37 +22,133 @@ const WORKFLOW_PROVIDER = {
13
22
  * Unlike MCP mode, every CLI invocation is intentionally isolated and does not
14
23
  * retain a target or bridge resources for a subsequent command.
15
24
  */
16
- export const runDirectAnalysis = async (path, tool, arguments_, logger = silentLogger) => runAnalysis(path, tool, arguments_, logger);
25
+ export const runDirectAnalysis = async (path, tool, arguments_, options = {}) => withProcessCancellation(options.signal, (signal) => runAnalysis(path, tool, arguments_, {
26
+ logger: options.logger ?? silentLogger,
27
+ snapshotPath: options.snapshotPath,
28
+ signal,
29
+ }));
17
30
  /** Execute one provider-native semantic operation with atomic provenance. */
18
- export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger) => runAnalysis(path, tool, arguments_, logger);
31
+ export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger, signal) => withProcessCancellation(signal, (operationSignal) => runAnalysis(path, tool, arguments_, {
32
+ logger,
33
+ snapshotPath: undefined,
34
+ signal: operationSignal,
35
+ }));
19
36
  /** Describe configured providers without opening a target or launching Hopper. */
20
37
  export const runSessionStatus = async (logger = silentLogger) => {
21
38
  const config = parseConfig(process.env);
22
39
  if (!config.ok)
23
- return { error: config.error._tag, message: config.error.message };
40
+ return cliError(config.error);
24
41
  const session = createBinarySession(config.value, logger);
25
42
  try {
26
- return session.status();
43
+ return {
44
+ ...jsonObjectSchema.parse(session.status()),
45
+ server_identity: createServerIdentity({
46
+ startedAt: new Date().toISOString(),
47
+ }),
48
+ };
27
49
  }
28
50
  finally {
29
51
  await session.close();
30
52
  }
31
53
  };
32
- const runAnalysis = async (path, tool, arguments_, logger) => {
54
+ const authorizeAnalysis = async (authority, tool, arguments_, snapshotPath) => {
55
+ 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
+ if (tool === "extract_artifact" && typeof arguments_.output_root === "string")
68
+ requests.push({
69
+ capability: "artifact_extract",
70
+ path: arguments_.output_root,
71
+ access: "write",
72
+ });
73
+ for (const request of requests) {
74
+ const result = await authority.authorize({
75
+ capability: request.capability,
76
+ roots: [request.path],
77
+ executables: [],
78
+ environment_names: [],
79
+ network: "none",
80
+ mount: false,
81
+ operation_identity: `${tool}:${request.capability}:${request.path}`,
82
+ }, request.access);
83
+ if (!result.ok)
84
+ return err(result.error instanceof PermissionRequiredError
85
+ ? result.error
86
+ : new AnalysisProtocolError(result.error.message, {
87
+ cause: result.error,
88
+ }));
89
+ }
90
+ if (tool === "inventory_artifact" &&
91
+ arguments_.native_mount_approved === true) {
92
+ const result = await authority.authorize({
93
+ capability: "native_mount",
94
+ roots: [],
95
+ executables: [],
96
+ environment_names: [],
97
+ network: "none",
98
+ mount: true,
99
+ operation_identity: "inventory_artifact:native_mount",
100
+ }, "read");
101
+ if (!result.ok)
102
+ return err(result.error instanceof PermissionRequiredError
103
+ ? result.error
104
+ : new AnalysisProtocolError(result.error.message, {
105
+ cause: result.error,
106
+ }));
107
+ }
108
+ return ok(null);
109
+ };
110
+ const runAnalysis = async (path, tool, arguments_, options) => {
111
+ const { logger, signal, snapshotPath } = options;
33
112
  const config = parseConfig(process.env);
34
113
  if (!config.ok)
35
- return { error: config.error._tag, message: config.error.message };
114
+ 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);
121
+ const unavailable = snapshotUnavailable(snapshotPath, config.value.hopperLoaderArgs);
122
+ if (unavailable !== undefined)
123
+ return cliError(unavailable);
36
124
  const session = createBinarySession(config.value, logger);
37
125
  try {
38
- const opened = await session.open(path);
126
+ const prepared = await prepareSnapshot({
127
+ path,
128
+ tool,
129
+ arguments: arguments_,
130
+ snapshotPath,
131
+ policy: config.value.analysisSnapshotFilePolicy,
132
+ provider: replayProviderFor(session, tool),
133
+ });
134
+ if (!prepared.ok)
135
+ return cliError(prepared.error);
136
+ if (prepared.value.evidence !== undefined)
137
+ return prepared.value.evidence;
138
+ const { snapshot } = prepared.value;
139
+ const opened = await session.open(path, snapshot === undefined ? { signal } : { signal, snapshot });
39
140
  if (!opened.ok)
40
- return { error: opened.error._tag, message: opened.error.message };
141
+ return cliError(opened.error);
142
+ let output;
143
+ let evidence;
41
144
  if (tool === "binary_overview" ||
42
145
  tool === "analyze_function" ||
43
146
  tool === "trace_feature") {
44
- const result = await new EnhancedTools(session).execute(tool, arguments_);
45
- return result.ok
46
- ? createEvidence(opened.value, tool === "analyze_function"
147
+ const result = await new EnhancedTools(session).execute(tool, arguments_, signal);
148
+ if (!result.ok)
149
+ output = cliError(result.error);
150
+ else {
151
+ evidence = createEvidence(opened.value, tool === "analyze_function"
47
152
  ? session.providerIdentity(tool)
48
153
  : WORKFLOW_PROVIDER, {
49
154
  operation: tool,
@@ -51,22 +156,98 @@ const runAnalysis = async (path, tool, arguments_, logger) => {
51
156
  result: result.value,
52
157
  confidence: "derived",
53
158
  limitations: ["Derived by an REA composed workflow."],
54
- })
55
- : { error: result.error._tag, message: result.error.message };
159
+ });
160
+ output = evidence;
161
+ }
56
162
  }
57
- const result = await session.execute(tool, arguments_);
58
- return result.ok
59
- ? createEvidence(result.value.subject ?? opened.value, result.value.provider, {
60
- operation: tool,
61
- parameters: arguments_,
62
- result: result.value.result,
63
- rawResult: result.value.rawResult,
64
- limitations: result.value.limitations,
65
- locations: result.value.locations,
66
- })
67
- : { error: result.error._tag, message: result.error.message };
163
+ else {
164
+ const result = await session.execute(tool, arguments_, { signal });
165
+ if (!result.ok)
166
+ output = cliError(result.error);
167
+ else {
168
+ evidence = createEvidence(result.value.subject ?? opened.value, result.value.provider, {
169
+ operation: tool,
170
+ parameters: arguments_,
171
+ result: result.value.result,
172
+ rawResult: result.value.rawResult,
173
+ limitations: result.value.limitations,
174
+ locations: result.value.locations,
175
+ });
176
+ output = evidence;
177
+ }
178
+ }
179
+ if (evidence !== undefined)
180
+ session.recordEvidence(evidence);
181
+ if (snapshotPath !== undefined && evidence !== undefined) {
182
+ const snapshot = session.exportAnalysisSnapshot();
183
+ if (!snapshot.ok)
184
+ return cliError(snapshot.error);
185
+ const written = await writeAnalysisSnapshot(snapshot.value, snapshotPath, true, config.value.analysisSnapshotFilePolicy);
186
+ if (!written.ok)
187
+ return cliError(written.error);
188
+ }
189
+ return output;
68
190
  }
69
191
  finally {
70
192
  await session.close();
71
193
  }
72
194
  };
195
+ const withProcessCancellation = async (suppliedSignal, operation) => {
196
+ if (suppliedSignal !== undefined)
197
+ return operation(suppliedSignal);
198
+ const controller = new AbortController();
199
+ const cancel = () => controller.abort();
200
+ process.once("SIGINT", cancel);
201
+ try {
202
+ return await operation(controller.signal);
203
+ }
204
+ finally {
205
+ process.off("SIGINT", cancel);
206
+ }
207
+ };
208
+ const prepareSnapshot = async (options) => {
209
+ const { path, tool, arguments: arguments_, snapshotPath, policy, provider, } = options;
210
+ if (snapshotPath === undefined || !(await fileExists(snapshotPath)))
211
+ return ok({});
212
+ const loaded = await readAnalysisSnapshot(snapshotPath, policy);
213
+ if (!loaded.ok)
214
+ return loaded;
215
+ const target = await parseBinaryTarget(path);
216
+ if (!target.ok)
217
+ return target;
218
+ if (!snapshotMatchesTarget(loaded.value.target, target.value))
219
+ return err(new EvidenceIntegrityError("Analysis snapshot target does not match the requested binary"));
220
+ const evidence = snapshotEvidenceForQuery(loaded.value, {
221
+ target: target.value,
222
+ operation: tool,
223
+ parameters: arguments_,
224
+ provider,
225
+ });
226
+ return ok(evidence === undefined
227
+ ? { snapshot: loaded.value }
228
+ : { snapshot: loaded.value, evidence });
229
+ };
230
+ const snapshotUnavailable = (snapshotPath, loaderArgs) => snapshotPath !== undefined && loaderArgs.length > 0
231
+ ? new EvidenceIntegrityError("Analysis snapshots are unavailable with custom Hopper loader arguments")
232
+ : undefined;
233
+ const replayProviderFor = (session, tool) => tool === "binary_overview" || tool === "trace_feature"
234
+ ? WORKFLOW_PROVIDER
235
+ : session.providerIdentity(tool);
236
+ const fileExists = async (path) => {
237
+ try {
238
+ await access(path);
239
+ return true;
240
+ }
241
+ catch (cause) {
242
+ if (typeof cause === "object" &&
243
+ cause !== null &&
244
+ "code" in cause &&
245
+ cause.code === "ENOENT")
246
+ return false;
247
+ throw cause;
248
+ }
249
+ };
250
+ const cliError = (error) => ({
251
+ error: "Analysis failed",
252
+ ...projectAnalysisError(error),
253
+ });