rea-agents 0.2.1 → 0.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 (122) hide show
  1. package/README.md +124 -20
  2. package/bridge/hopper_bridge.py +141 -13
  3. package/dist/application/AnalysisProvider.js +12 -1
  4. package/dist/application/ArtifactExtraction.js +166 -0
  5. package/dist/application/ArtifactGraphConstruction.js +257 -0
  6. package/dist/application/ArtifactInventory.js +253 -0
  7. package/dist/application/BinarySession.js +164 -14
  8. package/dist/application/CompositeProvider.js +73 -0
  9. package/dist/application/DirectAnalysis.js +27 -3
  10. package/dist/application/Doctor.js +35 -6
  11. package/dist/application/EnhancedTools.js +8 -7
  12. package/dist/application/EvidenceBundleCommands.js +29 -0
  13. package/dist/application/EvidenceBundleFiles.js +127 -0
  14. package/dist/application/EvidenceLedger.js +225 -18
  15. package/dist/application/FilesystemSnapshot.js +124 -0
  16. package/dist/application/LinuxHopper.js +186 -0
  17. package/dist/application/LoopbackReplay.js +195 -37
  18. package/dist/application/ProcessHarness.js +200 -191
  19. package/dist/application/ProcessNormalization.js +44 -0
  20. package/dist/application/ProcessOwnership.js +84 -0
  21. package/dist/application/ProcessSampling.js +284 -0
  22. package/dist/application/RealHopperAssertions.js +105 -0
  23. package/dist/application/ReferenceSourceImport.js +182 -0
  24. package/dist/application/ReferenceSourceImportEntries.js +122 -0
  25. package/dist/application/ReferenceSourceImportPolicy.js +73 -0
  26. package/dist/application/ReferenceSourceImportTypes.js +18 -0
  27. package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
  28. package/dist/application/Setup.js +192 -41
  29. package/dist/application/Uninstall.js +130 -0
  30. package/dist/application/runtime.js +8 -1
  31. package/dist/artifacts/ArtifactPaths.js +51 -0
  32. package/dist/artifacts/ArtifactProvider.js +130 -0
  33. package/dist/artifacts/ArtifactReader.js +9 -0
  34. package/dist/artifacts/AsarArtifactReader.js +62 -0
  35. package/dist/artifacts/DirectoryArtifactReader.js +107 -0
  36. package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
  37. package/dist/artifacts/SafeOutputTree.js +199 -0
  38. package/dist/artifacts/StreamBytes.js +10 -0
  39. package/dist/artifacts/ZipArtifactReader.js +109 -0
  40. package/dist/cli.js +153 -6
  41. package/dist/config.js +35 -1
  42. package/dist/contracts/artifactComparisonExample.js +95 -0
  43. package/dist/contracts/artifactToolContracts.js +84 -0
  44. package/dist/contracts/enhancedInputs.js +4 -0
  45. package/dist/contracts/functionComparisonExample.js +57 -0
  46. package/dist/contracts/investigationExamples.js +118 -0
  47. package/dist/contracts/nativeToolContracts.js +52 -0
  48. package/dist/contracts/processCaptureExample.js +25 -0
  49. package/dist/contracts/toolContractExamples.js +69 -0
  50. package/dist/contracts/toolContracts.js +99 -36
  51. package/dist/contracts/toolOutputSchemas.js +154 -82
  52. package/dist/contracts/unknownContractExamples.js +33 -0
  53. package/dist/domain/artifactComparison.js +273 -0
  54. package/dist/domain/artifactGraph.js +194 -0
  55. package/dist/domain/artifactInventoryEvidence.js +150 -0
  56. package/dist/domain/binaryTarget.js +55 -0
  57. package/dist/domain/bundleComparison.js +266 -0
  58. package/dist/domain/callPath.js +346 -0
  59. package/dist/domain/changedBehavior.js +294 -0
  60. package/dist/domain/errors.js +192 -4
  61. package/dist/domain/evidence.js +41 -10
  62. package/dist/domain/evidenceBundle.js +187 -6
  63. package/dist/domain/functionComparison.js +201 -0
  64. package/dist/domain/functionComparisonNormalization.js +112 -0
  65. package/dist/domain/functionComparisonResults.js +54 -0
  66. package/dist/domain/functionComparisonSchemas.js +82 -0
  67. package/dist/domain/functionDossierEvidence.js +171 -0
  68. package/dist/domain/hopperValues.js +14 -7
  69. package/dist/domain/nativeInspection.js +142 -0
  70. package/dist/domain/processCapture.js +152 -54
  71. package/dist/domain/processComparison.js +106 -0
  72. package/dist/domain/reconstructionUnknowns.js +90 -0
  73. package/dist/domain/reconstructionVerification.js +285 -0
  74. package/dist/domain/reconstructionVerificationSchemas.js +126 -0
  75. package/dist/domain/referenceSourceClassification.js +496 -0
  76. package/dist/domain/referenceSourceGraph.js +376 -0
  77. package/dist/domain/referenceSourceImportParsing.js +235 -0
  78. package/dist/domain/residualUnknown.js +239 -0
  79. package/dist/domain/staticRuntimeCorrelation.js +375 -0
  80. package/dist/hopper/BridgeLauncher.js +40 -3
  81. package/dist/hopper/HopperClient.js +14 -5
  82. package/dist/hopper/HopperProvider.js +57 -22
  83. package/dist/identity.js +1 -0
  84. package/dist/main.js +5 -1
  85. package/dist/native/CommandRunner.js +156 -0
  86. package/dist/native/NativeMacOSProvider.js +306 -0
  87. package/dist/native/NativeMachoInspection.js +135 -0
  88. package/dist/native/parsers/codesign.js +55 -0
  89. package/dist/native/parsers/demangle.js +26 -0
  90. package/dist/native/parsers/dyldInfo.js +25 -0
  91. package/dist/native/parsers/lipo.js +67 -0
  92. package/dist/native/parsers/otool.js +193 -0
  93. package/dist/native/parsers/plist.js +23 -0
  94. package/dist/reference/ReferenceSourceReader.js +73 -0
  95. package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
  96. package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
  97. package/dist/reference/ReferenceSourceReaderFile.js +119 -0
  98. package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
  99. package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
  100. package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
  101. package/dist/server/createServer.js +31 -5
  102. package/dist/server/recordDerivedEvidence.js +10 -0
  103. package/dist/server/registerArtifactComparisonTool.js +62 -0
  104. package/dist/server/registerArtifactTools.js +6 -0
  105. package/dist/server/registerBundleComparisonTool.js +47 -0
  106. package/dist/server/registerEnhancedTools.js +64 -12
  107. package/dist/server/registerEvidenceTools.js +36 -0
  108. package/dist/server/registerFunctionComparisonTool.js +68 -0
  109. package/dist/server/registerInvestigationTools.js +224 -0
  110. package/dist/server/registerNativeTools.js +6 -0
  111. package/dist/server/registerOfficialTools.js +47 -14
  112. package/dist/server/registerProcessComparisonTool.js +106 -0
  113. package/dist/server/registerSessionTools.js +179 -70
  114. package/dist/server/sessionEvidence.js +28 -0
  115. package/dist/server/sessionToolPolicies.js +64 -0
  116. package/dist/server/toolRegistrationOptions.js +7 -0
  117. package/dist/server/toolResult.js +8 -5
  118. package/install.sh +198 -0
  119. package/package.json +19 -2
  120. package/scripts/rea.mjs +5 -1
  121. package/skills/rea-analysis/SKILL.md +77 -2
  122. /package/dist/{application/HopperToolPort.js → domain/referenceSourcePolicy.js} +0 -0
