rea-agents 0.3.0 → 0.5.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 (125) hide show
  1. package/README.md +139 -21
  2. package/bridge/hopper_bridge.py +156 -14
  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 +174 -14
  8. package/dist/application/CompositeProvider.js +73 -0
  9. package/dist/application/DirectAnalysis.js +44 -3
  10. package/dist/application/Doctor.js +35 -6
  11. package/dist/application/EnhancedTools.js +10 -7
  12. package/dist/application/EvidenceBundleCommands.js +51 -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 +105 -0
  21. package/dist/application/ProcessSampling.js +284 -0
  22. package/dist/application/RealHopperAssertions.js +116 -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 +229 -21
  41. package/dist/cliEvidenceCommands.js +68 -0
  42. package/dist/cliLogging.js +21 -0
  43. package/dist/config.js +35 -1
  44. package/dist/contracts/artifactComparisonExample.js +95 -0
  45. package/dist/contracts/artifactToolContracts.js +84 -0
  46. package/dist/contracts/enhancedInputs.js +4 -0
  47. package/dist/contracts/functionComparisonExample.js +57 -0
  48. package/dist/contracts/investigationExamples.js +118 -0
  49. package/dist/contracts/nativeToolContracts.js +52 -0
  50. package/dist/contracts/processCaptureExample.js +25 -0
  51. package/dist/contracts/toolContractExamples.js +69 -0
  52. package/dist/contracts/toolContracts.js +99 -36
  53. package/dist/contracts/toolOutputSchemas.js +156 -82
  54. package/dist/contracts/unknownContractExamples.js +33 -0
  55. package/dist/domain/artifactComparison.js +273 -0
  56. package/dist/domain/artifactGraph.js +194 -0
  57. package/dist/domain/artifactInventoryEvidence.js +150 -0
  58. package/dist/domain/binaryTarget.js +55 -0
  59. package/dist/domain/bundleComparison.js +266 -0
  60. package/dist/domain/callPath.js +346 -0
  61. package/dist/domain/changedBehavior.js +294 -0
  62. package/dist/domain/errors.js +199 -5
  63. package/dist/domain/evidence.js +41 -10
  64. package/dist/domain/evidenceBundle.js +187 -6
  65. package/dist/domain/functionComparison.js +201 -0
  66. package/dist/domain/functionComparisonNormalization.js +112 -0
  67. package/dist/domain/functionComparisonResults.js +54 -0
  68. package/dist/domain/functionComparisonSchemas.js +82 -0
  69. package/dist/domain/functionDossierEvidence.js +171 -0
  70. package/dist/domain/hopperValues.js +35 -8
  71. package/dist/domain/nativeInspection.js +142 -0
  72. package/dist/domain/processCapture.js +152 -54
  73. package/dist/domain/processComparison.js +106 -0
  74. package/dist/domain/reconstructionUnknowns.js +90 -0
  75. package/dist/domain/reconstructionVerification.js +285 -0
  76. package/dist/domain/reconstructionVerificationSchemas.js +126 -0
  77. package/dist/domain/referenceSourceClassification.js +496 -0
  78. package/dist/domain/referenceSourceGraph.js +376 -0
  79. package/dist/domain/referenceSourceImportParsing.js +235 -0
  80. package/dist/domain/referenceSourcePolicy.js +1 -0
  81. package/dist/domain/residualUnknown.js +239 -0
  82. package/dist/domain/staticRuntimeCorrelation.js +375 -0
  83. package/dist/hopper/BridgeLauncher.js +39 -3
  84. package/dist/hopper/HopperClient.js +14 -5
  85. package/dist/hopper/HopperProvider.js +57 -22
  86. package/dist/hopper/protocol.js +13 -2
  87. package/dist/identity.js +1 -0
  88. package/dist/main.js +5 -1
  89. package/dist/native/CommandRunner.js +156 -0
  90. package/dist/native/NativeMacOSProvider.js +306 -0
  91. package/dist/native/NativeMachoInspection.js +135 -0
  92. package/dist/native/parsers/codesign.js +55 -0
  93. package/dist/native/parsers/demangle.js +26 -0
  94. package/dist/native/parsers/dyldInfo.js +25 -0
  95. package/dist/native/parsers/lipo.js +67 -0
  96. package/dist/native/parsers/otool.js +193 -0
  97. package/dist/native/parsers/plist.js +23 -0
  98. package/dist/reference/ReferenceSourceReader.js +73 -0
  99. package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
  100. package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
  101. package/dist/reference/ReferenceSourceReaderFile.js +119 -0
  102. package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
  103. package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
  104. package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
  105. package/dist/server/createServer.js +31 -5
  106. package/dist/server/recordDerivedEvidence.js +10 -0
  107. package/dist/server/registerArtifactComparisonTool.js +62 -0
  108. package/dist/server/registerArtifactTools.js +6 -0
  109. package/dist/server/registerBundleComparisonTool.js +47 -0
  110. package/dist/server/registerEnhancedTools.js +64 -12
  111. package/dist/server/registerEvidenceTools.js +36 -0
  112. package/dist/server/registerFunctionComparisonTool.js +68 -0
  113. package/dist/server/registerInvestigationTools.js +224 -0
  114. package/dist/server/registerNativeTools.js +6 -0
  115. package/dist/server/registerOfficialTools.js +47 -14
  116. package/dist/server/registerProcessComparisonTool.js +106 -0
  117. package/dist/server/registerSessionTools.js +179 -70
  118. package/dist/server/sessionEvidence.js +28 -0
  119. package/dist/server/sessionToolPolicies.js +64 -0
  120. package/dist/server/toolRegistrationOptions.js +7 -0
  121. package/dist/server/toolResult.js +8 -5
  122. package/install.sh +198 -0
  123. package/package.json +18 -1
  124. package/scripts/rea.mjs +5 -1
  125. package/skills/rea-analysis/SKILL.md +77 -2
