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.
- package/README.md +95 -27
- package/bridge/hopper_bridge.py +172 -14
- package/dist/application/AnalysisSnapshotCache.js +131 -0
- package/dist/application/AnalysisSnapshotFiles.js +31 -0
- package/dist/application/ArtifactGraphConstruction.js +1 -1
- package/dist/application/ArtifactInventory.js +100 -24
- package/dist/application/AuthorizedArtifactInventory.js +16 -0
- package/dist/application/BinarySession.js +149 -36
- package/dist/application/BinarySessionPort.js +1 -0
- package/dist/application/BoundedJsonFiles.js +109 -0
- package/dist/application/CapabilityInventory.js +127 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/CrossVersionInventory.js +109 -0
- package/dist/application/CrossVersionInvestigation.js +358 -0
- package/dist/application/DirectAnalysis.js +205 -24
- package/dist/application/Doctor.js +104 -3
- package/dist/application/EvidenceBundleFiles.js +15 -108
- package/dist/application/EvidenceLedger.js +3 -2
- package/dist/application/InvestigationProviders.js +48 -0
- package/dist/application/InvestigationWorkspaceStore.js +214 -0
- package/dist/application/LinuxHopper.js +72 -48
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureAuthority.js +2 -2
- package/dist/application/ProcessCaptureError.js +25 -0
- package/dist/application/ProcessCaptureLifecycle.js +22 -2
- package/dist/application/ProcessCli.js +111 -31
- package/dist/application/ProcessHarness.js +6 -3
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +110 -0
- package/dist/application/ReferenceSourceImportEntries.js +18 -3
- package/dist/application/ReferenceSourceImportTypes.js +32 -0
- package/dist/application/Setup.js +35 -101
- package/dist/application/SetupClients.js +19 -0
- package/dist/application/SetupInstallFailure.js +53 -0
- package/dist/application/SetupPlan.js +31 -0
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/Uninstall.js +34 -24
- package/dist/application/UnknownEvidence.js +33 -0
- package/dist/application/Upgrade.js +3 -1
- package/dist/application/runtime.js +2 -2
- package/dist/artifacts/ArtifactProvider.js +15 -8
- package/dist/catalogIdentity.js +121 -0
- package/dist/cli.js +76 -13
- package/dist/cliEvidenceCommands.js +52 -12
- package/dist/cliInvestigationCommands.js +137 -0
- package/dist/cliLogging.js +20 -3
- package/dist/cliOutput.js +41 -0
- package/dist/cliPolicyCommands.js +115 -0
- package/dist/config.js +102 -27
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/errorSchemas.js +98 -0
- package/dist/contracts/promptContracts.js +256 -0
- package/dist/contracts/sessionLifecycleInputs.js +11 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +20 -7
- package/dist/contracts/toolOutputSchemas.js +70 -9
- package/dist/domain/analysisSnapshot.js +149 -0
- package/dist/domain/artifactComparison.js +38 -11
- package/dist/domain/artifactGraph.js +25 -1
- package/dist/domain/artifactInventoryEvidence.js +18 -0
- package/dist/domain/changedBehavior.js +35 -10
- package/dist/domain/errors.js +384 -32
- package/dist/domain/evidence.js +4 -2
- package/dist/domain/evidenceBundle.js +28 -0
- package/dist/domain/hopperStartupFailure.js +55 -0
- package/dist/domain/investigationWorkspace.js +341 -0
- package/dist/domain/jsonValue.js +2 -0
- package/dist/domain/nativeInspection.js +3 -2
- package/dist/domain/permissionPolicy.js +157 -0
- package/dist/domain/processCapture.js +5 -4
- package/dist/domain/processComparison.js +6 -4
- package/dist/domain/reconstructionVerification.js +1 -1
- package/dist/domain/reconstructionVerificationSchemas.js +1 -0
- package/dist/domain/staticRuntimeCorrelation.js +5 -1
- package/dist/generatedPackageMetadata.js +9 -0
- package/dist/hopper/BridgeLauncher.js +51 -20
- package/dist/hopper/HopperClient.js +45 -3
- package/dist/hopper/HopperProvider.js +1 -0
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +139 -28
- package/dist/server/createServer.js +54 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/promptCompletion.js +144 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +20 -7
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +59 -8
- package/dist/server/registerFunctionComparisonTool.js +7 -3
- package/dist/server/registerInvestigationTools.js +111 -18
- package/dist/server/registerOfficialTools.js +25 -14
- package/dist/server/registerProcessComparisonTool.js +8 -10
- package/dist/server/registerPrompts.js +67 -0
- package/dist/server/registerSessionStatusTool.js +52 -0
- package/dist/server/registerSessionTools.js +178 -22
- package/dist/server/runDerivedOperation.js +35 -0
- package/dist/server/sessionEvidence.js +2 -8
- package/dist/server/sessionToolPolicies.js +1 -42
- package/dist/server/toolResult.js +34 -6
- package/dist/serverIdentity.js +70 -0
- package/install.sh +11 -11
- package/package.json +10 -6
- package/scripts/hopper-demo-x11.py +349 -0
- package/scripts/rea.mjs +6 -3
- 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_,
|
|
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_,
|
|
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
|
|
40
|
+
return cliError(config.error);
|
|
24
41
|
const session = createBinarySession(config.value, logger);
|
|
25
42
|
try {
|
|
26
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
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
|
-
|
|
159
|
+
});
|
|
160
|
+
output = evidence;
|
|
161
|
+
}
|
|
56
162
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
result
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
});
|