@@ -0,0 +1,239 @@
1
+ import { createHash } from "node:crypto";
2
+ import canonicalize from "canonicalize";
3
+ import { z } from "zod";
4
+ const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
5
+ const unknownIdSchema = z.string().regex(/^unk_[a-f0-9]{64}$/u);
6
+ const boundedText = z.string().trim().min(1).max(1_000);
7
+ const environmentRequirementSchema = z.object({
8
+ id: z.string().min(1).max(200).nullable(),
9
+ platform: z.string().min(1).max(100).nullable(),
10
+ architecture: z.string().min(1).max(100).nullable(),
11
+ isolation: z
12
+ .enum(["none", "process", "container", "virtual-machine"])
13
+ .nullable(),
14
+ });
15
+ const probeSchema = z.object({
16
+ operation: z.string().min(1).max(200),
17
+ rationale: boundedText,
18
+ });
19
+ const relationshipSchema = z.object({
20
+ type: z.enum(["depends-on", "related-to", "contradicts"]),
21
+ unknown_id: unknownIdSchema,
22
+ });
23
+ const resolutionSchema = z.discriminatedUnion("disposition", [
24
+ z.object({
25
+ disposition: z.literal("verified"),
26
+ rationale: boundedText,
27
+ evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
28
+ }),
29
+ z.object({
30
+ disposition: z.enum(["withdrawn", "out-of-scope"]),
31
+ rationale: boundedText,
32
+ evidence_ids: z.array(evidenceIdSchema).max(100),
33
+ }),
34
+ ]);
35
+ /** Strict durable record for one unresolved or evidence-qualified question. */
36
+ const residualUnknownObjectSchema = z.object({
37
+ registry_version: z.literal(1),
38
+ unknown_id: unknownIdSchema,
39
+ revision: z.number().int().min(1),
40
+ previous_revision_digest: z
41
+ .string()
42
+ .regex(/^[a-f0-9]{64}$/u)
43
+ .nullable(),
44
+ revision_digest: z.string().regex(/^[a-f0-9]{64}$/u),
45
+ scope_digest: z
46
+ .string()
47
+ .regex(/^[a-f0-9]{64}$/u)
48
+ .nullable(),
49
+ question: boundedText,
50
+ status: z.enum([
51
+ "open",
52
+ "investigating",
53
+ "blocked",
54
+ "contradicted",
55
+ "resolved",
56
+ ]),
57
+ severity: z.enum(["low", "medium", "high", "critical"]),
58
+ domain: z.string().trim().min(1).max(100),
59
+ supporting_evidence_ids: z.array(evidenceIdSchema).max(100),
60
+ contradicting_evidence_ids: z.array(evidenceIdSchema).max(100),
61
+ required_authority: z
62
+ .enum([
63
+ "shipped-artifact",
64
+ "controlled-replay",
65
+ "historical-reference",
66
+ "external-service",
67
+ "analyst-inference",
68
+ ])
69
+ .nullable(),
70
+ required_confidence: z.enum(["observed", "derived", "inferred"]),
71
+ required_environment: environmentRequirementSchema.nullable(),
72
+ recommended_probes: z.array(probeSchema).max(20),
73
+ relationships: z.array(relationshipSchema).max(100),
74
+ resolution: resolutionSchema.nullable(),
75
+ mutation_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
76
+ });
77
+ export const residualUnknownSchema = residualUnknownObjectSchema.superRefine((value, context) => {
78
+ if (computeRevisionDigest(value) !== value.revision_digest)
79
+ context.addIssue({
80
+ code: "custom",
81
+ message: "Residual unknown revision digest does not match",
82
+ path: ["revision_digest"],
83
+ });
84
+ if ((value.revision === 1) !== (value.previous_revision_digest === null))
85
+ context.addIssue({
86
+ code: "custom",
87
+ message: "Only the first revision has no predecessor",
88
+ path: ["previous_revision_digest"],
89
+ });
90
+ if ((value.status === "resolved") !== (value.resolution !== null))
91
+ context.addIssue({
92
+ code: "custom",
93
+ message: "Only resolved unknowns carry a resolution",
94
+ path: ["resolution"],
95
+ });
96
+ if (value.status === "contradicted" &&
97
+ value.contradicting_evidence_ids.length === 0)
98
+ context.addIssue({
99
+ code: "custom",
100
+ message: "Contradicted status requires contradicting evidence",
101
+ path: ["contradicting_evidence_ids"],
102
+ });
103
+ if (value.relationships.some((relationship) => relationship.unknown_id === value.unknown_id))
104
+ context.addIssue({
105
+ code: "custom",
106
+ message: "An unknown cannot relate to itself",
107
+ path: ["relationships"],
108
+ });
109
+ for (const ids of [
110
+ value.supporting_evidence_ids,
111
+ value.contradicting_evidence_ids,
112
+ value.mutation_evidence_ids,
113
+ ])
114
+ if (new Set(ids).size !== ids.length)
115
+ context.addIssue({
116
+ code: "custom",
117
+ message: "Evidence IDs must be unique",
118
+ });
119
+ if (value.supporting_evidence_ids.some((id) => value.contradicting_evidence_ids.includes(id)))
120
+ context.addIssue({
121
+ code: "custom",
122
+ message: "Evidence cannot simultaneously support and contradict",
123
+ });
124
+ const relationships = value.relationships.map((relationship) => `${relationship.type}:${relationship.unknown_id}`);
125
+ if (new Set(relationships).size !== relationships.length)
126
+ context.addIssue({
127
+ code: "custom",
128
+ message: "Relationships must be unique",
129
+ path: ["relationships"],
130
+ });
131
+ });
132
+ /** Input for deterministic creation of one residual unknown. */
133
+ export const recordUnknownInputSchema = residualUnknownObjectSchema
134
+ .omit({
135
+ registry_version: true,
136
+ unknown_id: true,
137
+ revision: true,
138
+ previous_revision_digest: true,
139
+ revision_digest: true,
140
+ scope_digest: true,
141
+ status: true,
142
+ supporting_evidence_ids: true,
143
+ contradicting_evidence_ids: true,
144
+ resolution: true,
145
+ mutation_evidence_ids: true,
146
+ })
147
+ .extend({
148
+ supporting_evidence_ids: z.array(evidenceIdSchema).max(100).default([]),
149
+ contradicting_evidence_ids: z.array(evidenceIdSchema).max(100).default([]),
150
+ approved: z.literal(true),
151
+ });
152
+ /** Explicit optimistic-concurrency update command for one unknown. */
153
+ export const updateUnknownInputSchema = z.object({
154
+ unknown_id: unknownIdSchema,
155
+ expected_revision: z.number().int().min(1),
156
+ approved: z.literal(true),
157
+ status: residualUnknownObjectSchema.shape.status,
158
+ severity: residualUnknownObjectSchema.shape.severity,
159
+ supporting_evidence_ids: z.array(evidenceIdSchema).max(100),
160
+ contradicting_evidence_ids: z.array(evidenceIdSchema).max(100),
161
+ required_authority: residualUnknownObjectSchema.shape.required_authority,
162
+ required_confidence: residualUnknownObjectSchema.shape.required_confidence,
163
+ required_environment: environmentRequirementSchema.nullable(),
164
+ recommended_probes: z.array(probeSchema).max(20),
165
+ relationships: z.array(relationshipSchema).max(100),
166
+ resolution: resolutionSchema.nullable(),
167
+ });
168
+ /** Create stable identity from immutable question and domain fields. */
169
+ export const createResidualUnknown = (input, mutationEvidenceId, scopeDigest) => {
170
+ const question = input.question.replace(/\s+/gu, " ");
171
+ const identity = canonicalJson({
172
+ domain: input.domain,
173
+ question,
174
+ scope_digest: scopeDigest,
175
+ });
176
+ const revision = {
177
+ registry_version: 1,
178
+ unknown_id: `unk_${createHash("sha256").update(identity).digest("hex")}`,
179
+ revision: 1,
180
+ previous_revision_digest: null,
181
+ scope_digest: scopeDigest,
182
+ question,
183
+ status: input.contradicting_evidence_ids.length > 0 ? "contradicted" : "open",
184
+ domain: input.domain,
185
+ ...projectRevisionDetails(input),
186
+ resolution: null,
187
+ mutation_evidence_ids: [mutationEvidenceId],
188
+ };
189
+ return residualUnknownSchema.parse({
190
+ ...revision,
191
+ revision_digest: computeRevisionDigest(revision),
192
+ });
193
+ };
194
+ /** Apply a complete, revision-guarded replacement while preserving identity. */
195
+ export const updateResidualUnknown = (current, input, mutationEvidenceId) => {
196
+ const revision = {
197
+ registry_version: 1,
198
+ unknown_id: current.unknown_id,
199
+ revision: current.revision + 1,
200
+ previous_revision_digest: current.revision_digest,
201
+ scope_digest: current.scope_digest,
202
+ question: current.question,
203
+ status: input.status,
204
+ domain: current.domain,
205
+ ...projectRevisionDetails(input),
206
+ resolution: input.resolution,
207
+ mutation_evidence_ids: sortedUnique([
208
+ ...current.mutation_evidence_ids,
209
+ mutationEvidenceId,
210
+ ]),
211
+ };
212
+ return residualUnknownSchema.parse({
213
+ ...revision,
214
+ revision_digest: computeRevisionDigest(revision),
215
+ });
216
+ };
217
+ const projectRevisionDetails = (input) => ({
218
+ severity: input.severity,
219
+ supporting_evidence_ids: sortedUnique(input.supporting_evidence_ids),
220
+ contradicting_evidence_ids: sortedUnique(input.contradicting_evidence_ids),
221
+ required_authority: input.required_authority,
222
+ required_confidence: input.required_confidence,
223
+ required_environment: input.required_environment,
224
+ recommended_probes: [...input.recommended_probes],
225
+ relationships: sortedRelationships(input.relationships),
226
+ });
227
+ const computeRevisionDigest = (value) => {
228
+ const { revision_digest: _revisionDigest, ...semantic } = "revision_digest" in value ? value : { ...value, revision_digest: "" };
229
+ void _revisionDigest;
230
+ return createHash("sha256").update(canonicalJson(semantic)).digest("hex");
231
+ };
232
+ const canonicalJson = (value) => {
233
+ const serialized = canonicalize(value);
234
+ if (serialized === undefined)
235
+ throw new TypeError("Unknown identity failed");
236
+ return serialized;
237
+ };
238
+ const sortedUnique = (values) => [...new Set(values)].sort((left, right) => left.localeCompare(right));
239
+ const sortedRelationships = (values) => [...values].sort((left, right) => `${left.type}:${left.unknown_id}`.localeCompare(`${right.type}:${right.unknown_id}`));
@@ -0,0 +1,375 @@
1
+ import { createHash } from "node:crypto";
2
+ import canonicalize from "canonicalize";
3
+ import { z } from "zod";
4
+ import { artifactComparisonResultSchema } from "./artifactComparison.js";
5
+ import { evidenceSchema, parseEvidence } from "./evidence.js";
6
+ import { functionComparisonResultSchema } from "./functionComparison.js";
7
+ import { comparisonStatusSchema, processCaptureComparisonSchema, } from "./processCapture.js";
8
+ const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
9
+ const functionDimensionSchema = z.enum([
10
+ "identity",
11
+ "pseudocode",
12
+ "assembly",
13
+ "comments",
14
+ "calls",
15
+ "references",
16
+ "strings_names",
17
+ "cfg",
18
+ ]);
19
+ const runtimeDimensionSchema = z.enum([
20
+ "terminal",
21
+ "exit",
22
+ "filesystem",
23
+ "protocol",
24
+ "process",
25
+ ]);
26
+ const expectedPatternSchema = z.enum([
27
+ "cochanged",
28
+ "static_only",
29
+ "runtime_only",
30
+ "both_unchanged",
31
+ ]);
32
+ const observedPatternSchema = z.enum([
33
+ ...expectedPatternSchema.options,
34
+ "unknown",
35
+ "truncated",
36
+ ]);
37
+ const artifactSelectorSchema = z
38
+ .object({
39
+ kind: z.literal("artifact"),
40
+ logical_path: z.string().min(1).max(4_096),
41
+ })
42
+ .strict();
43
+ const functionSelectorSchema = z
44
+ .object({ kind: z.literal("function"), dimension: functionDimensionSchema })
45
+ .strict();
46
+ const staticSelectorSchema = z.discriminatedUnion("kind", [
47
+ artifactSelectorSchema,
48
+ functionSelectorSchema,
49
+ ]);
50
+ const mappingSchema = z
51
+ .object({
52
+ static: z
53
+ .object({
54
+ comparison_evidence_id: evidenceIdSchema,
55
+ selector: staticSelectorSchema,
56
+ })
57
+ .strict(),
58
+ runtime: z
59
+ .object({
60
+ comparison_evidence_id: evidenceIdSchema,
61
+ dimension: runtimeDimensionSchema,
62
+ })
63
+ .strict(),
64
+ side_alignment: z.enum(["left_to_left", "left_to_right"]),
65
+ hypothesis: z
66
+ .object({
67
+ statement: z.string().trim().min(1).max(500),
68
+ expected_pattern: expectedPatternSchema,
69
+ })
70
+ .strict(),
71
+ })
72
+ .strict();
73
+ /** Strict bounded input for explicit static/runtime hypothesis correlation. */
74
+ export const staticRuntimeCorrelationInputSchema = z
75
+ .object({
76
+ static_comparisons: z.array(evidenceSchema).min(1).max(100),
77
+ runtime_comparisons: z.array(evidenceSchema).min(1).max(100),
78
+ mappings: z.array(mappingSchema).min(1).max(500),
79
+ offset: z.number().int().min(0).default(0),
80
+ limit: z.number().int().min(1).max(100).default(100),
81
+ unknown_registry_approved: z.literal(true).optional(),
82
+ })
83
+ .strict();
84
+ const correlationItemSchema = z.object({
85
+ correlation_id: z.string().regex(/^cor_[a-f0-9]{64}$/u),
86
+ static: z.object({
87
+ comparison_evidence_id: evidenceIdSchema,
88
+ selector: staticSelectorSchema,
89
+ status: comparisonStatusSchema,
90
+ conclusion_kind: z.enum([
91
+ "derived_relationship",
92
+ "contradiction",
93
+ "unresolved_branch",
94
+ ]),
95
+ }),
96
+ runtime: z.object({
97
+ comparison_evidence_id: evidenceIdSchema,
98
+ dimension: runtimeDimensionSchema,
99
+ status: comparisonStatusSchema,
100
+ }),
101
+ side_alignment: z.enum(["left_to_left", "left_to_right"]),
102
+ hypothesis: z.object({
103
+ statement: z.string().min(1).max(500),
104
+ expected_pattern: expectedPatternSchema,
105
+ }),
106
+ observed_pattern: observedPatternSchema,
107
+ classification: z.enum(["hypothesis", "contradiction", "unresolved_branch"]),
108
+ evidence_links: z.array(evidenceIdSchema).min(2).max(20_100),
109
+ limitations: z.array(z.string()),
110
+ });
111
+ /** Deterministic, paginated static/runtime correlation payload. */
112
+ export const staticRuntimeCorrelationResultSchema = z.object({
113
+ status: z.enum(["correlated", "contradicted", "unknown", "truncated"]),
114
+ summary: z.object({
115
+ hypotheses: z.number().int().min(0),
116
+ contradictions: z.number().int().min(0),
117
+ unresolved: z.number().int().min(0),
118
+ }),
119
+ correlations: z.object({
120
+ items: z.array(correlationItemSchema).max(100),
121
+ offset: z.number().int().min(0),
122
+ limit: z.number().int().min(1).max(100),
123
+ total: z.number().int().min(0),
124
+ next_offset: z.number().int().min(0).nullable(),
125
+ }),
126
+ evidence_links: z.array(evidenceIdSchema).min(2).max(20_100),
127
+ limitations: z.array(z.string()),
128
+ });
129
+ const EXPECTED = {
130
+ compare_artifacts: [
131
+ "rea.artifact-comparison/v1",
132
+ "rea-artifact-comparison",
133
+ "REA artifact comparison",
134
+ ],
135
+ compare_functions: [
136
+ "rea.function-comparison/v1",
137
+ "rea-function-comparison",
138
+ "REA function comparison",
139
+ ],
140
+ compare_process_captures: [
141
+ "rea.process-comparison/v1",
142
+ "rea-process",
143
+ "REA deterministic process harness",
144
+ ],
145
+ };
146
+ /** Correlate only caller-declared static/runtime hypotheses. */
147
+ export const correlateStaticAndRuntime = (input) => {
148
+ const parsed = staticRuntimeCorrelationInputSchema.parse(input);
149
+ const staticEvidence = parsed.static_comparisons.map(parseStatic);
150
+ const runtimeEvidence = parsed.runtime_comparisons.map(parseRuntime);
151
+ assertUnique(staticEvidence, "static comparison Evidence");
152
+ assertUnique(runtimeEvidence, "runtime comparison Evidence");
153
+ const staticById = byId(staticEvidence);
154
+ const runtimeById = byId(runtimeEvidence);
155
+ const correlations = parsed.mappings
156
+ .map((mapping) => correlate(mapping, staticById, runtimeById))
157
+ .sort((left, right) => left.correlation_id.localeCompare(right.correlation_id, "en"));
158
+ if (new Set(correlations.map(({ correlation_id: id }) => id)).size !==
159
+ correlations.length)
160
+ throw new TypeError("Static/runtime correlation rejects duplicate mappings");
161
+ const page = correlations.slice(parsed.offset, parsed.offset + parsed.limit);
162
+ const links = uniqueSorted(correlations.flatMap(({ evidence_links: evidenceLinks }) => evidenceLinks));
163
+ if (links.length > 20_100)
164
+ throw new TypeError("Static/runtime Evidence closure exceeds limit");
165
+ const unresolved = correlations.filter(({ classification }) => classification === "unresolved_branch");
166
+ const contradictions = correlations.filter(({ classification }) => classification === "contradiction");
167
+ return staticRuntimeCorrelationResultSchema.parse({
168
+ status: correlations.some(({ observed_pattern: pattern }) => pattern === "truncated")
169
+ ? "truncated"
170
+ : unresolved.length > 0
171
+ ? "unknown"
172
+ : contradictions.length > 0
173
+ ? "contradicted"
174
+ : "correlated",
175
+ summary: {
176
+ hypotheses: correlations.length - contradictions.length - unresolved.length,
177
+ contradictions: contradictions.length,
178
+ unresolved: unresolved.length,
179
+ },
180
+ correlations: {
181
+ items: page,
182
+ offset: parsed.offset,
183
+ limit: parsed.limit,
184
+ total: correlations.length,
185
+ next_offset: parsed.offset + page.length < correlations.length
186
+ ? parsed.offset + page.length
187
+ : null,
188
+ },
189
+ evidence_links: links,
190
+ limitations: [
191
+ "Mappings are caller-declared hypotheses; consistency does not establish causality.",
192
+ "Only explicit mappings were evaluated; similar unmapped observations were not correlated.",
193
+ ],
194
+ });
195
+ };
196
+ const correlate = (mapping, staticById, runtimeById) => {
197
+ const staticEvidence = required(staticById, mapping.static.comparison_evidence_id, "static comparison");
198
+ const runtimeEvidence = required(runtimeById, mapping.runtime.comparison_evidence_id, "runtime comparison");
199
+ const selectedStatic = selectStatic(staticEvidence, mapping.static.selector);
200
+ const process = processCaptureComparisonSchema.parse(runtimeEvidence.normalized_result);
201
+ const runtimeStatus = process[mapping.runtime.dimension];
202
+ const pattern = observedPattern(selectedStatic.status, runtimeStatus);
203
+ const classification = pattern === "unknown" || pattern === "truncated"
204
+ ? "unresolved_branch"
205
+ : pattern === mapping.hypothesis.expected_pattern
206
+ ? "hypothesis"
207
+ : "contradiction";
208
+ const projection = {
209
+ static: mapping.static,
210
+ runtime: mapping.runtime,
211
+ side_alignment: mapping.side_alignment,
212
+ hypothesis: mapping.hypothesis,
213
+ };
214
+ return correlationItemSchema.parse({
215
+ correlation_id: `cor_${sha256(canonicalJson(projection))}`,
216
+ static: { ...mapping.static, ...selectedStatic },
217
+ runtime: { ...mapping.runtime, status: runtimeStatus },
218
+ side_alignment: mapping.side_alignment,
219
+ hypothesis: mapping.hypothesis,
220
+ observed_pattern: pattern,
221
+ classification,
222
+ evidence_links: uniqueSorted([
223
+ staticEvidence.evidence_id,
224
+ ...staticEvidence.evidence_links,
225
+ runtimeEvidence.evidence_id,
226
+ ...runtimeEvidence.evidence_links,
227
+ ]),
228
+ limitations: uniqueSorted([
229
+ ...staticEvidence.limitations,
230
+ ...runtimeEvidence.limitations,
231
+ ...process.limitations,
232
+ "This correlation tests an explicit hypothesis and does not establish causality.",
233
+ "Side alignment is caller-declared and was not independently validated against source subjects or environments.",
234
+ ]),
235
+ });
236
+ };
237
+ const selectStatic = (evidence, selector) => {
238
+ if (selector.kind === "function") {
239
+ if (evidence.operation !== "compare_functions")
240
+ throw new TypeError("Function selector requires function comparison Evidence");
241
+ const result = functionComparisonResultSchema.parse(evidence.normalized_result);
242
+ const matches = result.dimensions.filter(({ dimension }) => dimension === selector.dimension);
243
+ if (matches.length !== 1)
244
+ throw new TypeError("Function selector must resolve exactly once");
245
+ return {
246
+ status: matches[0].status,
247
+ conclusion_kind: matches[0].conclusion_kind,
248
+ };
249
+ }
250
+ if (evidence.operation !== "compare_artifacts")
251
+ throw new TypeError("Artifact selector requires artifact comparison Evidence");
252
+ const result = artifactComparisonResultSchema.parse(evidence.normalized_result);
253
+ if (result.changes.offset !== 0 ||
254
+ result.changes.next_offset !== null ||
255
+ result.changes.items.length !== result.changes.total)
256
+ throw new TypeError("Static/runtime correlation requires complete artifact comparison pagination");
257
+ const matches = result.changes.items.filter(({ logical_path: path }) => path === selector.logical_path);
258
+ if (matches.length !== 1)
259
+ throw new TypeError("Artifact selector must resolve exactly one reported delta");
260
+ const status = matches[0].classification;
261
+ return {
262
+ status,
263
+ conclusion_kind: status === "unknown" ? "unresolved_branch" : "derived_relationship",
264
+ };
265
+ };
266
+ const parseStatic = (input) => {
267
+ const evidence = parseEvidence(input);
268
+ if (evidence.operation !== "compare_artifacts" &&
269
+ evidence.operation !== "compare_functions")
270
+ throw new TypeError("Static/runtime correlation requires artifact or function comparison Evidence");
271
+ assertComparisonIdentity(evidence);
272
+ if (evidence.operation === "compare_artifacts") {
273
+ const result = artifactComparisonResultSchema.parse(evidence.normalized_result);
274
+ assertCompleteArtifactResult(result);
275
+ assertNestedLinks(evidence, result.changes.items.flatMap(({ evidence_links: links }) => links));
276
+ }
277
+ else {
278
+ const result = functionComparisonResultSchema.parse(evidence.normalized_result);
279
+ assertNestedLinks(evidence, result.dimensions.flatMap(({ evidence_links: links }) => links));
280
+ }
281
+ return evidence;
282
+ };
283
+ const assertNestedLinks = (evidence, nestedLinks) => {
284
+ const closure = new Set(evidence.evidence_links);
285
+ if (nestedLinks.some((evidenceId) => !closure.has(evidenceId)))
286
+ throw new TypeError("Comparison finding cites Evidence outside its top-level closure");
287
+ };
288
+ const parseRuntime = (input) => {
289
+ const evidence = parseEvidence(input);
290
+ if (evidence.operation !== "compare_process_captures")
291
+ throw new TypeError("Static/runtime correlation requires process comparison Evidence");
292
+ assertComparisonIdentity(evidence);
293
+ processCaptureComparisonSchema.parse(evidence.normalized_result);
294
+ return evidence;
295
+ };
296
+ const assertComparisonIdentity = (evidence) => {
297
+ const expected = EXPECTED[evidence.operation];
298
+ if (expected === undefined ||
299
+ evidence.predicate_type !== expected[0] ||
300
+ evidence.provider.id !== expected[1] ||
301
+ evidence.provider.name !== expected[2] ||
302
+ evidence.provider.version !== "1" ||
303
+ evidence.confidence !== "derived" ||
304
+ evidence.authority !== "analyst-inference" ||
305
+ evidence.subject !== null ||
306
+ evidence.evidence_links.length < 2 ||
307
+ new Set(evidence.evidence_links).size !== evidence.evidence_links.length)
308
+ throw new TypeError("Comparison Evidence operation, predicate, provider, or citations disagree");
309
+ const parameterLinks = comparisonParameterLinks(evidence);
310
+ if (!sameSet(parameterLinks, evidence.evidence_links))
311
+ throw new TypeError("Comparison Evidence closure disagrees with its source parameters");
312
+ };
313
+ const comparisonParameterLinks = (evidence) => {
314
+ if (evidence.operation === "compare_process_captures") {
315
+ const parameters = z
316
+ .object({
317
+ left_evidence_id: evidenceIdSchema,
318
+ right_evidence_id: evidenceIdSchema,
319
+ })
320
+ .passthrough()
321
+ .parse(evidence.parameters);
322
+ return [parameters.left_evidence_id, parameters.right_evidence_id];
323
+ }
324
+ const parameters = z
325
+ .object({
326
+ left_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
327
+ right_evidence_ids: z.array(evidenceIdSchema).min(1).max(100),
328
+ })
329
+ .passthrough()
330
+ .parse(evidence.parameters);
331
+ return [...parameters.left_evidence_ids, ...parameters.right_evidence_ids];
332
+ };
333
+ const sameSet = (left, right) => left.length === right.length &&
334
+ new Set(left).size === left.length &&
335
+ left.every((item) => right.includes(item));
336
+ const assertCompleteArtifactResult = (result) => {
337
+ if (result.changes.offset !== 0 ||
338
+ result.changes.next_offset !== null ||
339
+ result.changes.items.length !== result.changes.total)
340
+ throw new TypeError("Static/runtime correlation requires complete artifact comparison pagination");
341
+ };
342
+ const observedPattern = (staticStatus, runtimeStatus) => {
343
+ if (staticStatus === "truncated" || runtimeStatus === "truncated")
344
+ return "truncated";
345
+ if (staticStatus === "unknown" || runtimeStatus === "unknown")
346
+ return "unknown";
347
+ const staticChanged = staticStatus !== "unchanged";
348
+ const runtimeChanged = runtimeStatus !== "unchanged";
349
+ if (staticChanged && runtimeChanged)
350
+ return "cochanged";
351
+ if (staticChanged)
352
+ return "static_only";
353
+ if (runtimeChanged)
354
+ return "runtime_only";
355
+ return "both_unchanged";
356
+ };
357
+ const required = (values, id, label) => {
358
+ const value = values.get(id);
359
+ if (value === undefined)
360
+ throw new TypeError(`Mapping references absent ${label} Evidence`);
361
+ return value;
362
+ };
363
+ const byId = (evidence) => new Map(evidence.map((item) => [item.evidence_id, item]));
364
+ const assertUnique = (evidence, label) => {
365
+ if (new Set(evidence.map(({ evidence_id: id }) => id)).size !== evidence.length)
366
+ throw new TypeError(`Static/runtime correlation rejects duplicate ${label}`);
367
+ };
368
+ const uniqueSorted = (values) => [...new Set(values)].sort((left, right) => left.localeCompare(right, "en"));
369
+ const sha256 = (value) => createHash("sha256").update(value).digest("hex");
370
+ const canonicalJson = (value) => {
371
+ const serialized = canonicalize(value);
372
+ if (serialized === undefined)
373
+ throw new TypeError("RFC 8785 canonicalization rejected correlation mapping");
374
+ return serialized;
375
+ };
@@ -5,6 +5,8 @@ import { basename, dirname, extname, join } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { HopperCancelledError, HopperStartError } from "../domain/errors.js";
7
7
  import { err, ok } from "../domain/result.js";
