rea-agents 0.3.0 → 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 +149 -2
  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/referenceSourcePolicy.js +1 -0
  79. package/dist/domain/residualUnknown.js +239 -0
  80. package/dist/domain/staticRuntimeCorrelation.js +375 -0
  81. package/dist/hopper/BridgeLauncher.js +40 -3
  82. package/dist/hopper/HopperClient.js +14 -5
  83. package/dist/hopper/HopperProvider.js +57 -22
  84. package/dist/identity.js +1 -0
  85. package/dist/main.js +5 -1
  86. package/dist/native/CommandRunner.js +156 -0
  87. package/dist/native/NativeMacOSProvider.js +306 -0
  88. package/dist/native/NativeMachoInspection.js +135 -0
  89. package/dist/native/parsers/codesign.js +55 -0
  90. package/dist/native/parsers/demangle.js +26 -0
  91. package/dist/native/parsers/dyldInfo.js +25 -0
  92. package/dist/native/parsers/lipo.js +67 -0
  93. package/dist/native/parsers/otool.js +193 -0
  94. package/dist/native/parsers/plist.js +23 -0
  95. package/dist/reference/ReferenceSourceReader.js +73 -0
  96. package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
  97. package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
  98. package/dist/reference/ReferenceSourceReaderFile.js +119 -0
  99. package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
  100. package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
  101. package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
  102. package/dist/server/createServer.js +31 -5
  103. package/dist/server/recordDerivedEvidence.js +10 -0
  104. package/dist/server/registerArtifactComparisonTool.js +62 -0
  105. package/dist/server/registerArtifactTools.js +6 -0
  106. package/dist/server/registerBundleComparisonTool.js +47 -0
  107. package/dist/server/registerEnhancedTools.js +64 -12
  108. package/dist/server/registerEvidenceTools.js +36 -0
  109. package/dist/server/registerFunctionComparisonTool.js +68 -0
  110. package/dist/server/registerInvestigationTools.js +224 -0
  111. package/dist/server/registerNativeTools.js +6 -0
  112. package/dist/server/registerOfficialTools.js +47 -14
  113. package/dist/server/registerProcessComparisonTool.js +106 -0
  114. package/dist/server/registerSessionTools.js +179 -70
  115. package/dist/server/sessionEvidence.js +28 -0
  116. package/dist/server/sessionToolPolicies.js +64 -0
  117. package/dist/server/toolRegistrationOptions.js +7 -0
  118. package/dist/server/toolResult.js +8 -5
  119. package/install.sh +198 -0
  120. package/package.json +18 -1
  121. package/scripts/rea.mjs +5 -1
  122. package/skills/rea-analysis/SKILL.md +77 -2
@@ -1,21 +1,90 @@
1
1
  import { z } from "zod";
2
2
  import { evidenceSchema } from "../domain/evidence.js";
3
- import { processCaptureSchema } from "../domain/processCapture.js";
3
+ import { processCaptureComparisonSchema, processCaptureSchema, } from "../domain/processCapture.js";
4
4
  import { evidenceBundleSchema } from "../domain/evidenceBundle.js";
5
- const resultOf = (schema) => evidenceSchema.omit({ result: true }).extend({ result: schema });
5
+ import { residualUnknownSchema } from "../domain/residualUnknown.js";
6
+ import { functionDossierSchema } from "../domain/hopperValues.js";
7
+ import { demangleSwiftSchema, inspectMachoSchema, inspectPlistSchema, inspectSignatureSchema, listArchitecturesSchema, } from "../domain/nativeInspection.js";
8
+ import { artifactExtractionResultSchema, artifactInventoryResultSchema, } from "../domain/artifactGraph.js";
9
+ import { artifactComparisonResultSchema } from "../domain/artifactComparison.js";
10
+ import { functionComparisonResultSchema } from "../domain/functionComparison.js";
11
+ import { bundleComparisonResultSchema } from "../domain/bundleComparison.js";
12
+ import { changedBehaviorResultSchema } from "../domain/changedBehavior.js";
13
+ import { callPathResultSchema } from "../domain/callPath.js";
14
+ import { staticRuntimeCorrelationResultSchema } from "../domain/staticRuntimeCorrelation.js";
15
+ import { reconstructionVerificationResultSchema } from "../domain/reconstructionVerification.js";
16
+ const resultOf = (schema) => evidenceSchema
17
+ .omit({ normalized_result: true })
18
+ .extend({ normalized_result: schema });
6
19
  const lifecycleResultOf = (schema) => z.object({ result: schema });
