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
@@ -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
+ };
@@ -0,0 +1,194 @@
1
+ import { z } from "zod";
2
+ const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/u);
3
+ const artifactIdSchema = z.string().regex(/^art_[a-f0-9]{64}$/u);
4
+ const occurrenceIdSchema = z.string().regex(/^occ_[a-f0-9]{64}$/u);
5
+ const edgeIdSchema = z.string().regex(/^edge_[a-f0-9]{64}$/u);
6
+ const manifestIdSchema = z.string().regex(/^agm_[a-f0-9]{64}$/u);
7
+ const extractionIdSchema = z.string().regex(/^aex_[a-f0-9]{64}$/u);
8
+ const boundedRelativePathSchema = z
9
+ .string()
10
+ .min(1)
11
+ .max(4_096)
12
+ .refine((path) => !path.startsWith("/") &&
13
+ !path.includes("\\") &&
14
+ path.split("/").every((part) => part !== "" && part !== ".."), "Expected a normalized relative POSIX path without traversal");
15
+ /** Closed artifact categories used to traverse application containers. */
16
+ const artifactKindSchema = z.enum([
17
+ "container",
18
+ "executable",
19
+ "dynamic-library",
20
+ "framework",
21
+ "native-addon",
22
+ "javascript",
23
+ "source-map",
24
+ "plist",
25
+ "entitlements",
26
+ "resource",
27
+ "universal-slice",
28
+ "package-metadata",
29
+ "unknown",
30
+ ]);
31
+ /** Recognized storage or application formats; unknown bytes remain explicit. */
32
+ const artifactFormatSchema = z.enum([
33
+ "dmg",
34
+ "zip",
35
+ "pkg",
36
+ "asar",
37
+ "javascript-bundle",
38
+ "source-map",
39
+ "mach-o-universal",
40
+ "mach-o",
41
+ "elf",
42
+ "pe",
43
+ "hopper",
44
+ "ipa",
45
+ "apk",
46
+ "plist",
47
+ "entitlements",
48
+ "directory",
49
+ "file",
50
+ "unknown",
51
+ ]);
52
+ /** Exact bounded policy applied while discovering or extracting artifacts. */
53
+ const artifactTraversalLimitsSchema = z.object({
54
+ max_entries: z.number().int().min(1).max(1_000_000),
55
+ max_total_bytes: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
56
+ max_entry_bytes: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
57
+ max_compression_ratio: z.number().min(1).max(100_000),
58
+ max_depth: z.number().int().min(0).max(100),
59
+ max_path_bytes: z.number().int().min(1).max(65_535),
60
+ });
61
+ /** File byte interval for an artifact derived without writing it to disk. */
62
+ const artifactByteRangeSchema = z.object({
63
+ offset: z.number().int().min(0),
64
+ length: z.number().int().min(0),
65
+ });
66
+ /** Bounded external command provenance, including observable side effects. */
67
+ const artifactCommandSchema = z.object({
68
+ tool: z.string().min(1),
69
+ arguments: z.array(z.string().max(4_096)).max(1_024),
70
+ tool_version: z.string().nullable(),
71
+ executable_sha256: sha256Schema.nullable(),
72
+ exit_code: z.number().int().nullable(),
73
+ effects: z.array(z.enum(["read", "write", "mount"])).max(3),
74
+ });
75
+ /** One content-addressed application artifact in deterministic manifest order. */
76
+ const artifactNodeSchema = z.object({
77
+ artifact_id: artifactIdSchema,
78
+ kind: artifactKindSchema,
79
+ format: artifactFormatSchema,
80
+ sha256: sha256Schema,
81
+ size: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
82
+ media_type: z.string().min(1).nullable(),
83
+ architecture: z.string().min(1).nullable(),
84
+ executable: z.boolean(),
85
+ content_state: z.enum(["materialized", "embedded", "virtual"]),
86
+ limitations: z.array(z.string()),
87
+ });
88
+ /** One path or virtual location where an artifact occurs in its parent. */
89
+ const artifactOccurrenceSchema = z.object({
90
+ occurrence_id: occurrenceIdSchema,
91
+ artifact_id: artifactIdSchema.nullable(),
92
+ parent_occurrence_id: occurrenceIdSchema.nullable(),
93
+ logical_path: boundedRelativePathSchema,
94
+ entry_kind: z.enum(["file", "directory", "symlink", "slice"]),
95
+ declared_size: z
96
+ .number()
97
+ .int()
98
+ .min(0)
99
+ .max(Number.MAX_SAFE_INTEGER)
100
+ .nullable(),
101
+ compressed_size: z
102
+ .number()
103
+ .int()
104
+ .min(0)
105
+ .max(Number.MAX_SAFE_INTEGER)
106
+ .nullable(),
107
+ executable: z.boolean(),
108
+ encrypted: z.boolean(),
109
+ hash_status: z.enum(["verified", "not-hashed-limit", "unavailable"]),
110
+ source_location: artifactByteRangeSchema.nullable(),
111
+ limitations: z.array(z.string()),
112
+ });
113
+ /** Parent-child derivation relation between two content-addressed artifacts. */
114
+ const artifactEdgeSchema = z.object({
115
+ edge_id: edgeIdSchema,
116
+ ordinal: z.number().int().min(0),
117
+ parent_artifact_id: artifactIdSchema,
118
+ child_artifact_id: artifactIdSchema,
119
+ relation: z.enum([
120
+ "contains",
121
+ "extracts",
122
+ "slice-of",
123
+ "embeds",
124
+ "loads",
125
+ "maps-source",
126
+ "derived-from",
127
+ ]),
128
+ occurrence_id: occurrenceIdSchema,
129
+ logical_path: boundedRelativePathSchema.nullable(),
130
+ producer: artifactCommandSchema.nullable(),
131
+ });
132
+ /** Stable cryptographic commitment and counts for a complete artifact graph. */
133
+ const artifactGraphManifestSchema = z.object({
134
+ schema_version: z.literal(1),
135
+ manifest_id: manifestIdSchema,
136
+ root_artifact_id: artifactIdSchema,
137
+ root_sha256: sha256Schema,
138
+ root_format: artifactFormatSchema,
139
+ graph_sha256: sha256Schema,
140
+ node_count: z.number().int().min(1),
141
+ occurrence_count: z.number().int().min(1),
142
+ edge_count: z.number().int().min(0),
143
+ });
144
+ /** Independently bounded offset page used for deterministic graph traversal. */
145
+ const artifactPageSchema = (item) => z.object({
146
+ items: z.array(item).max(500),
147
+ offset: z.number().int().min(0),
148
+ limit: z.number().int().min(1).max(500),
149
+ total: z.number().int().min(0),
150
+ next_offset: z.number().int().min(0).nullable(),
151
+ });
152
+ /** Inventory observation containing stable manifest identity and bounded pages. */
153
+ export const artifactInventoryResultSchema = z.object({
154
+ manifest: artifactGraphManifestSchema,
155
+ nodes: artifactPageSchema(artifactNodeSchema),
156
+ occurrences: artifactPageSchema(artifactOccurrenceSchema),
157
+ edges: artifactPageSchema(artifactEdgeSchema),
158
+ limits: artifactTraversalLimitsSchema,
159
+ provenance: z.array(artifactCommandSchema).max(256),
160
+ limitations: z.array(z.string()),
161
+ });
162
+ /** One safely materialized artifact beneath caller-approved output root. */
163
+ const extractedArtifactSchema = z.object({
164
+ artifact_id: artifactIdSchema,
165
+ relative_path: boundedRelativePathSchema,
166
+ sha256: sha256Schema,
167
+ bytes_written: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
168
+ created: z.boolean(),
169
+ });
170
+ /** Path-independent commitment to one selected extraction operation. */
171
+ const artifactExtractionManifestSchema = z.object({
172
+ schema_version: z.literal(1),
173
+ extraction_id: extractionIdSchema,
174
+ source_manifest_id: manifestIdSchema,
175
+ selected_occurrence_ids: z.array(occurrenceIdSchema).min(1).max(500),
176
+ files_sha256: sha256Schema,
177
+ output_root_alias: z.literal("$OUTPUT_ROOT"),
178
+ });
179
+ /** Safe extraction observation with containment and cleanup verification. */
180
+ export const artifactExtractionResultSchema = z.object({
181
+ manifest: artifactGraphManifestSchema,
182
+ extraction_manifest: artifactExtractionManifestSchema,
183
+ output_root: z.string().min(1),
184
+ artifacts: artifactPageSchema(extractedArtifactSchema),
185
+ containment_verified: z.boolean(),
186
+ cleanup: z.object({
187
+ attempted: z.boolean(),
188
+ verified: z.boolean(),
189
+ residual_paths: z.array(boundedRelativePathSchema).max(500),
190
+ }),
191
+ limits: artifactTraversalLimitsSchema,
192
+ provenance: z.array(artifactCommandSchema).max(256),
193
+ limitations: z.array(z.string()),
194
+ });
@@ -0,0 +1,150 @@
1
+ import { createHash } from "node:crypto";
2
+ import canonicalize from "canonicalize";
3
+ import { artifactInventoryResultSchema, } from "./artifactGraph.js";
4
+ import { parseEvidence } from "./evidence.js";
5
+ /** Parse, merge, and verify a bounded set of inventory Evidence pages. */
6
+ export const parseArtifactInventoryEvidence = (input) => assembleInventorySet((Array.isArray(input) ? input : [input]).map((value) => {
7
+ const evidence = parseEvidence(value);
8
+ if (evidence.operation !== "inventory_artifact")
9
+ throw new TypeError("Artifact comparison requires inventory_artifact Evidence");
10
+ const inventory = artifactInventoryResultSchema.parse(evidence.normalized_result);
11
+ if (evidence.subject === null ||
12
+ evidence.subject.digest.sha256 !== inventory.manifest.root_sha256 ||
13
+ evidence.subject.format !== inventory.manifest.root_format)
14
+ throw new TypeError("Artifact inventory Evidence subject does not match its root digest");
15
+ validateInventoryPage(inventory);
16
+ return { evidence, inventory };
17
+ }));
18
+ const validateInventoryPage = (inventory) => {
19
+ assertUnique(inventory.nodes.items.map(({ artifact_id: id }) => id), "node ID");
20
+ assertUnique(inventory.occurrences.items.map(({ occurrence_id: id }) => id), "occurrence ID");
21
+ assertUnique(inventory.occurrences.items.map(({ logical_path: path }) => path), "logical path");
22
+ assertUnique(inventory.edges.items.map(({ edge_id: id }) => id), "edge ID");
23
+ for (const node of inventory.nodes.items)
24
+ if (node.artifact_id !==
25
+ `art_${digestCanonical({ schema_version: 1, sha256: node.sha256 })}`)
26
+ throw new TypeError("Artifact node ID is not content-addressed");
27
+ };
28
+ const assembleInventorySet = (pages) => {
29
+ if (pages.length === 0 || pages.length > 100)
30
+ throw new TypeError("Artifact comparison requires 1 to 100 Evidence pages");
31
+ const first = pages[0];
32
+ if (first === undefined)
33
+ throw new TypeError("Artifact comparison requires Evidence pages");
34
+ const manifestJson = canonicalJson(first.inventory.manifest);
35
+ const nodes = new Map();
36
+ const occurrences = new Map();
37
+ const paths = new Map();
38
+ const edges = new Map();
39
+ for (const page of pages) {
40
+ if (canonicalJson(page.inventory.manifest) !== manifestJson)
41
+ throw new TypeError("Artifact inventory pages do not share one manifest");
42
+ for (const node of page.inventory.nodes.items)
43
+ mergeExact(nodes, node.artifact_id, node, "node");
44
+ for (const occurrence of page.inventory.occurrences.items) {
45
+ mergeExact(occurrences, occurrence.occurrence_id, occurrence, "occurrence");
46
+ mergeExact(paths, occurrence.logical_path, occurrence, "logical path");
47
+ }
48
+ for (const edge of page.inventory.edges.items)
49
+ mergeExact(edges, edge.edge_id, edge, "edge");
50
+ }
51
+ const inventory = orderedInventory(first.inventory, pages, {
52
+ nodes,
53
+ occurrences,
54
+ edges,
55
+ });
56
+ if (inventory.complete)
57
+ validateCompleteInventory(inventory);
58
+ return { evidence: pages.map(({ evidence }) => evidence), inventory };
59
+ };
60
+ const orderedInventory = (first, pages, values) => {
61
+ const nodes = [...values.nodes.values()].sort((left, right) => left.artifact_id.localeCompare(right.artifact_id));
62
+ const occurrences = [...values.occurrences.values()].sort((left, right) => left.logical_path.localeCompare(right.logical_path, "en"));
63
+ const edges = [...values.edges.values()].sort((left, right) => left.edge_id.localeCompare(right.edge_id));
64
+ return {
65
+ manifest: first.manifest,
66
+ nodes,
67
+ occurrences,
68
+ edges,
69
+ limitations: [
70
+ ...new Set(pages.flatMap(({ inventory }) => inventory.limitations)),
71
+ ].sort((left, right) => left.localeCompare(right)),
72
+ complete: nodes.length === first.manifest.node_count &&
73
+ occurrences.length === first.manifest.occurrence_count &&
74
+ edges.length === first.manifest.edge_count,
75
+ };
76
+ };
77
+ const validateCompleteInventory = (inventory) => {
78
+ const nodeIds = new Set(inventory.nodes.map(({ artifact_id: id }) => id));
79
+ const occurrenceIds = new Set(inventory.occurrences.map(({ occurrence_id: id }) => id));
80
+ const rootNode = inventory.nodes.find(({ artifact_id: id }) => id === inventory.manifest.root_artifact_id);
81
+ if (rootNode?.sha256 !== inventory.manifest.root_sha256)
82
+ throw new TypeError("Artifact root node does not match its manifest digest");
83
+ for (const occurrence of inventory.occurrences)
84
+ validateOccurrence(occurrence, inventory, nodeIds, occurrenceIds);
85
+ for (const edge of inventory.edges)
86
+ validateEdge(edge, nodeIds, occurrenceIds);
87
+ const graphSha256 = digestCanonical({
88
+ nodes: inventory.nodes,
89
+ occurrences: inventory.occurrences,
90
+ edges: inventory.edges,
91
+ });
92
+ if (graphSha256 !== inventory.manifest.graph_sha256)
93
+ throw new TypeError("Artifact graph commitment does not match its members");
94
+ const manifestId = `agm_${digestCanonical({
95
+ schema_version: 1,
96
+ root_artifact_id: inventory.manifest.root_artifact_id,
97
+ graph_sha256: graphSha256,
98
+ })}`;
99
+ if (manifestId !== inventory.manifest.manifest_id)
100
+ throw new TypeError("Artifact manifest ID does not match its commitment");
101
+ };
102
+ const validateOccurrence = (occurrence, inventory, nodeIds, occurrenceIds) => {
103
+ if (occurrence.artifact_id !== null && !nodeIds.has(occurrence.artifact_id))
104
+ throw new TypeError("Artifact occurrence references a missing node");
105
+ if (occurrence.parent_occurrence_id !== null &&
106
+ !occurrenceIds.has(occurrence.parent_occurrence_id))
107
+ throw new TypeError("Artifact occurrence references a missing parent");
108
+ const expectedId = occurrence.logical_path === "."
109
+ ? `occ_${digestCanonical({ root: inventory.manifest.root_artifact_id })}`
110
+ : `occ_${digestCanonical({
111
+ root_artifact_id: inventory.manifest.root_artifact_id,
112
+ logical_path: occurrence.logical_path,
113
+ entry_kind: occurrence.entry_kind,
114
+ })}`;
115
+ if (occurrence.occurrence_id !== expectedId)
116
+ throw new TypeError("Artifact occurrence ID does not match its identity");
117
+ };
118
+ const validateEdge = (edge, nodeIds, occurrenceIds) => {
119
+ if (!nodeIds.has(edge.parent_artifact_id) ||
120
+ !nodeIds.has(edge.child_artifact_id) ||
121
+ !occurrenceIds.has(edge.occurrence_id))
122
+ throw new TypeError("Artifact edge references a missing graph member");
123
+ const semantic = {
124
+ parent_artifact_id: edge.parent_artifact_id,
125
+ child_artifact_id: edge.child_artifact_id,
126
+ relation: edge.relation,
127
+ occurrence_id: edge.occurrence_id,
128
+ logical_path: edge.logical_path,
129
+ };
130
+ if (edge.edge_id !== `edge_${digestCanonical(semantic)}`)
131
+ throw new TypeError("Artifact edge ID does not match its identity");
132
+ };
133
+ const mergeExact = (output, key, value, label) => {
134
+ const previous = output.get(key);
135
+ if (previous !== undefined &&
136
+ canonicalJson(previous) !== canonicalJson(value))
137
+ throw new TypeError(`Artifact inventory has conflicting ${label} pages`);
138
+ output.set(key, value);
139
+ };
140
+ const assertUnique = (values, label) => {
141
+ if (new Set(values).size !== values.length)
142
+ throw new TypeError(`Artifact inventory contains a duplicate ${label}`);
143
+ };
144
+ const digestCanonical = (value) => createHash("sha256").update(canonicalJson(value)).digest("hex");
145
+ const canonicalJson = (value) => {
146
+ const encoded = canonicalize(value);
147
+ if (encoded === undefined)
148
+ throw new TypeError("Artifact inventory could not canonicalize graph data");
149
+ return encoded;
150
+ };