8
+ import { cleanupOwnedProcessGroup, } from "../application/ProcessOwnership.js";
9
+ import writeFileAtomic from "write-file-atomic";
8
10
  const execFileAsync = promisify(execFile);
9
11
  const HOPPER_BACKGROUND_STARTUP_MS = 5_000;
10
12
  /**
@@ -17,8 +19,9 @@ const HOPPER_BACKGROUND_STARTUP_MS = 5_000;
17
19
  * supported launcher; callers must treat possible foreground UI as an upstream
18
20
  * Hopper constraint, not as evidence that analysis failed.
19
21
  *
20
- * The returned process is the short-lived launcher helper, not the Hopper app,
21
- * so REA does not claim ownership of the application's lifetime.
22
+ * REA owns only the short-lived launcher helper's run-token-authenticated
23
+ * process group. It does not claim ownership of the Hopper GUI process that
24
+ * macOS LaunchServices may create or reuse.
22
25
  */
23
26
  export class HopperApplicationLauncher {
24
27
  options;
@@ -30,6 +33,7 @@ export class HopperApplicationLauncher {
30
33
  const source = [
31
34
  `REA_SOCKET = ${JSON.stringify(session.socketPath)}`,
32
35
  `REA_TOKEN = ${JSON.stringify(session.token)}`,
36
+ `REA_RUN_ID = ${JSON.stringify(session.runId)}`,
33
37
  `exec(compile(open(${JSON.stringify(this.options.bridgeScriptPath)}, 'rb').read(), ${JSON.stringify(this.options.bridgeScriptPath)}, 'exec'))`,
34
38
  "",
35
39
  ].join("\n");
@@ -57,6 +61,8 @@ export class HopperApplicationLauncher {
57
61
  return err(new HopperCancelledError());
58
62
  const child = spawn(this.options.launcherPath, args, {
59
63
  stdio: ["ignore", "ignore", "pipe"],
64
+ detached: process.platform !== "win32",
65
+ env: { ...process.env, REA_PROCESS_RUN_ID: session.runId },
60
66
  });
61
67
  const started = await new Promise((resolve) => {
62
68
  const onSpawn = () => {
@@ -72,7 +78,38 @@ export class HopperApplicationLauncher {
72
78
  });
73
79
  if (!started.ok)
74
80
  return started;
75
- return ok({ process: child, ownsProcessLifetime: false });
81
+ const pid = child.pid;
82
+ if (pid === undefined)
83
+ return err(new HopperStartError({ cause: new Error("missing launcher PID") }));
84
+ const ownership = {
85
+ schema_version: 1,
86
+ run_id: session.runId,
87
+ pid,
88
+ process_group_id: pid,
89
+ parent_pid: process.pid,
90
+ launcher: this.options.launcherPath,
91
+ created_at: new Date().toISOString(),
92
+ };
93
+ try {
94
+ await writeFileAtomic(join(session.directory, "ownership.json"), `${JSON.stringify(ownership)}\n`, { encoding: "utf8", mode: 0o600 });
95
+ }
96
+ catch (cause) {
97
+ await cleanupOwnedProcessGroup({
98
+ runId: session.runId,
99
+ leaderPid: pid,
100
+ processGroupId: pid,
101
+ });
102
+ return err(new HopperStartError({ cause }));
103
+ }
104
+ return ok({
105
+ process: child,
106
+ ownsProcessLifetime: true,
107
+ cleanup: () => cleanupOwnedProcessGroup({
108
+ runId: session.runId,
109
+ leaderPid: pid,
110
+ processGroupId: pid,
111
+ }),
112
+ });
76
113
  }
77
114
  catch (cause) {
78
115
  return err(new HopperStartError({ cause }));