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,201 @@
1
+ import { diffLines } from "diff";
2
+ import { parseFunctionEvidence, } from "./functionDossierEvidence.js";
3
+ import { commentProjection, combineCoverage, functionMatch, identityProjection, isAutoName, normalizeCfg, project, referenceProjection, sorted, stringAndNameProjection, } from "./functionComparisonNormalization.js";
4
+ import { functionComparisonResultSchema, } from "./functionComparisonSchemas.js";
5
+ import { canonicalJson, dimensionResult, overallStatus, summarize, unresolvedDimension as buildUnresolvedDimension, } from "./functionComparisonResults.js";
6
+ export { functionComparisonInputSchema, functionComparisonResultSchema, } from "./functionComparisonSchemas.js";
7
+ const unresolvedDimension = (...[dimension, status, links, leftCount, rightCount, limitations]) => buildUnresolvedDimension({
8
+ dimension,
9
+ status,
10
+ links,
11
+ leftCount,
12
+ rightCount,
13
+ limitations,
14
+ });
15
+ /** Compare two explicit function Evidence page sets without fuzzy matching. */
16
+ export const compareFunctions = (leftInput, rightInput, offset, limit) => {
17
+ const left = parseFunctionEvidence(leftInput);
18
+ const right = parseFunctionEvidence(rightInput);
19
+ const links = [
20
+ ...left.evidence.map(({ evidence_id: id }) => id),
21
+ ...right.evidence.map(({ evidence_id: id }) => id),
22
+ ];
23
+ const providersDiffer = canonicalJson(left.provider) !== canonicalJson(right.provider);
24
+ const dimensions = compareDimensions(left, right, links, providersDiffer);
25
+ const match = functionMatch(left, right);
26
+ const changes = dimensions.filter(({ status }) => status !== "unchanged");
27
+ const page = changes.slice(offset, offset + limit);
28
+ return functionComparisonResultSchema.parse({
29
+ status: overallStatus(dimensions, match.status),
30
+ function_match: match,
31
+ left_subject_sha256: left.subject.digest.sha256,
32
+ right_subject_sha256: right.subject.digest.sha256,
33
+ summary: summarize(dimensions),
34
+ dimensions,
35
+ changes: {
36
+ items: page,
37
+ offset,
38
+ limit,
39
+ total: changes.length,
40
+ next_offset: offset + page.length < changes.length ? offset + page.length : null,
41
+ },
42
+ limitations: [
43
+ ...new Set([
44
+ ...left.limitations.map((item) => `Left: ${item}`),
45
+ ...right.limitations.map((item) => `Right: ${item}`),
46
+ ...(providersDiffer
47
+ ? [
48
+ "Provider-specific pseudocode and assembly representations were not equated.",
49
+ ]
50
+ : []),
51
+ ]),
52
+ ].sort((a, b) => a.localeCompare(b)),
53
+ });
54
+ };
55
+ const compareDimensions = (left, right, links, providersDiffer) => [
56
+ compareIdentity(left, right, links, providersDiffer),
57
+ compareText("pseudocode", left.pseudocode.text, right.pseudocode.text, left.pseudocode.complete, right.pseudocode.complete, links, providersDiffer),
58
+ compareText("assembly", left.collections.assembly.items.join("\n"), right.collections.assembly.items.join("\n"), left.collections.assembly.complete, right.collections.assembly.complete, links, providersDiffer, left.collections.assembly.truncated || right.collections.assembly.truncated, "Assembly is opaque provider text; relocation normalization is unavailable."),
59
+ compareComments(left, right, links, providersDiffer),
60
+ compareCalls(left, right, links, providersDiffer),
61
+ compareReferences(left, right, links, providersDiffer),
62
+ compareStringsAndNames(left, right, links, providersDiffer),
63
+ compareCfg(left, right, links, providersDiffer),
64
+ ];
65
+ const compareText = (...[dimension, left, right, leftComplete, rightComplete, links, providersDiffer, explicitTruncation, limitation,]) => {
66
+ const truncated = explicitTruncation ?? (!leftComplete || !rightComplete);
67
+ if (!leftComplete || !rightComplete || providersDiffer)
68
+ return unresolvedDimension(dimension, truncated ? "truncated" : "unknown", links, left.length, right.length, [
69
+ ...(limitation === undefined ? [] : [limitation]),
70
+ ...(providersDiffer
71
+ ? ["Exact text comparison requires one provider identity."]
72
+ : []),
73
+ ]);
74
+ const changes = diffLines(left, right, {
75
+ timeout: 100,
76
+ maxEditLength: 10_000,
77
+ });
78
+ if (changes === undefined)
79
+ return unresolvedDimension(dimension, "unknown", links, left.length, right.length, ["Bounded line diff exceeded its time or edit-distance limit."]);
80
+ const delta = changes.reduce((summary, change) => ({
81
+ added_lines: summary.added_lines + (change.added ? (change.count ?? 0) : 0),
82
+ removed_lines: summary.removed_lines + (change.removed ? (change.count ?? 0) : 0),
83
+ hunks: summary.hunks + (change.added || change.removed ? 1 : 0),
84
+ }), { added_lines: 0, removed_lines: 0, hunks: 0 });
85
+ return dimensionResult({
86
+ dimension,
87
+ status: delta.hunks === 0 ? "unchanged" : "changed",
88
+ left,
89
+ right,
90
+ links,
91
+ providersDiffer,
92
+ leftCount: [...left].length,
93
+ rightCount: [...right].length,
94
+ textDelta: delta,
95
+ limitations: limitation === undefined ? [] : [limitation],
96
+ });
97
+ };
98
+ const compareCollections = (...[dimension, left, right, leftCoverage, rightCoverage, links, providersDiffer,]) => {
99
+ if (!leftCoverage.complete || !rightCoverage.complete)
100
+ return unresolvedDimension(dimension, leftCoverage.truncated || rightCoverage.truncated
101
+ ? "truncated"
102
+ : "unknown", links, left.length, right.length, []);
103
+ return compareValues(dimension, sorted(left), sorted(right), links, true, true, providersDiffer);
104
+ };
105
+ const compareComments = (left, right, links, providersDiffer) => {
106
+ const leftValues = commentProjection(left);
107
+ const rightValues = commentProjection(right);
108
+ if (leftValues === null || rightValues === null)
109
+ return unresolvedDimension("comments", "unknown", links, left.collections.comments.items.length, right.collections.comments.items.length, ["Comment locations could not be normalized relative to the function."]);
110
+ return compareCollections("comments", leftValues, rightValues, left.collections.comments, right.collections.comments, links, providersDiffer);
111
+ };
112
+ const compareStringsAndNames = (left, right, links, providersDiffer) => {
113
+ const leftValues = stringAndNameProjection(left);
114
+ const rightValues = stringAndNameProjection(right);
115
+ if (leftValues === null || rightValues === null)
116
+ return unresolvedDimension("strings_names", "unknown", links, null, null, [
117
+ "Reference source locations could not be normalized to function offsets.",
118
+ ]);
119
+ return compareCollections("strings_names", leftValues, rightValues, combineCoverage(left.collections.referenced_strings, left.collections.referenced_names), combineCoverage(right.collections.referenced_strings, right.collections.referenced_names), links, providersDiffer);
120
+ };
121
+ const compareReferences = (left, right, links, providersDiffer) => {
122
+ const leftCoverage = combineCoverage(left.collections.incoming_references, left.collections.outgoing_references);
123
+ const rightCoverage = combineCoverage(right.collections.incoming_references, right.collections.outgoing_references);
124
+ if (!leftCoverage.complete || !rightCoverage.complete)
125
+ return unresolvedDimension("references", leftCoverage.truncated || rightCoverage.truncated
126
+ ? "truncated"
127
+ : "unknown", links, leftCoverage.items.length, rightCoverage.items.length, []);
128
+ const leftProjection = referenceProjection(left);
129
+ const rightProjection = referenceProjection(right);
130
+ if (leftProjection.length === 0 && rightProjection.length === 0)
131
+ return compareValues("references", leftProjection, rightProjection, links, true, true, providersDiffer);
132
+ if (canonicalJson(leftProjection) !== canonicalJson(rightProjection))
133
+ return dimensionResult({
134
+ dimension: "references",
135
+ status: "changed",
136
+ left: leftProjection,
137
+ right: rightProjection,
138
+ links,
139
+ providersDiffer,
140
+ leftCount: leftProjection.length,
141
+ rightCount: rightProjection.length,
142
+ textDelta: null,
143
+ limitations: [
144
+ "Reference endpoints differ; exact reference kinds remain unavailable.",
145
+ ],
146
+ });
147
+ return unresolvedDimension("references", "unknown", links, leftProjection.length, rightProjection.length, ["Reference kinds are unavailable in the current dossier contract."]);
148
+ };
149
+ const compareIdentity = (left, right, links, providersDiffer) => {
150
+ if (isAutoName(left.procedure.name) || isAutoName(right.procedure.name))
151
+ return unresolvedDimension("identity", "unknown", links, null, null, [
152
+ "Address-derived function names are not stable cross-version identity.",
153
+ ]);
154
+ return compareValues("identity", identityProjection(left), identityProjection(right), links, true, true, providersDiffer);
155
+ };
156
+ const compareCalls = (left, right, links, providersDiffer) => {
157
+ const leftValues = callProjection(left);
158
+ const rightValues = callProjection(right);
159
+ if ([...leftValues, ...rightValues].some(({ name }) => isAutoName(name)))
160
+ return unresolvedDimension("calls", "unknown", links, leftValues.length, rightValues.length, ["Address-derived callee or caller names cannot be matched safely."]);
161
+ return compareCollections("calls", leftValues, rightValues, combineCoverage(left.collections.callers, left.collections.callees), combineCoverage(right.collections.callers, right.collections.callees), links, providersDiffer);
162
+ };
163
+ const callProjection = (snapshot) => [
164
+ ...project(snapshot.collections.callers, ({ name }) => ({
165
+ direction: "in",
166
+ name,
167
+ })),
168
+ ...project(snapshot.collections.callees, ({ name }) => ({
169
+ direction: "out",
170
+ name,
171
+ })),
172
+ ];
173
+ const compareCfg = (left, right, links, providersDiffer) => {
174
+ const leftBlocks = left.collections.basic_blocks;
175
+ const rightBlocks = right.collections.basic_blocks;
176
+ if (!leftBlocks.complete || !rightBlocks.complete)
177
+ return unresolvedDimension("cfg", leftBlocks.truncated || rightBlocks.truncated ? "truncated" : "unknown", links, leftBlocks.items.length, rightBlocks.items.length, []);
178
+ const leftGraph = normalizeCfg(leftBlocks.items);
179
+ const rightGraph = normalizeCfg(rightBlocks.items);
180
+ if (leftGraph === null || rightGraph === null)
181
+ return unresolvedDimension("cfg", "unknown", links, leftBlocks.items.length, rightBlocks.items.length, ["CFG addresses could not be normalized to local block indices."]);
182
+ return compareValues("cfg", leftGraph, rightGraph, links, true, true, providersDiffer);
183
+ };
184
+ const compareValues = (...[dimension, left, right, links, leftComplete, rightComplete, providersDiffer,]) => {
185
+ if (!leftComplete || !rightComplete)
186
+ return unresolvedDimension(dimension, "unknown", links, null, null, []);
187
+ const leftJson = canonicalJson(left);
188
+ const rightJson = canonicalJson(right);
189
+ return dimensionResult({
190
+ dimension,
191
+ status: leftJson === rightJson ? "unchanged" : "changed",
192
+ left,
193
+ right,
194
+ links,
195
+ providersDiffer,
196
+ leftCount: Array.isArray(left) ? left.length : null,
197
+ rightCount: Array.isArray(right) ? right.length : null,
198
+ textDelta: null,
199
+ limitations: [],
200
+ });
201
+ };
@@ -0,0 +1,112 @@
1
+ import canonicalize from "canonicalize";
2
+ export const identityProjection = (snapshot) => ({
3
+ name: snapshot.procedure.name,
4
+ signature: snapshot.procedure.signature,
5
+ locals: snapshot.procedure.locals
6
+ .map(({ description }) => description)
7
+ .sort(),
8
+ });
9
+ export const functionMatch = (left, right) => {
10
+ if (!isAutoName(left.procedure.name) &&
11
+ left.procedure.name === right.procedure.name)
12
+ return {
13
+ status: "matched",
14
+ method: "symbol",
15
+ left_name: left.procedure.name,
16
+ right_name: right.procedure.name,
17
+ };
18
+ return {
19
+ status: left.procedure.name === right.procedure.name ? "ambiguous" : "mismatched",
20
+ method: "explicit",
21
+ left_name: left.procedure.name,
22
+ right_name: right.procedure.name,
23
+ };
24
+ };
25
+ export const normalizeCfg = (blocks) => {
26
+ const parsed = blocks.map((block) => ({
27
+ block,
28
+ start: parseAddress(block.start),
29
+ end: parseAddress(block.end),
30
+ }));
31
+ if (parsed.some(({ start, end }) => start === null || end === null))
32
+ return null;
33
+ const ordered = parsed.sort((left, right) => (left.start ?? 0n) < (right.start ?? 0n) ? -1 : 1);
34
+ if (new Set(ordered.map(({ block }) => block.start)).size !== ordered.length)
35
+ return null;
36
+ if (ordered.some(({ start, end }) => (end ?? 0n) <= (start ?? 0n)))
37
+ return null;
38
+ const indices = new Map(ordered.map(({ block }, index) => [block.start, index]));
39
+ const graph = [];
40
+ for (const { block, start, end } of ordered) {
41
+ const successors = block.successors.map((address) => indices.get(address));
42
+ if (successors.some((index) => index === undefined))
43
+ return null;
44
+ graph.push({
45
+ size: String((end ?? 0n) - (start ?? 0n)),
46
+ successors: successors.sort((left, right) => (left ?? 0) - (right ?? 0)),
47
+ });
48
+ }
49
+ return graph;
50
+ };
51
+ export const referenceProjection = (snapshot) => sorted([
52
+ ...snapshot.collections.incoming_references.items.map((item) => ({
53
+ direction: "in",
54
+ source: item.source_procedure?.name ?? null,
55
+ target: item.target_procedure?.name ?? null,
56
+ })),
57
+ ...snapshot.collections.outgoing_references.items.map((item) => ({
58
+ direction: "out",
59
+ source: item.source_procedure?.name ?? null,
60
+ target: item.target_procedure?.name ?? null,
61
+ })),
62
+ ]);
63
+ export const commentProjection = (snapshot) => {
64
+ const values = snapshot.collections.comments.items.map(({ address, kind, text }) => ({
65
+ offset: relativeAddress(address, snapshot.procedure.address),
66
+ kind,
67
+ text,
68
+ }));
69
+ return values.some(({ offset }) => offset === null) ? null : sorted(values);
70
+ };
71
+ export const stringAndNameProjection = (snapshot) => {
72
+ const values = [
73
+ ...snapshot.collections.referenced_strings.items.map(({ source_address: sourceAddress, value }) => ({
74
+ kind: "string",
75
+ source_offset: relativeAddress(sourceAddress, snapshot.procedure.address),
76
+ value,
77
+ })),
78
+ ...snapshot.collections.referenced_names.items.map(({ source_address: sourceAddress, value }) => ({
79
+ kind: "name",
80
+ source_offset: relativeAddress(sourceAddress, snapshot.procedure.address),
81
+ value,
82
+ })),
83
+ ];
84
+ return values.some(({ source_offset: offset }) => offset === null)
85
+ ? null
86
+ : sorted(values);
87
+ };
88
+ export const sorted = (values) => [...values].sort((left, right) => canonicalJson(left).localeCompare(canonicalJson(right)));
89
+ export const combineCoverage = (left, right) => ({
90
+ items: [...left.items, ...right.items],
91
+ total: left.total === null || right.total === null
92
+ ? null
93
+ : left.total + right.total,
94
+ complete: left.complete && right.complete,
95
+ truncated: left.truncated || right.truncated,
96
+ });
97
+ export const project = (collection, mapper) => collection.items.map(mapper);
98
+ export const isAutoName = (name) => /^(?:sub_[0-9a-f]+|fcn\.)/iu.test(name);
99
+ const parseAddress = (value) => /^0x[0-9a-f]+$/iu.test(value) ? BigInt(value) : null;
100
+ const relativeAddress = (value, base) => {
101
+ const parsed = parseAddress(value);
102
+ const parsedBase = parseAddress(base);
103
+ return parsed === null || parsedBase === null || parsed < parsedBase
104
+ ? null
105
+ : String(parsed - parsedBase);
106
+ };
107
+ const canonicalJson = (value) => {
108
+ const encoded = canonicalize(value);
109
+ if (encoded === undefined)
110
+ throw new TypeError("Function normalization could not canonicalize data");
111
+ return encoded;
112
+ };
@@ -0,0 +1,54 @@
1
+ import { createHash } from "node:crypto";
2
+ import canonicalize from "canonicalize";
3
+ export const dimensionResult = (input) => ({
4
+ dimension: input.dimension,
5
+ status: input.status,
6
+ left_digest: digest(input.left),
7
+ right_digest: digest(input.right),
8
+ left_count: input.leftCount,
9
+ right_count: input.rightCount,
10
+ text_delta: input.textDelta,
11
+ conclusion_kind: input.status === "changed" && input.providersDiffer
12
+ ? "contradiction"
13
+ : "derived_relationship",
14
+ evidence_links: [...input.links],
15
+ limitations: [...input.limitations],
16
+ });
17
+ export const unresolvedDimension = (input) => ({
18
+ dimension: input.dimension,
19
+ status: input.status,
20
+ left_digest: null,
21
+ right_digest: null,
22
+ left_count: input.leftCount,
23
+ right_count: input.rightCount,
24
+ text_delta: null,
25
+ conclusion_kind: "unresolved_branch",
26
+ evidence_links: [...input.links],
27
+ limitations: [...input.limitations],
28
+ });
29
+ export const overallStatus = (dimensions, match) => {
30
+ if (dimensions.some(({ status }) => status === "truncated"))
31
+ return "truncated";
32
+ if (match === "ambiguous" ||
33
+ match === "unknown" ||
34
+ dimensions.some(({ status }) => status === "unknown"))
35
+ return "unknown";
36
+ if (match === "mismatched")
37
+ return "changed";
38
+ return dimensions.some(({ status }) => status === "changed")
39
+ ? "changed"
40
+ : "unchanged";
41
+ };
42
+ export const summarize = (dimensions) => ({
43
+ unchanged: dimensions.filter(({ status }) => status === "unchanged").length,
44
+ changed: dimensions.filter(({ status }) => status === "changed").length,
45
+ truncated: dimensions.filter(({ status }) => status === "truncated").length,
46
+ unknown: dimensions.filter(({ status }) => status === "unknown").length,
47
+ });
48
+ export const canonicalJson = (value) => {
49
+ const encoded = canonicalize(value);
50
+ if (encoded === undefined)
51
+ throw new TypeError("Function comparison could not canonicalize data");
52
+ return encoded;
53
+ };
54
+ const digest = (value) => createHash("sha256").update(canonicalJson(value)).digest("hex");
@@ -0,0 +1,82 @@
1
+ import { z } from "zod";
2
+ import { evidenceSchema } from "./evidence.js";
3
+ const evidenceInput = z.union([
4
+ evidenceSchema,
5
+ z.array(evidenceSchema).min(1).max(100),
6
+ ]);
7
+ const comparisonStatusSchema = z.enum([
8
+ "unchanged",
9
+ "added",
10
+ "removed",
11
+ "changed",
12
+ "truncated",
13
+ "unknown",
14
+ ]);
15
+ const dimensionNameSchema = z.enum([
16
+ "identity",
17
+ "pseudocode",
18
+ "assembly",
19
+ "comments",
20
+ "calls",
21
+ "references",
22
+ "strings_names",
23
+ "cfg",
24
+ ]);
25
+ const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
26
+ const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
27
+ /** Strict bounded input for explicit function-to-function comparison. */
28
+ export const functionComparisonInputSchema = z.object({
29
+ left: evidenceInput,
30
+ right: evidenceInput,
31
+ offset: z.number().int().min(0).default(0),
32
+ limit: z.number().int().min(1).max(100).default(100),
33
+ unknown_registry_approved: z.literal(true).optional(),
34
+ });
35
+ const textDeltaSchema = z.object({
36
+ added_lines: z.number().int().min(0),
37
+ removed_lines: z.number().int().min(0),
38
+ hunks: z.number().int().min(0),
39
+ });
40
+ const functionDimensionSchema = z.object({
41
+ dimension: dimensionNameSchema,
42
+ status: comparisonStatusSchema,
43
+ left_digest: digestSchema.nullable(),
44
+ right_digest: digestSchema.nullable(),
45
+ left_count: z.number().int().min(0).nullable(),
46
+ right_count: z.number().int().min(0).nullable(),
47
+ text_delta: textDeltaSchema.nullable(),
48
+ conclusion_kind: z.enum([
49
+ "derived_relationship",
50
+ "contradiction",
51
+ "unresolved_branch",
52
+ ]),
53
+ evidence_links: z.array(evidenceIdSchema).min(2).max(200),
54
+ limitations: z.array(z.string()),
55
+ });
56
+ /** Deterministic dimension-classified function comparison Evidence payload. */
57
+ export const functionComparisonResultSchema = z.object({
58
+ status: comparisonStatusSchema,
59
+ function_match: z.object({
60
+ status: z.enum(["matched", "mismatched", "ambiguous", "unknown"]),
61
+ method: z.enum(["explicit", "symbol"]),
62
+ left_name: z.string(),
63
+ right_name: z.string(),
64
+ }),
65
+ left_subject_sha256: digestSchema,
66
+ right_subject_sha256: digestSchema,
67
+ summary: z.object({
68
+ unchanged: z.number().int().min(0),
69
+ changed: z.number().int().min(0),
70
+ truncated: z.number().int().min(0),
71
+ unknown: z.number().int().min(0),
72
+ }),
73
+ dimensions: z.array(functionDimensionSchema).length(8),
74
+ changes: z.object({
75
+ items: z.array(functionDimensionSchema).max(100),
76
+ offset: z.number().int().min(0),
77
+ limit: z.number().int().min(1).max(100),
78
+ total: z.number().int().min(0),
79
+ next_offset: z.number().int().min(0).nullable(),
80
+ }),
81
+ limitations: z.array(z.string()),
82
+ });
@@ -0,0 +1,171 @@
1
+ import canonicalize from "canonicalize";
2
+ import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
3
+ import { parseEvidence } from "./evidence.js";
4
+ import { functionDossierSchema } from "./hopperValues.js";
5
+ const MAX_PAGES = 100;
6
+ const MAX_ITEMS = 50_000;
7
+ const MAX_PSEUDOCODE_CHARS = 2_000_000;
8
+ const DOSSIER_COLLECTION_FIELDS = [
9
+ "assembly",
10
+ "comments",
11
+ "callers",
12
+ "callees",
13
+ "incoming_references",
14
+ "outgoing_references",
15
+ "referenced_strings",
16
+ "referenced_names",
17
+ "basic_blocks",
18
+ ];
19
+ /** Parse and assemble up to 100 mutually consistent analyze_function pages. */
20
+ export const parseFunctionEvidence = (input) => {
21
+ const values = Array.isArray(input) ? input : [input];
22
+ if (values.length === 0 || values.length > MAX_PAGES)
23
+ throw new TypeError("Function comparison requires 1 to 100 Evidence pages");
24
+ const pages = values.map(parsePage);
25
+ assertPageGroup(pages);
26
+ const first = pages[0];
27
+ if (first === undefined)
28
+ throw new TypeError("Function comparison requires Evidence pages");
29
+ const pseudocode = mergePseudocode(pages);
30
+ const collections = {
31
+ assembly: mergeCollection(pages, "assembly"),
32
+ comments: mergeCollection(pages, "comments"),
33
+ callers: mergeCollection(pages, "callers"),
34
+ callees: mergeCollection(pages, "callees"),
35
+ incoming_references: mergeCollection(pages, "incoming_references"),
36
+ outgoing_references: mergeCollection(pages, "outgoing_references"),
37
+ referenced_strings: mergeCollection(pages, "referenced_strings"),
38
+ referenced_names: mergeCollection(pages, "referenced_names"),
39
+ basic_blocks: mergeCollection(pages, "basic_blocks"),
40
+ };
41
+ return {
42
+ evidence: pages.map(({ evidence }) => evidence),
43
+ procedure: first.dossier.procedure,
44
+ provider: first.evidence.provider,
45
+ subject: first.evidence.subject,
46
+ pseudocode,
47
+ collections,
48
+ instructionScan: first.dossier.instruction_scan,
49
+ limitations: [
50
+ ...new Set(pages.flatMap(({ evidence }) => evidence.limitations)),
51
+ ].sort((left, right) => left.localeCompare(right)),
52
+ };
53
+ };
54
+ const parsePage = (input) => {
55
+ const evidence = parseEvidence(input);
56
+ if (evidence.operation !== "analyze_function")
57
+ throw new TypeError("Function comparison requires analyze_function Evidence");
58
+ if (evidence.subject === null)
59
+ throw new TypeError("Function comparison requires artifact-bound Evidence");
60
+ return {
61
+ evidence: { ...evidence, subject: evidence.subject },
62
+ dossier: functionDossierSchema.parse(evidence.normalized_result),
63
+ parameters: enhancedInputSchemas.analyze_function.parse(evidence.parameters),
64
+ };
65
+ };
66
+ const assertPageGroup = (pages) => {
67
+ const first = pages[0];
68
+ if (first === undefined)
69
+ return;
70
+ const invariant = pageInvariant(first);
71
+ for (const page of pages)
72
+ if (canonicalJson(pageInvariant(page)) !== canonicalJson(invariant))
73
+ throw new TypeError("Function Evidence pages mix subjects, providers, or immutable analysis controls");
74
+ };
75
+ const pageInvariant = (page) => ({
76
+ subject: {
77
+ digest: page.evidence.subject?.digest,
78
+ format: page.evidence.subject?.format,
79
+ architecture: page.evidence.subject?.architecture,
80
+ },
81
+ provider: page.evidence.provider,
82
+ procedure: page.dossier.procedure,
83
+ requested_procedure: page.parameters.procedure,
84
+ include_assembly: page.parameters.include_assembly,
85
+ max_instructions: page.parameters.max_instructions,
86
+ instruction_scan: page.dossier.instruction_scan,
87
+ });
88
+ const mergePseudocode = (pages) => {
89
+ const totals = new Set(pages.map(({ dossier }) => dossier.pseudocode.total_chars));
90
+ if (totals.size !== 1)
91
+ throw new TypeError("Function pseudocode pages disagree on total length");
92
+ const total = pages[0]?.dossier.pseudocode.total_chars ?? 0;
93
+ if (total > MAX_PSEUDOCODE_CHARS)
94
+ throw new TypeError("Function pseudocode exceeds comparison limit");
95
+ const characters = new Map();
96
+ for (const page of pages) {
97
+ const offset = page.parameters.pseudocode_offset;
98
+ for (const [index, character] of [
99
+ ...page.dossier.pseudocode.text,
100
+ ].entries())
101
+ mergeIndexed(characters, offset + index, character, "pseudocode");
102
+ if (characters.size > MAX_PSEUDOCODE_CHARS)
103
+ throw new TypeError("Function pseudocode exceeds comparison limit");
104
+ }
105
+ const complete = hasCompleteRange(characters, total);
106
+ return {
107
+ text: orderedValues(characters).join(""),
108
+ total,
109
+ complete,
110
+ truncated: !complete,
111
+ };
112
+ };
113
+ const mergeCollection = (pages, field) => {
114
+ const totals = new Set(pages.map(({ dossier }) => dossier[field].total));
115
+ if (totals.size !== 1)
116
+ throw new TypeError(`Function ${field} pages disagree on total count`);
117
+ const total = pages[0]?.dossier[field].total ?? null;
118
+ if (total !== null && total > MAX_ITEMS)
119
+ throw new TypeError(`Function ${field} exceeds comparison item limit`);
120
+ const items = new Map();
121
+ for (const page of pages) {
122
+ const offset = collectionOffset(page, field);
123
+ for (const [index, item] of page.dossier[field].items.entries())
124
+ mergeIndexed(items, offset + index, item, field);
125
+ if (items.size > MAX_ITEMS)
126
+ throw new TypeError(`Function ${field} exceeds comparison item limit`);
127
+ }
128
+ const scanLimited = field !== "assembly" && pageDependsOnScan(field)
129
+ ? pages.some(({ dossier }) => dossier.instruction_scan.truncated)
130
+ : false;
131
+ const unavailable = field === "assembly" &&
132
+ pages.every(({ parameters }) => !parameters.include_assembly);
133
+ const complete = !scanLimited &&
134
+ !unavailable &&
135
+ total !== null &&
136
+ hasCompleteRange(items, total);
137
+ return {
138
+ items: orderedValues(items),
139
+ total,
140
+ complete,
141
+ truncated: scanLimited || (!unavailable && !complete),
142
+ };
143
+ };
144
+ const collectionOffset = (page, field) => field === "assembly"
145
+ ? page.parameters.assembly_offset
146
+ : page.parameters.collection_offset[field];
147
+ const pageDependsOnScan = (field) => field !== "callers" && field !== "callees";
148
+ const mergeIndexed = (output, index, value, field) => {
149
+ const previous = output.get(index);
150
+ if (previous !== undefined &&
151
+ canonicalJson(previous) !== canonicalJson(value))
152
+ throw new TypeError(`Function ${field} pages overlap with conflicting data`);
153
+ output.set(index, value);
154
+ };
155
+ const hasCompleteRange = (values, total) => {
156
+ if (values.size !== total)
157
+ return false;
158
+ for (let index = 0; index < total; index += 1)
159
+ if (!values.has(index))
160
+ return false;
161
+ return true;
162
+ };
163
+ const orderedValues = (values) => [...values.entries()]
164
+ .sort(([left], [right]) => left - right)
165
+ .map(([, value]) => value);
166
+ const canonicalJson = (value) => {
167
+ const encoded = canonicalize(value);
168
+ if (encoded === undefined)
169
+ throw new TypeError("Function Evidence could not be canonicalized");
170
+ return encoded;
171
+ };