7
- const comparisonStatus = z.enum([
8
- "unchanged",
9
- "added",
10
- "removed",
11
- "changed",
12
- "truncated",
13
- "unknown",
20
+ /** Resolve a required named output schema or reject contract drift. */
21
+ export const requireOutputSchema = (schemas, name) => {
22
+ const schema = schemas[name];
23
+ if (schema === undefined)
24
+ throw new Error(`Missing output schema for ${name}`);
25
+ return schema;
26
+ };
27
+ const targetFormatSchema = z.enum([
28
+ "hopper",
29
+ "mach-o",
30
+ "elf",
31
+ "pe",
32
+ "zip",
33
+ "ipa",
34
+ "apk",
35
+ "asar",
36
+ "dmg",
37
+ "pkg",
38
+ "plist",
39
+ "javascript",
40
+ "source-map",
41
+ ]);
42
+ const targetKindSchema = z.enum([
43
+ "executable",
44
+ "database",
45
+ "archive",
46
+ "artifact",
14
47
  ]);
48
+ const providerCapability = z.object({
49
+ operation: z.string(),
50
+ available: z.boolean(),
51
+ reason: z.string().nullable(),
52
+ input_contract_version: z.number().int().min(1),
53
+ output_contract_version: z.number().int().min(1),
54
+ pagination: z.enum(["none", "offset", "cursor"]),
55
+ exhaustive: z.boolean(),
56
+ effects: z.object({
57
+ mutates_artifact: z.boolean(),
58
+ launches_process: z.boolean(),
59
+ may_show_ui: z.boolean(),
60
+ may_access_network: z.boolean(),
61
+ may_write_filesystem: z.boolean(),
62
+ changes_permissions: z.boolean(),
63
+ requires_root: z.boolean(),
64
+ }),
65
+ limits: z.object({
66
+ max_results: z.number().int().min(0).nullable(),
67
+ max_payload_bytes: z.number().int().min(0).nullable(),
68
+ timeout_ms: z.number().int().min(0).nullable(),
69
+ }),
70
+ limitations: z.array(z.string()),
71
+ });
72
+ const sessionProvider = z.object({
73
+ provider: z.object({
74
+ id: z.string(),
75
+ name: z.string(),
76
+ version: z.string().nullable(),
77
+ }),
78
+ capabilities: z.array(providerCapability),
79
+ });
15
80
  const nullableText = z.string().nullable();
16
81
  const addressList = z.array(z.string());
17
82
  const addressedEntry = z.object({ address: z.string(), name: z.string() });
18
83
  const procedureIdentity = z.object({ address: z.string(), name: z.string() });
84
+ const localVariable = z.object({
85
+ description: z.string(),
86
+ provenance: z.literal("hopper-public-python-api"),
87
+ });
19
88
  const containingProcedureResolution = z.discriminatedUnion("found", [
20
89
  z.object({
21
90
  query_address: z.string(),
@@ -48,22 +117,31 @@ const pageOutput = z.object({
48
117
  next_offset: z.number().int().min(0).nullable(),
49
118
  has_more: z.boolean(),
50
119
  });
51
- const segmentOutput = resultOf(z.array(z.object({
120
+ const searchPageOutput = pageOutput.extend({
121
+ items: z.array(z.object({
122
+ address: z.string(),
123
+ value: z.string(),
124
+ value_truncated: z.boolean(),
125
+ })),
126
+ });
127
+ const memoryRegionOutput = z.object({
52
128
  name: z.string(),
53
129
  start: z.string(),
54
130
  end: z.string(),
131
+ readable: z.boolean().nullable(),
55
132
  writable: z.boolean().nullable(),
56
133
  executable: z.boolean().nullable(),
57
134
  permissions: unavailable,
58
- sections: z.array(z.object({ name: z.string(), start: z.string(), end: z.string() })),
59
- })));
135
+ provenance: z.literal("hopper-public-python-api"),
136
+ });
137
+ const segmentOutput = resultOf(z.array(memoryRegionOutput.extend({ sections: z.array(memoryRegionOutput) })));
60
138
  const procedureInfoOutput = resultOf(z.object({
61
139
  name: z.string(),
62
140
  entrypoint: z.string(),
63
141
  basicblock_count: z.number().int().min(0),
64
142
  length: z.number().min(0),
65
143
  signature: nullableText,
66
- locals: z.array(z.json()),
144
+ locals: z.array(localVariable),
67
145
  }));
68
146
  const symbolDiscoveryOutput = (property) => resultOf(z.object({
69
147
  count: z.number().int().min(0),
@@ -73,54 +151,7 @@ const graphNode = z.union([
73
151
  z.object({ address: z.string(), calls: z.array(z.string()) }),
74
152
  z.object({ address: z.string(), error: z.string() }),
75
153
  ]);
76
- const referenceEdge = z.object({
77
- source_address: z.string(),
78
- target_address: z.string(),
79
- source_procedure: procedureIdentity.nullable(),
80
- target_procedure: procedureIdentity.nullable(),
81
- kind: unavailable,
82
- });
83
- const referencedValue = z.object({
84
- address: z.string(),
85
- value: z.string(),
86
- source_address: z.string(),
87
- });
88
- const functionDossierOutput = resultOf(z.object({
89
- procedure: z.object({
90
- address: z.string(),
91
- name: z.string(),
92
- signature: nullableText,
93
- locals: z.array(z.json()),
94
- }),
95
- pseudocode: z.object({
96
- text: z.string(),
97
- total_chars: z.number().int().min(0),
98
- returned_chars: z.number().int().min(0),
99
- truncated: z.boolean(),
100
- next_offset: z.number().int().min(0).nullable(),
101
- }),
102
- assembly: bounded(z.string()),
103
- comments: bounded(z.object({
104
- address: z.string(),
105
- kind: z.enum(["comment", "inline"]),
106
- text: z.string(),
107
- })),
108
- callers: bounded(procedureIdentity),
109
- callees: bounded(procedureIdentity),
110
- incoming_references: bounded(referenceEdge),
111
- outgoing_references: bounded(referenceEdge),
112
- referenced_strings: bounded(referencedValue),
113
- referenced_names: bounded(referencedValue),
114
- basic_blocks: bounded(z.object({
115
- start: z.string(),
116
- end: z.string(),
117
- successors: z.array(z.string()),
118
- })),
119
- instruction_scan: z.object({
120
- scanned: z.number().int().min(0),
121
- truncated: z.boolean(),
122
- }),
123
- }));
154
+ const functionDossierOutput = resultOf(functionDossierSchema);
124
155
  /** Exact structured-content schemas for the direct Hopper operations. */
125
156
  export const officialOutputSchemas = {
126
157
  address_name: resultOf(nullableText),
@@ -158,8 +189,8 @@ export const officialOutputSchemas = {
158
189
  })),
159
190
  procedure_pseudo_code: resultOf(nullableText),
160
191
  resolve_containing_procedure: resultOf(containingProcedureResolution),
161
- search_procedures: resultOf(z.record(z.string(), z.string())),
162
- search_strings: resultOf(z.record(z.string(), z.string())),
192
+ search_procedures: resultOf(searchPageOutput),
193
+ search_strings: resultOf(searchPageOutput),
163
194
  set_address_name: resultOf(z.boolean()),
164
195
  set_addresses_names: resultOf(z.record(z.string(), z.boolean())),
165
196
  set_bookmark: resultOf(z.boolean()),
@@ -223,45 +254,86 @@ export const enhancedOutputSchemas = {
223
254
  residual_unknowns: z.array(z.string()),
224
255
  })),
225
256
  };
257
+ /** Exact Evidence v2 schemas for provider-neutral native inspection. */
258
+ export const nativeOutputSchemas = {
259
+ inspect_macho: resultOf(inspectMachoSchema),
260
+ inspect_signature: resultOf(inspectSignatureSchema),
261
+ inspect_plist: resultOf(inspectPlistSchema),
262
+ list_architectures: resultOf(listArchitecturesSchema),
263
+ demangle_swift: resultOf(demangleSwiftSchema),
264
+ };
265
+ /** Exact Evidence v2 schemas for provider-neutral artifact graph operations. */
266
+ export const artifactOutputSchemas = {
267
+ inventory_artifact: resultOf(artifactInventoryResultSchema),
268
+ extract_artifact: resultOf(artifactExtractionResultSchema),
269
+ };
226
270
  /** Exact structured-content schemas for target lifecycle operations. */
227
271
  export const sessionOutputSchemas = {
228
272
  open_binary: lifecycleResultOf(z.object({
229
273
  path: z.string(),
230
- format: z.enum(["hopper", "mach-o", "elf", "pe"]),
231
- kind: z.enum(["executable", "database"]),
274
+ format: targetFormatSchema,
275
+ kind: targetKindSchema,
232
276
  loaderArgs: z.array(z.string()),
233
277
  sha256: z.string().regex(/^[a-f0-9]{64}$/u),
234
278
  architecture: z.enum(["x86", "x86_64", "arm", "arm64"]).nullable(),
235
279
  })),
236
280
  close_binary: lifecycleResultOf(z.null()),
237
281
  binary_session: lifecycleResultOf(z.union([
238
- z.object({ open: z.literal(false) }),
239
- z.object({
282
+ sessionProvider.extend({ open: z.literal(false) }),
283
+ sessionProvider.extend({
240
284
  open: z.literal(true),
241
285
  path: z.string(),
242
- format: z.enum(["hopper", "mach-o", "elf", "pe"]),
243
- kind: z.enum(["executable", "database"]),
286
+ format: targetFormatSchema,
287
+ kind: targetKindSchema,
244
288
  sha256: z.string().regex(/^[a-f0-9]{64}$/u),
245
289
  architecture: z.enum(["x86", "x86_64", "arm", "arm64"]).nullable(),
246
290
  }),
247
291
  ])),
248
- export_evidence_bundle: lifecycleResultOf(evidenceBundleSchema),
292
+ export_evidence_bundle: lifecycleResultOf(z.union([
293
+ evidenceBundleSchema,
294
+ z.object({
295
+ path: z.string(),
296
+ bytes: z.number().int().min(0),
297
+ records: z.number().int().min(0),
298
+ }),
299
+ ])),
249
300
  import_evidence_bundle: lifecycleResultOf(z.object({
250
301
  imported: z.number().int().min(0),
251
302
  total: z.number().int().min(0),
252
303
  })),
253
304
  capture_process_scenario: lifecycleResultOf(evidenceSchema
254
- .omit({ result: true })
255
- .extend({ result: processCaptureSchema })),
256
- compare_process_captures: lifecycleResultOf(evidenceSchema.omit({ result: true }).extend({
257
- result: z.object({
258
- status: comparisonStatus,
259
- terminal: comparisonStatus,
260
- exit: comparisonStatus,
261
- filesystem: comparisonStatus,
262
- protocol: comparisonStatus,
263
- process: comparisonStatus,
264
- limitations: z.array(z.string()),
265
- }),
305
+ .omit({ normalized_result: true })
306
+ .extend({ normalized_result: processCaptureSchema })),
307
+ compare_process_captures: lifecycleResultOf(evidenceSchema.omit({ normalized_result: true }).extend({
308
+ normalized_result: processCaptureComparisonSchema,
309
+ })),
310
+ compare_artifacts: lifecycleResultOf(evidenceSchema.omit({ normalized_result: true }).extend({
311
+ normalized_result: artifactComparisonResultSchema,
312
+ })),
313
+ compare_functions: lifecycleResultOf(evidenceSchema.omit({ normalized_result: true }).extend({
314
+ normalized_result: functionComparisonResultSchema,
315
+ })),
316
+ compare_bundles: lifecycleResultOf(evidenceSchema.omit({ normalized_result: true }).extend({
317
+ normalized_result: bundleComparisonResultSchema,
318
+ })),
319
+ find_changed_behavior: lifecycleResultOf(evidenceSchema.omit({ normalized_result: true }).extend({
320
+ normalized_result: changedBehaviorResultSchema,
321
+ })),
322
+ build_call_path: lifecycleResultOf(evidenceSchema.omit({ normalized_result: true }).extend({
323
+ normalized_result: callPathResultSchema,
324
+ })),
325
+ correlate_static_and_runtime: lifecycleResultOf(evidenceSchema.omit({ normalized_result: true }).extend({
326
+ normalized_result: staticRuntimeCorrelationResultSchema,
327
+ })),
328
+ verify_reconstruction: lifecycleResultOf(evidenceSchema.omit({ normalized_result: true }).extend({
329
+ normalized_result: reconstructionVerificationResultSchema,
330
+ })),
331
+ list_unknowns: lifecycleResultOf(z.array(residualUnknownSchema)),
332
+ record_unknown: lifecycleResultOf(residualUnknownSchema),
333
+ update_unknown: lifecycleResultOf(residualUnknownSchema),
334
+ verify_unknown_resolution: lifecycleResultOf(z.object({
335
+ valid: z.boolean(),
336
+ truthVerified: z.boolean(),
337
+ unknown: residualUnknownSchema,
266
338
  })),
267
339
  };
@@ -0,0 +1,33 @@
1
+ /** Exact example inputs for residual-unknown lifecycle contracts. */
2
+ export const UNKNOWN_CONTRACT_EXAMPLES = {
3
+ list_unknowns: {},
4
+ record_unknown: {
5
+ approved: true,
6
+ question: "Does this branch require an unavailable external service?",
7
+ severity: "medium",
8
+ domain: "protocol",
9
+ required_authority: "controlled-replay",
10
+ required_confidence: "observed",
11
+ required_environment: null,
12
+ recommended_probes: [
13
+ { operation: "capture_process_scenario", rationale: "Replay branch." },
14
+ ],
15
+ relationships: [],
16
+ },
17
+ update_unknown: {
18
+ approved: true,
19
+ unknown_id: `unk_${"0".repeat(64)}`,
20
+ expected_revision: 1,
21
+ status: "investigating",
22
+ severity: "medium",
23
+ supporting_evidence_ids: [],
24
+ contradicting_evidence_ids: [],
25
+ required_authority: "controlled-replay",
26
+ required_confidence: "observed",
27
+ required_environment: null,
28
+ recommended_probes: [],
29
+ relationships: [],
30
+ resolution: null,
31
+ },
32
+ verify_unknown_resolution: { unknown_id: `unk_${"0".repeat(64)}` },
33
+ };
@@ -0,0 +1,273 @@
1
+ import canonicalize from "canonicalize";
2
+ import { z } from "zod";
3
+ import {} from "./artifactGraph.js";
4
+ import { evidenceSchema } from "./evidence.js";
5
+ import { parseArtifactInventoryEvidence, } from "./artifactInventoryEvidence.js";
6
+ const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
7
+ const comparisonStatusSchema = z.enum([
8
+ "unchanged",
9
+ "added",
10
+ "removed",
11
+ "changed",
12
+ "truncated",
13
+ "unknown",
14
+ ]);
15
+ const changeKindSchema = z.enum(["added", "removed", "changed", "unknown"]);
16
+ const comparisonDimensionSchema = z.enum([
17
+ "content",
18
+ "kind",
19
+ "format",
20
+ "size",
21
+ "executable",
22
+ "relations",
23
+ "metadata",
24
+ "availability",
25
+ ]);
26
+ /** Strict Evidence-backed input for deterministic artifact comparison. */
27
+ export const artifactComparisonInputSchema = z.object({
28
+ left: z.union([evidenceSchema, z.array(evidenceSchema).min(1).max(100)]),
29
+ right: z.union([evidenceSchema, z.array(evidenceSchema).min(1).max(100)]),
30
+ offset: z.number().int().min(0).default(0),
31
+ limit: z.number().int().min(1).max(500).default(100),
32
+ unknown_registry_approved: z
33
+ .literal(true)
34
+ .optional()
35
+ .describe("Explicit approval to record incomplete or differing evidence"),
36
+ });
37
+ /** One path-classified artifact change with citations to both observations. */
38
+ const artifactChangeSchema = z.object({
39
+ classification: changeKindSchema,
40
+ logical_path: z.string().min(1).max(4_096),
41
+ dimensions: z.array(comparisonDimensionSchema).min(1).max(8),
42
+ left_occurrence_id: z
43
+ .string()
44
+ .regex(/^occ_[a-f0-9]{64}$/u)
45
+ .nullable(),
46
+ right_occurrence_id: z
47
+ .string()
48
+ .regex(/^occ_[a-f0-9]{64}$/u)
49
+ .nullable(),
50
+ left_artifact_id: z
51
+ .string()
52
+ .regex(/^art_[a-f0-9]{64}$/u)
53
+ .nullable(),
54
+ right_artifact_id: z
55
+ .string()
56
+ .regex(/^art_[a-f0-9]{64}$/u)
57
+ .nullable(),
58
+ evidence_links: z.array(evidenceIdSchema).min(2).max(200),
59
+ });
60
+ /** Bounded deterministic artifact comparison with an independently paged delta. */
61
+ export const artifactComparisonResultSchema = z.object({
62
+ status: comparisonStatusSchema,
63
+ left_manifest_id: z.string().regex(/^agm_[a-f0-9]{64}$/u),
64
+ right_manifest_id: z.string().regex(/^agm_[a-f0-9]{64}$/u),
65
+ summary: z.object({
66
+ unchanged: z.number().int().min(0),
67
+ added: z.number().int().min(0),
68
+ removed: z.number().int().min(0),
69
+ changed: z.number().int().min(0),
70
+ unknown: z.number().int().min(0),
71
+ }),
72
+ changes: z.object({
73
+ items: z.array(artifactChangeSchema).max(500),
74
+ offset: z.number().int().min(0),
75
+ limit: z.number().int().min(1).max(500),
76
+ total: z.number().int().min(0),
77
+ next_offset: z.number().int().min(0).nullable(),
78
+ }),
79
+ limitations: z.array(z.string()),
80
+ });
81
+ /** Compare complete inventory pages without treating missing evidence as equality. */
82
+ export const compareArtifacts = (leftEvidence, rightEvidence, offset, limit) => {
83
+ const left = parseArtifactInventoryEvidence(leftEvidence);
84
+ const right = parseArtifactInventoryEvidence(rightEvidence);
85
+ const leftComplete = left.inventory.complete;
86
+ const rightComplete = right.inventory.complete;
87
+ const leftCovered = leftComplete && left.inventory.limitations.length === 0;
88
+ const rightCovered = rightComplete && right.inventory.limitations.length === 0;
89
+ const limitations = [
90
+ ...left.inventory.limitations.map((item) => `Left: ${item}`),
91
+ ...right.inventory.limitations.map((item) => `Right: ${item}`),
92
+ ...(leftComplete ? [] : ["Left artifact inventory is incomplete."]),
93
+ ...(rightComplete ? [] : ["Right artifact inventory is incomplete."]),
94
+ ];
95
+ const changes = compareOccurrences(left.inventory, right.inventory, [
96
+ ...left.evidence.map(({ evidence_id: id }) => id),
97
+ ...right.evidence.map(({ evidence_id: id }) => id),
98
+ ], leftCovered && rightCovered);
99
+ const summary = summarize(left.inventory, right.inventory, changes, leftCovered && rightCovered);
100
+ const page = changes.slice(offset, offset + limit);
101
+ return artifactComparisonResultSchema.parse({
102
+ status: !leftComplete || !rightComplete
103
+ ? "truncated"
104
+ : !leftCovered || !rightCovered
105
+ ? "unknown"
106
+ : changes.some(({ classification }) => classification === "unknown")
107
+ ? "unknown"
108
+ : changes.length === 0
109
+ ? "unchanged"
110
+ : "changed",
111
+ left_manifest_id: left.inventory.manifest.manifest_id,
112
+ right_manifest_id: right.inventory.manifest.manifest_id,
113
+ summary,
114
+ changes: {
115
+ items: page,
116
+ offset,
117
+ limit,
118
+ total: changes.length,
119
+ next_offset: offset + page.length < changes.length ? offset + page.length : null,
120
+ },
121
+ limitations: [...new Set(limitations)].sort((a, b) => a.localeCompare(b)),
122
+ });
123
+ };
124
+ const compareOccurrences = (left, right, evidenceLinks, complete) => {
125
+ const leftByPath = new Map(left.occurrences.map((item) => [item.logical_path, item]));
126
+ const rightByPath = new Map(right.occurrences.map((item) => [item.logical_path, item]));
127
+ const leftNodes = new Map(left.nodes.map((item) => [item.artifact_id, item]));
128
+ const rightNodes = new Map(right.nodes.map((item) => [item.artifact_id, item]));
129
+ const leftRelations = relationsByPath(left);
130
+ const rightRelations = relationsByPath(right);
131
+ const paths = [
132
+ ...new Set([...leftByPath.keys(), ...rightByPath.keys()]),
133
+ ].sort((a, b) => a.localeCompare(b, "en"));
134
+ const output = [];
135
+ for (const path of paths) {
136
+ const leftOccurrence = leftByPath.get(path);
137
+ const rightOccurrence = rightByPath.get(path);
138
+ const change = classifyPath({
139
+ path,
140
+ leftOccurrence,
141
+ rightOccurrence,
142
+ leftNode: nodeFor(leftOccurrence, leftNodes),
143
+ rightNode: nodeFor(rightOccurrence, rightNodes),
144
+ leftRelations: leftRelations.get(path) ?? [],
145
+ rightRelations: rightRelations.get(path) ?? [],
146
+ complete,
147
+ evidenceLinks,
148
+ });
149
+ if (change !== null)
150
+ output.push(change);
151
+ }
152
+ return output;
153
+ };
154
+ const classifyPath = (input) => {
155
+ const base = {
156
+ logical_path: input.path,
157
+ left_occurrence_id: input.leftOccurrence?.occurrence_id ?? null,
158
+ right_occurrence_id: input.rightOccurrence?.occurrence_id ?? null,
159
+ left_artifact_id: input.leftOccurrence?.artifact_id ?? null,
160
+ right_artifact_id: input.rightOccurrence?.artifact_id ?? null,
161
+ evidence_links: [...input.evidenceLinks],
162
+ };
163
+ if (input.leftOccurrence === undefined)
164
+ return {
165
+ ...base,
166
+ classification: input.complete ? "added" : "unknown",
167
+ dimensions: [input.complete ? "content" : "availability"],
168
+ };
169
+ if (input.rightOccurrence === undefined)
170
+ return {
171
+ ...base,
172
+ classification: input.complete ? "removed" : "unknown",
173
+ dimensions: [input.complete ? "content" : "availability"],
174
+ };
175
+ if (input.leftOccurrence.hash_status !== "verified" ||
176
+ input.rightOccurrence.hash_status !== "verified" ||
177
+ input.leftNode === undefined ||
178
+ input.rightNode === undefined)
179
+ return { ...base, classification: "unknown", dimensions: ["availability"] };
180
+ const dimensions = changedDimensions(input);
181
+ return dimensions.length === 0
182
+ ? null
183
+ : { ...base, classification: "changed", dimensions };
184
+ };
185
+ const changedDimensions = (input) => {
186
+ const left = input.leftNode;
187
+ const right = input.rightNode;
188
+ if (left === undefined || right === undefined)
189
+ return ["availability"];
190
+ const checks = [
191
+ ["content", left.sha256 !== right.sha256],
192
+ ["kind", left.kind !== right.kind],
193
+ ["format", left.format !== right.format],
194
+ ["size", left.size !== right.size],
195
+ [
196
+ "executable",
197
+ input.leftOccurrence?.executable !== input.rightOccurrence?.executable,
198
+ ],
199
+ [
200
+ "relations",
201
+ JSON.stringify(input.leftRelations) !==
202
+ JSON.stringify(input.rightRelations),
203
+ ],
204
+ ["metadata", metadataChanged(input, left, right)],
205
+ ];
206
+ return checks
207
+ .filter(([, changed]) => changed)
208
+ .map(([dimension]) => dimension);
209
+ };
210
+ const metadataChanged = (input, left, right) => left.media_type !== right.media_type ||
211
+ left.architecture !== right.architecture ||
212
+ left.executable !== right.executable ||
213
+ left.content_state !== right.content_state ||
214
+ input.leftOccurrence?.entry_kind !== input.rightOccurrence?.entry_kind ||
215
+ input.leftOccurrence?.encrypted !== input.rightOccurrence?.encrypted ||
216
+ JSON.stringify(left.limitations) !== JSON.stringify(right.limitations) ||
217
+ JSON.stringify(input.leftOccurrence?.limitations) !==
218
+ JSON.stringify(input.rightOccurrence?.limitations);
219
+ const canonicalJson = (value) => {
220
+ const encoded = canonicalize(value);
221
+ if (encoded === undefined)
222
+ throw new TypeError("Artifact comparison could not canonicalize graph data");
223
+ return encoded;
224
+ };
225
+ const nodeFor = (occurrence, nodes) => occurrence?.artifact_id === null || occurrence?.artifact_id === undefined
226
+ ? undefined
227
+ : nodes.get(occurrence.artifact_id);
228
+ const relationsByPath = (inventory) => {
229
+ const output = new Map();
230
+ const occurrences = new Map(inventory.occurrences.map((item) => [item.occurrence_id, item]));
231
+ for (const edge of inventory.edges) {
232
+ const path = edge.logical_path ?? ".";
233
+ const values = output.get(path);
234
+ const occurrence = occurrences.get(edge.occurrence_id);
235
+ const projection = {
236
+ parent_logical_path: occurrence?.parent_occurrence_id === null ||
237
+ occurrence?.parent_occurrence_id === undefined
238
+ ? null
239
+ : (occurrences.get(occurrence.parent_occurrence_id)?.logical_path ??
240
+ null),
241
+ child_artifact_id: edge.child_artifact_id,
242
+ relation: edge.relation,
243
+ logical_path: edge.logical_path,
244
+ producer: edge.producer,
245
+ };
246
+ if (values === undefined)
247
+ output.set(path, [projection]);
248
+ else
249
+ values.push(projection);
250
+ }
251
+ for (const values of output.values())
252
+ values.sort((left, right) => canonicalJson(left).localeCompare(canonicalJson(right)));
253
+ return output;
254
+ };
255
+ const summarize = (left, right, changes, covered) => {
256
+ const counts = { added: 0, removed: 0, changed: 0, unknown: 0 };
257
+ for (const change of changes)
258
+ counts[change.classification] += 1;
259
+ return {
260
+ unchanged: covered
261
+ ? Math.max(0, Math.min(left.occurrences.length, right.occurrences.length) -
262
+ counts.changed -
263
+ counts.unknown)
264
+ : 0,
265
+ ...counts,
266
+ unknown: covered
267
+ ? counts.unknown
268
+ : Math.max(counts.unknown, Math.max(left.manifest.occurrence_count, right.manifest.occurrence_count) -
269
+ counts.added -
270
+ counts.removed -
271
+ counts.changed),
272
+ };
273
+ };