@@ -28,6 +28,7 @@ export const parseBinaryTarget = async (input, cwd = process.cwd(), hostArchitec
28
28
  (targetKind === undefined && path.toLowerCase().endsWith(".hop")))
29
29
  return ok({
30
30
  path,
31
+ sourcePath: canonical,
31
32
  sha256: await sha256File(path),
32
33
  kind: "database",
33
34
  format: "hopper",
@@ -36,6 +37,16 @@ export const parseBinaryTarget = async (input, cwd = process.cwd(), hostArchitec
36
37
  const handle = await open(path, "r");
37
38
  let detected;
38
39
  try {
40
+ const artifactFormat = await detectArtifactFormat(path, handle);
41
+ if (artifactFormat !== undefined)
42
+ return ok({
43
+ path,
44
+ sourcePath: canonical,
45
+ sha256: await sha256File(path),
46
+ kind: isArchiveFormat(artifactFormat) ? "archive" : "artifact",
47
+ format: artifactFormat,
48
+ loaderArgs: [],
49
+ });
39
50
  detected = await readExecutableMetadata(handle, hostArchitecture);
40
51
  }
41
52
  finally {
@@ -45,6 +56,7 @@ export const parseBinaryTarget = async (input, cwd = process.cwd(), hostArchitec
45
56
  return err(new BinaryTargetError(path, detected.error));
46
57
  return ok({
47
58
  path,
59
+ sourcePath: canonical,
48
60
  sha256: await sha256File(path),
49
61
  kind: "executable",
50
62
  ...detected.value,
@@ -54,6 +66,49 @@ export const parseBinaryTarget = async (input, cwd = process.cwd(), hostArchitec
54
66
  return err(new BinaryTargetError(candidate, "path is not readable", { cause }));
55
67
  }
56
68
  };
69
+ const detectArtifactFormat = async (path, handle) => {
70
+ const lower = path.toLowerCase();
71
+ const magic = Buffer.alloc(8);
72
+ const observed = await handle.read(magic, 0, magic.length, 0);
73
+ if (observed.bytesRead >= 4 &&
74
+ magic[0] === 0x50 &&
75
+ magic[1] === 0x4b &&
76
+ [0x03, 0x05, 0x07].includes(magic[2] ?? -1) &&
77
+ [0x04, 0x06, 0x08].includes(magic[3] ?? -1)) {
78
+ return lower.endsWith(".ipa")
79
+ ? "ipa"
80
+ : lower.endsWith(".apk")
81
+ ? "apk"
82
+ : "zip";
83
+ }
84
+ const named = namedArtifactFormat(lower);
85
+ if (named !== undefined)
86
+ return named;
87
+ if (lower.endsWith(".pkg") &&
88
+ observed.bytesRead >= 4 &&
89
+ magic.subarray(0, 4).toString("ascii") === "xar!")
90
+ return "pkg";
91
+ if (lower.endsWith(".dmg")) {
92
+ const size = (await handle.stat()).size;
93
+ if (size >= 512) {
94
+ const trailer = Buffer.alloc(4);
95
+ const read = await handle.read(trailer, 0, trailer.length, size - 512);
96
+ if (read.bytesRead === 4 && trailer.toString("ascii") === "koly")
97
+ return "dmg";
98
+ }
99
+ }
100
+ return undefined;
101
+ };
102
+ const namedArtifactFormat = (lowerPath) => {
103
+ if (lowerPath.endsWith(".asar"))
104
+ return "asar";
105
+ if (lowerPath.endsWith(".plist"))
106
+ return "plist";
107
+ if (lowerPath.endsWith(".map"))
108
+ return "source-map";
109
+ return /\.(?:m?js|cjs)$/u.test(lowerPath) ? "javascript" : undefined;
110
+ };
111
+ const isArchiveFormat = (format) => ["zip", "ipa", "apk", "asar", "dmg", "pkg"].includes(format);
57
112
  const sha256File = async (path) => {
58
113
  const hash = createHash("sha256");
59
114
  for await (const chunk of createReadStream(path))
@@ -0,0 +1,266 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { evidenceBundleSchema, parseEvidenceBundle, serializeEvidenceBundle, } from "./evidenceBundle.js";
4
+ const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
5
+ const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
6
+ const classificationSchema = z.enum([
7
+ "added",
8
+ "removed",
9
+ "changed",
10
+ "unknown",
11
+ "history_advanced",
12
+ "history_diverged",
13
+ ]);
14
+ /** Strict bounded input for canonical Evidence bundle comparison. */
15
+ export const bundleComparisonInputSchema = z.object({
16
+ left: evidenceBundleSchema,
17
+ right: evidenceBundleSchema,
18
+ record_pairs: z
19
+ .array(z.object({
20
+ left_evidence_id: evidenceIdSchema,
21
+ right_evidence_id: evidenceIdSchema,
22
+ }))
23
+ .max(500)
24
+ .default([]),
25
+ offset: z.number().int().min(0).default(0),
26
+ limit: z.number().int().min(1).max(500).default(100),
27
+ });
28
+ const bundleChangeSchema = z.object({
29
+ entity: z.enum(["evidence", "residual_unknown"]),
30
+ key: z.string().min(1),
31
+ classification: classificationSchema,
32
+ conclusion_kind: z.enum([
33
+ "observed_change",
34
+ "contradiction",
35
+ "unresolved_branch",
36
+ ]),
37
+ left_evidence_ids: z.array(evidenceIdSchema),
38
+ right_evidence_ids: z.array(evidenceIdSchema),
39
+ left_digest: digestSchema.nullable(),
40
+ right_digest: digestSchema.nullable(),
41
+ limitations: z.array(z.string()),
42
+ });
43
+ /** Deterministic classified delta between two canonical Evidence bundles. */
44
+ export const bundleComparisonResultSchema = z.object({
45
+ status: z.enum(["unchanged", "changed", "unknown"]),
46
+ left_bundle_sha256: digestSchema,
47
+ right_bundle_sha256: digestSchema,
48
+ summary: z.object({
49
+ records_unchanged: z.number().int().min(0),
50
+ records_added: z.number().int().min(0),
51
+ records_removed: z.number().int().min(0),
52
+ records_changed: z.number().int().min(0),
53
+ unknowns_unchanged: z.number().int().min(0),
54
+ unknowns_added: z.number().int().min(0),
55
+ unknowns_removed: z.number().int().min(0),
56
+ unknowns_advanced: z.number().int().min(0),
57
+ unknowns_diverged: z.number().int().min(0),
58
+ unresolved: z.number().int().min(0),
59
+ }),
60
+ changes: z.object({
61
+ items: z.array(bundleChangeSchema).max(500),
62
+ offset: z.number().int().min(0),
63
+ limit: z.number().int().min(1).max(500),
64
+ total: z.number().int().min(0),
65
+ next_offset: z.number().int().min(0).nullable(),
66
+ }),
67
+ limitations: z.array(z.string()),
68
+ });
69
+ const MAX_ENTITIES = 100_000;
70
+ /** Compare canonical bundle membership and only explicitly paired observations. */
71
+ export const compareBundles = (...[leftInput, rightInput, recordPairs, offset, limit]) => {
72
+ const left = parseEvidenceBundle(leftInput);
73
+ const right = parseEvidenceBundle(rightInput);
74
+ enforceBounds(left, right);
75
+ const leftDigest = bundleDigest(left);
76
+ const rightDigest = bundleDigest(right);
77
+ const recordResult = compareRecords(left, right, recordPairs);
78
+ const unknownResult = compareUnknowns(left, right);
79
+ const changes = [...recordResult.changes, ...unknownResult.changes].sort(compareChanges);
80
+ const page = changes.slice(offset, offset + limit);
81
+ const unresolved = changes.filter(({ conclusion_kind: kind }) => kind !== "observed_change").length;
82
+ return bundleComparisonResultSchema.parse({
83
+ status: leftDigest === rightDigest && changes.length === 0
84
+ ? "unchanged"
85
+ : unresolved > 0
86
+ ? "unknown"
87
+ : "changed",
88
+ left_bundle_sha256: leftDigest,
89
+ right_bundle_sha256: rightDigest,
90
+ summary: {
91
+ ...recordResult.summary,
92
+ ...unknownResult.summary,
93
+ unresolved,
94
+ },
95
+ changes: {
96
+ items: page,
97
+ offset,
98
+ limit,
99
+ total: changes.length,
100
+ next_offset: offset + page.length < changes.length ? offset + page.length : null,
101
+ },
102
+ limitations: changes.some(({ limitations }) => limitations.length > 0)
103
+ ? [
104
+ "One-sided membership proves only bundle inclusion or omission, not behavioral absence.",
105
+ ]
106
+ : [],
107
+ });
108
+ };
109
+ const compareRecords = (left, right, pairs) => {
110
+ const leftIds = new Set(left.records.map(({ evidence_id: id }) => id));
111
+ const rightIds = new Set(right.records.map(({ evidence_id: id }) => id));
112
+ const pairedLeft = new Set();
113
+ const pairedRight = new Set();
114
+ const changes = [];
115
+ let changed = 0;
116
+ for (const pair of pairs) {
117
+ if (!leftIds.has(pair.left_evidence_id))
118
+ throw new TypeError(`Explicit pair references missing left evidence ${pair.left_evidence_id}`);
119
+ if (!rightIds.has(pair.right_evidence_id))
120
+ throw new TypeError(`Explicit pair references missing right evidence ${pair.right_evidence_id}`);
121
+ if (pairedLeft.has(pair.left_evidence_id) ||
122
+ pairedRight.has(pair.right_evidence_id))
123
+ throw new TypeError("Explicit evidence pairs must be one-to-one");
124
+ pairedLeft.add(pair.left_evidence_id);
125
+ pairedRight.add(pair.right_evidence_id);
126
+ if (pair.left_evidence_id === pair.right_evidence_id)
127
+ continue;
128
+ changed += 1;
129
+ changes.push({
130
+ entity: "evidence",
131
+ key: `pair:${pair.left_evidence_id}:${pair.right_evidence_id}`,
132
+ classification: "changed",
133
+ conclusion_kind: "observed_change",
134
+ left_evidence_ids: [pair.left_evidence_id],
135
+ right_evidence_ids: [pair.right_evidence_id],
136
+ left_digest: pair.left_evidence_id.slice(3),
137
+ right_digest: pair.right_evidence_id.slice(3),
138
+ limitations: [],
139
+ });
140
+ }
141
+ const common = [...leftIds].filter((id) => rightIds.has(id) && !pairedLeft.has(id) && !pairedRight.has(id));
142
+ const removed = [...leftIds].filter((id) => !rightIds.has(id) && !pairedLeft.has(id));
143
+ const added = [...rightIds].filter((id) => !leftIds.has(id) && !pairedRight.has(id));
144
+ for (const [classification, ids, side] of [
145
+ ["removed", removed, "left"],
146
+ ["added", added, "right"],
147
+ ])
148
+ for (const id of ids)
149
+ changes.push({
150
+ entity: "evidence",
151
+ key: id,
152
+ classification,
153
+ conclusion_kind: "observed_change",
154
+ left_evidence_ids: side === "left" ? [id] : [],
155
+ right_evidence_ids: side === "right" ? [id] : [],
156
+ left_digest: side === "left" ? id.slice(3) : null,
157
+ right_digest: side === "right" ? id.slice(3) : null,
158
+ limitations: [
159
+ "One-sided membership does not establish behavioral absence.",
160
+ ],
161
+ });
162
+ return {
163
+ changes,
164
+ summary: {
165
+ records_unchanged: common.length + pairs.length - changed,
166
+ records_added: added.length,
167
+ records_removed: removed.length,
168
+ records_changed: changed,
169
+ },
170
+ };
171
+ };
172
+ const compareUnknowns = (left, right) => {
173
+ const leftHistories = histories(left.unknowns);
174
+ const rightHistories = histories(right.unknowns);
175
+ const ids = [...new Set([...leftHistories.keys(), ...rightHistories.keys()])];
176
+ const changes = [];
177
+ const summary = {
178
+ unknowns_unchanged: 0,
179
+ unknowns_added: 0,
180
+ unknowns_removed: 0,
181
+ unknowns_advanced: 0,
182
+ unknowns_diverged: 0,
183
+ };
184
+ for (const id of ids) {
185
+ const leftHistory = leftHistories.get(id);
186
+ const rightHistory = rightHistories.get(id);
187
+ if (leftHistory === undefined || rightHistory === undefined) {
188
+ const added = leftHistory === undefined;
189
+ summary[added ? "unknowns_added" : "unknowns_removed"] += 1;
190
+ changes.push(unknownChange(id, added ? "added" : "removed", leftHistory, rightHistory));
191
+ continue;
192
+ }
193
+ const leftDigests = leftHistory.map(({ revision_digest: digest }) => digest);
194
+ const rightDigests = rightHistory.map(({ revision_digest: digest }) => digest);
195
+ if (same(leftDigests, rightDigests)) {
196
+ summary.unknowns_unchanged += 1;
197
+ continue;
198
+ }
199
+ const advanced = isPrefix(leftDigests, rightDigests) ||
200
+ isPrefix(rightDigests, leftDigests);
201
+ summary[advanced ? "unknowns_advanced" : "unknowns_diverged"] += 1;
202
+ changes.push(unknownChange(id, advanced ? "history_advanced" : "history_diverged", leftHistory, rightHistory));
203
+ }
204
+ return { changes, summary };
205
+ };
206
+ const unknownChange = (id, classification, left, right) => ({
207
+ entity: "residual_unknown",
208
+ key: id,
209
+ classification,
210
+ conclusion_kind: classification === "history_diverged"
211
+ ? "contradiction"
212
+ : classification === "history_advanced"
213
+ ? "observed_change"
214
+ : "unresolved_branch",
215
+ left_evidence_ids: historyEvidenceIds(left ?? []),
216
+ right_evidence_ids: historyEvidenceIds(right ?? []),
217
+ left_digest: left?.at(-1)?.revision_digest ?? null,
218
+ right_digest: right?.at(-1)?.revision_digest ?? null,
219
+ limitations: left === undefined || right === undefined
220
+ ? ["A missing unknown history does not establish resolution."]
221
+ : [],
222
+ });
223
+ const histories = (unknowns) => {
224
+ const output = new Map();
225
+ for (const unknown of unknowns) {
226
+ const history = output.get(unknown.unknown_id) ?? [];
227
+ history.push(unknown);
228
+ output.set(unknown.unknown_id, history);
229
+ }
230
+ return output;
231
+ };
232
+ const historyEvidenceIds = (history) => [
233
+ ...new Set(history.flatMap((item) => [
234
+ ...item.supporting_evidence_ids,
235
+ ...item.contradicting_evidence_ids,
236
+ ...item.mutation_evidence_ids,
237
+ ...(item.resolution?.evidence_ids ?? []),
238
+ ])),
239
+ ].sort(ascii);
240
+ const enforceBounds = (left, right) => {
241
+ const total = left.records.length +
242
+ right.records.length +
243
+ left.unknowns.length +
244
+ right.unknowns.length;
245
+ if (total > MAX_ENTITIES)
246
+ throw new TypeError(`Bundle comparison exceeds ${MAX_ENTITIES} entities`);
247
+ };
248
+ const bundleDigest = (bundle) => createHash("sha256").update(serializeEvidenceBundle(bundle)).digest("hex");
249
+ const same = (left, right) => left.length === right.length &&
250
+ left.every((value, index) => value === right[index]);
251
+ const isPrefix = (shorter, longer) => shorter.length < longer.length &&
252
+ shorter.every((value, index) => value === longer[index]);
253
+ const ascii = (left, right) => left < right ? -1 : left > right ? 1 : 0;
254
+ const compareChanges = (left, right) => ascii([
255
+ left.entity,
256
+ left.key,
257
+ left.classification,
258
+ left.left_digest ?? "",
259
+ left.right_digest ?? "",
260
+ ].join("\u0000"), [
261
+ right.entity,
262
+ right.key,
263
+ right.classification,
264
+ right.left_digest ?? "",
265
+ right.right_digest ?? "",
266
+ ].join("\u0000"));
@@ -0,0 +1,346 @@
1
+ import { DirectedGraph } from "graphology";
2
+ import { bidirectional } from "graphology-shortest-path/unweighted.js";
3
+ import { z } from "zod";
4
+ import { evidenceSchema } from "./evidence.js";
5
+ import { parseFunctionEvidence, } from "./functionDossierEvidence.js";
6
+ const addressSchema = z.string().regex(/^0x(?:0|[1-9a-f][0-9a-f]*)$/u);
7
+ const inputAddressSchema = z
8
+ .string()
9
+ .regex(/^0[xX][0-9a-fA-F]+$/u)
10
+ .transform((address) => `0x${BigInt(address).toString(16)}`);
11
+ const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
12
+ const evidenceGroupSchema = z.union([
13
+ evidenceSchema,
14
+ z.array(evidenceSchema).min(1).max(100),
15
+ ]);
16
+ const MAX_EVIDENCE_PAGES = 2_000;
17
+ const MAX_GRAPH_EDGES = 10_000;
18
+ const MAX_PATH_EXPANSIONS = 100_000;
19
+ /** Strict, bounded input for explicit call-path reconstruction. */
20
+ export const callPathInputSchema = z
21
+ .object({
22
+ functions: z.array(evidenceGroupSchema).min(1).max(500),
23
+ start: z.object({ address: inputAddressSchema }).strict(),
24
+ goal: z.object({ address: inputAddressSchema }).strict(),
25
+ max_depth: z.number().int().min(0).max(32).default(8),
26
+ max_paths: z.number().int().min(1).max(100).default(10),
27
+ offset: z.number().int().min(0).default(0),
28
+ limit: z.number().int().min(1).max(100).default(100),
29
+ unknown_registry_approved: z.literal(true).optional(),
30
+ })
31
+ .superRefine(({ functions }, context) => {
32
+ const pages = functions.reduce((total, group) => total + (Array.isArray(group) ? group.length : 1), 0);
33
+ if (pages > MAX_EVIDENCE_PAGES)
34
+ context.addIssue({
35
+ code: "custom",
36
+ message: `Call-path Evidence exceeds ${MAX_EVIDENCE_PAGES} pages`,
37
+ path: ["functions"],
38
+ });
39
+ });
40
+ const citedNodeSchema = z.object({
41
+ address: addressSchema,
42
+ name: z.string().nullable(),
43
+ evidence_links: z.array(evidenceIdSchema).min(1).max(100),
44
+ });
45
+ const citedEdgeSchema = z.object({
46
+ source: addressSchema,
47
+ target: addressSchema,
48
+ evidence_links: z.array(evidenceIdSchema).min(1).max(100),
49
+ });
50
+ const pathSchema = z.object({
51
+ hops: z.number().int().min(0),
52
+ nodes: z.array(citedNodeSchema).min(1).max(33),
53
+ edges: z.array(citedEdgeSchema).max(32),
54
+ evidence_links: z.array(evidenceIdSchema).min(1).max(3_300),
55
+ });
56
+ /** Evidence-cited, bounded directed call-path result. */
57
+ export const callPathResultSchema = z.object({
58
+ status: z.enum(["found", "not_found", "unknown", "truncated"]),
59
+ start: addressSchema,
60
+ goal: addressSchema,
61
+ shortest_hops: z.number().int().min(0).nullable(),
62
+ search_scope: z.object({
63
+ max_depth: z.number().int().min(0).max(32),
64
+ max_paths: z.number().int().min(1).max(100),
65
+ exhaustive: z.boolean(),
66
+ }),
67
+ explored: z.object({
68
+ nodes: z.number().int().min(0),
69
+ edges: z.number().int().min(0),
70
+ depth_reached: z.number().int().min(0),
71
+ }),
72
+ paths: z.object({
73
+ items: z.array(pathSchema).max(100),
74
+ offset: z.number().int().min(0),
75
+ limit: z.number().int().min(1).max(100),
76
+ total: z.number().int().min(0).nullable(),
77
+ returned: z.number().int().min(0).max(100),
78
+ truncated: z.boolean(),
79
+ lower_bound: z.number().int().min(0),
80
+ next_offset: z.number().int().min(0).nullable(),
81
+ }),
82
+ evidence_links: z.array(evidenceIdSchema).min(1).max(50_000),
83
+ limitations: z.array(z.string()),
84
+ });
85
+ /** Reconstruct bounded direct-callee paths from explicit analyze_function Evidence. */
86
+ export const buildCallPath = (input) => {
87
+ const parsed = callPathInputSchema.parse(input);
88
+ const snapshots = parseSnapshots(parsed.functions);
89
+ assertCompatible(snapshots);
90
+ if (!snapshots.has(parsed.start.address))
91
+ throw new TypeError(`No analyze_function Evidence was supplied for start ${parsed.start.address}`);
92
+ const graph = createGraph(snapshots);
93
+ const search = inspectSearch({
94
+ graph,
95
+ snapshots,
96
+ start: parsed.start.address,
97
+ goal: parsed.goal.address,
98
+ maxDepth: parsed.max_depth,
99
+ });
100
+ const shortest = graph.hasNode(parsed.start.address) && graph.hasNode(parsed.goal.address)
101
+ ? bidirectional(graph, parsed.start.address, parsed.goal.address)
102
+ : null;
103
+ const enumeration = shortest === null || shortest.length - 1 > parsed.max_depth
104
+ ? { paths: [], budgetExhausted: false }
105
+ : enumeratePaths({
106
+ graph,
107
+ start: parsed.start.address,
108
+ goal: parsed.goal.address,
109
+ shortestDepth: shortest.length - 1,
110
+ maxDepth: parsed.max_depth,
111
+ limit: parsed.max_paths + 1,
112
+ });
113
+ const hasKnownExtraPath = enumeration.paths.length > parsed.max_paths;
114
+ const capped = hasKnownExtraPath || enumeration.budgetExhausted;
115
+ const retained = enumeration.paths.slice(0, parsed.max_paths);
116
+ const paths = retained.map((path) => citePath(path, snapshots));
117
+ const total = paths.length;
118
+ const items = paths.slice(parsed.offset, parsed.offset + parsed.limit);
119
+ const found = paths.length > 0;
120
+ const exhaustive = search.exhaustive && !capped;
121
+ const status = callPathStatus({ capped, found, exhaustive });
122
+ const limitations = [...search.limitations];
123
+ if (capped)
124
+ limitations.push(enumeration.budgetExhausted
125
+ ? `Path enumeration stopped at ${MAX_PATH_EXPANSIONS} expansions`
126
+ : `Path enumeration stopped at max_paths=${parsed.max_paths}`);
127
+ if (!found && shortest !== null && shortest.length - 1 > parsed.max_depth)
128
+ limitations.push(`The shortest observed path exceeds max_depth=${parsed.max_depth}`);
129
+ return callPathResultSchema.parse({
130
+ status,
131
+ start: parsed.start.address,
132
+ goal: parsed.goal.address,
133
+ shortest_hops: found ? (paths[0]?.hops ?? null) : null,
134
+ search_scope: {
135
+ max_depth: parsed.max_depth,
136
+ max_paths: parsed.max_paths,
137
+ exhaustive,
138
+ },
139
+ explored: {
140
+ nodes: search.reached.size,
141
+ edges: [...search.reached.keys()].reduce((count, node) => count + (graph.hasNode(node) ? graph.outDegree(node) : 0), 0),
142
+ depth_reached: Math.max(0, ...search.reached.values()),
143
+ },
144
+ paths: {
145
+ items,
146
+ offset: parsed.offset,
147
+ limit: parsed.limit,
148
+ total: capped ? null : total,
149
+ returned: items.length,
150
+ truncated: capped,
151
+ lower_bound: total + (hasKnownExtraPath ? 1 : 0),
152
+ next_offset: parsed.offset + items.length < total
153
+ ? parsed.offset + items.length
154
+ : null,
155
+ },
156
+ evidence_links: uniqueEvidence(snapshots.values()),
157
+ limitations: [...new Set(limitations)].sort((left, right) => left.localeCompare(right)),
158
+ });
159
+ };
160
+ const callPathStatus = ({ capped, found, exhaustive, }) => {
161
+ if (capped)
162
+ return "truncated";
163
+ if (found)
164
+ return "found";
165
+ return exhaustive ? "not_found" : "unknown";
166
+ };
167
+ const parseSnapshots = (groups) => {
168
+ const snapshots = new Map();
169
+ for (const group of groups) {
170
+ const snapshot = parseFunctionEvidence(group);
171
+ const address = normalizeAddress(snapshot.procedure.address);
172
+ for (const callee of snapshot.collections.callees.items)
173
+ normalizeAddress(callee.address);
174
+ if (snapshots.has(address))
175
+ throw new TypeError(`Duplicate function Evidence for ${address}`);
176
+ snapshots.set(address, snapshot);
177
+ }
178
+ return snapshots;
179
+ };
180
+ const assertCompatible = (snapshots) => {
181
+ const first = snapshots.values().next().value;
182
+ if (first === undefined)
183
+ return;
184
+ for (const snapshot of snapshots.values()) {
185
+ if (snapshot.subject.digest.sha256 !== first.subject.digest.sha256 ||
186
+ snapshot.subject.format !== first.subject.format ||
187
+ snapshot.subject.architecture !== first.subject.architecture)
188
+ throw new TypeError("Call-path Evidence mixes artifact subjects");
189
+ if (snapshot.provider.id !== first.provider.id ||
190
+ snapshot.provider.name !== first.provider.name ||
191
+ snapshot.provider.version !== first.provider.version)
192
+ throw new TypeError("Call-path Evidence mixes providers");
193
+ }
194
+ };
195
+ const createGraph = (snapshots) => {
196
+ const graph = new DirectedGraph({ allowSelfLoops: true, multi: false });
197
+ for (const [address, snapshot] of snapshots) {
198
+ graph.mergeNode(address);
199
+ for (const callee of snapshot.collections.callees.items) {
200
+ const calleeAddress = normalizeAddress(callee.address);
201
+ graph.mergeNode(calleeAddress);
202
+ graph.mergeDirectedEdge(address, calleeAddress);
203
+ if (graph.size > MAX_GRAPH_EDGES)
204
+ throw new TypeError(`Call graph exceeds ${MAX_GRAPH_EDGES} directed edges`);
205
+ }
206
+ }
207
+ return graph;
208
+ };
209
+ const inspectSearch = ({ graph, snapshots, start, goal, maxDepth, }) => {
210
+ if (!graph.hasNode(start))
211
+ return {
212
+ graph,
213
+ snapshots,
214
+ reached: new Map(),
215
+ exhaustive: false,
216
+ limitations: [
217
+ `No analyze_function Evidence was supplied for start ${start}`,
218
+ ],
219
+ };
220
+ const reached = new Map([[start, 0]]);
221
+ const queue = [start];
222
+ const limitations = [];
223
+ for (let index = 0; index < queue.length; index += 1) {
224
+ const node = queue[index];
225
+ if (node === undefined)
226
+ continue;
227
+ const depth = reached.get(node) ?? 0;
228
+ if (node === goal)
229
+ continue;
230
+ const snapshot = snapshots.get(node);
231
+ if (snapshot === undefined) {
232
+ limitations.push(`No analyze_function Evidence covers reachable function ${node}`);
233
+ continue;
234
+ }
235
+ if (!snapshot.collections.callees.complete)
236
+ limitations.push(`Callee coverage is incomplete for reachable function ${node}`);
237
+ const neighbors = graph
238
+ .outNeighbors(node)
239
+ .sort((left, right) => left.localeCompare(right));
240
+ if (depth === maxDepth) {
241
+ if (neighbors.length > 0)
242
+ limitations.push(`Search reached max_depth=${maxDepth} at ${node}`);
243
+ continue;
244
+ }
245
+ for (const neighbor of neighbors)
246
+ if (!reached.has(neighbor)) {
247
+ reached.set(neighbor, depth + 1);
248
+ queue.push(neighbor);
249
+ }
250
+ }
251
+ return {
252
+ graph,
253
+ snapshots,
254
+ reached,
255
+ exhaustive: limitations.length === 0,
256
+ limitations,
257
+ };
258
+ };
259
+ const enumeratePaths = ({ graph, start, goal, shortestDepth, maxDepth, limit, }) => {
260
+ const output = [];
261
+ const state = {
262
+ graph,
263
+ goal,
264
+ path: [start],
265
+ visited: new Set([start]),
266
+ output,
267
+ limit,
268
+ expansions: 0,
269
+ budgetExhausted: false,
270
+ };
271
+ for (let depth = shortestDepth; depth <= maxDepth && output.length < limit; depth += 1)
272
+ enumerateAtDepth(state, depth);
273
+ return { paths: output, budgetExhausted: state.budgetExhausted };
274
+ };
275
+ const enumerateAtDepth = (state, remaining) => {
276
+ const { graph, goal, path, visited, output, limit } = state;
277
+ state.expansions += 1;
278
+ if (state.expansions > MAX_PATH_EXPANSIONS) {
279
+ state.budgetExhausted = true;
280
+ return;
281
+ }
282
+ const current = path.at(-1);
283
+ if (current === undefined || output.length >= limit)
284
+ return;
285
+ if (remaining === 0) {
286
+ if (current === goal)
287
+ output.push([...path]);
288
+ return;
289
+ }
290
+ if (current === goal)
291
+ return;
292
+ for (const neighbor of graph
293
+ .outNeighbors(current)
294
+ .sort((left, right) => left.localeCompare(right))) {
295
+ if (visited.has(neighbor))
296
+ continue;
297
+ visited.add(neighbor);
298
+ path.push(neighbor);
299
+ enumerateAtDepth(state, remaining - 1);
300
+ path.pop();
301
+ visited.delete(neighbor);
302
+ if (output.length >= limit || state.budgetExhausted)
303
+ return;
304
+ }
305
+ };
306
+ const citePath = (addresses, snapshots) => {
307
+ const edges = addresses.slice(0, -1).map((source, index) => {
308
+ const target = addresses[index + 1];
309
+ if (target === undefined)
310
+ throw new TypeError("Call path has an invalid edge");
311
+ return {
312
+ source,
313
+ target,
314
+ evidence_links: snapshotLinks(snapshots.get(source)),
315
+ };
316
+ });
317
+ const nodes = addresses.map((address, index) => {
318
+ const snapshot = snapshots.get(address);
319
+ const supporting = snapshot ??
320
+ (index > 0 ? snapshots.get(addresses[index - 1] ?? "") : undefined);
321
+ return {
322
+ address,
323
+ name: snapshot?.procedure.name ?? null,
324
+ evidence_links: snapshotLinks(supporting),
325
+ };
326
+ });
327
+ return {
328
+ hops: edges.length,
329
+ nodes,
330
+ edges,
331
+ evidence_links: [
332
+ ...new Set(edges
333
+ .flatMap(({ evidence_links: links }) => links)
334
+ .concat(nodes.flatMap(({ evidence_links: links }) => links))),
335
+ ],
336
+ };
337
+ };
338
+ const snapshotLinks = (snapshot) => {
339
+ if (snapshot === undefined)
340
+ throw new TypeError("Every call-path claim requires supporting Evidence");
341
+ return snapshot.evidence.map(({ evidence_id }) => evidence_id);
342
+ };
343
+ const uniqueEvidence = (snapshots) => [
344
+ ...new Set([...snapshots].flatMap((snapshot) => snapshotLinks(snapshot))),
345
+ ].sort((left, right) => left.localeCompare(right));
346
+ const normalizeAddress = (input) => inputAddressSchema.parse(input);