rea-agents 1.2.0 → 1.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 (107) hide show
  1. package/README.md +95 -27
  2. package/bridge/hopper_bridge.py +172 -14
  3. package/dist/application/AnalysisSnapshotCache.js +131 -0
  4. package/dist/application/AnalysisSnapshotFiles.js +31 -0
  5. package/dist/application/ArtifactGraphConstruction.js +1 -1
  6. package/dist/application/ArtifactInventory.js +100 -24
  7. package/dist/application/AuthorizedArtifactInventory.js +16 -0
  8. package/dist/application/BinarySession.js +149 -36
  9. package/dist/application/BinarySessionPort.js +1 -0
  10. package/dist/application/BoundedJsonFiles.js +109 -0
  11. package/dist/application/CapabilityInventory.js +127 -0
  12. package/dist/application/ClientRegistrationStatus.js +69 -0
  13. package/dist/application/CrossVersionInventory.js +109 -0
  14. package/dist/application/CrossVersionInvestigation.js +358 -0
  15. package/dist/application/DirectAnalysis.js +205 -24
  16. package/dist/application/Doctor.js +104 -3
  17. package/dist/application/EvidenceBundleFiles.js +15 -108
  18. package/dist/application/EvidenceLedger.js +3 -2
  19. package/dist/application/InvestigationProviders.js +48 -0
  20. package/dist/application/InvestigationWorkspaceStore.js +214 -0
  21. package/dist/application/LinuxHopper.js +72 -48
  22. package/dist/application/PermissionAuthority.js +183 -0
  23. package/dist/application/PermissionConfiguration.js +26 -0
  24. package/dist/application/ProcessCaptureAuthority.js +2 -2
  25. package/dist/application/ProcessCaptureError.js +25 -0
  26. package/dist/application/ProcessCaptureLifecycle.js +22 -2
  27. package/dist/application/ProcessCli.js +111 -31
  28. package/dist/application/ProcessHarness.js +6 -3
  29. package/dist/application/ProgressReporter.js +30 -0
  30. package/dist/application/ProjectPermissionStore.js +110 -0
  31. package/dist/application/ReferenceSourceImportEntries.js +18 -3
  32. package/dist/application/ReferenceSourceImportTypes.js +32 -0
  33. package/dist/application/Setup.js +35 -101
  34. package/dist/application/SetupClients.js +19 -0
  35. package/dist/application/SetupInstallFailure.js +53 -0
  36. package/dist/application/SetupPlan.js +31 -0
  37. package/dist/application/SupportedClients.js +41 -0
  38. package/dist/application/Uninstall.js +34 -24
  39. package/dist/application/UnknownEvidence.js +33 -0
  40. package/dist/application/Upgrade.js +3 -1
  41. package/dist/application/runtime.js +2 -2
  42. package/dist/artifacts/ArtifactProvider.js +15 -8
  43. package/dist/catalogIdentity.js +121 -0
  44. package/dist/cli.js +76 -13
  45. package/dist/cliEvidenceCommands.js +52 -12
  46. package/dist/cliInvestigationCommands.js +137 -0
  47. package/dist/cliLogging.js +20 -3
  48. package/dist/cliOutput.js +41 -0
  49. package/dist/cliPolicyCommands.js +115 -0
  50. package/dist/config.js +102 -27
  51. package/dist/contracts/artifactToolContracts.js +14 -1
  52. package/dist/contracts/errorSchemas.js +98 -0
  53. package/dist/contracts/promptContracts.js +256 -0
  54. package/dist/contracts/sessionLifecycleInputs.js +11 -0
  55. package/dist/contracts/toolContractTypes.js +1 -0
  56. package/dist/contracts/toolContracts.js +20 -7
  57. package/dist/contracts/toolOutputSchemas.js +70 -9
  58. package/dist/domain/analysisSnapshot.js +149 -0
  59. package/dist/domain/artifactComparison.js +38 -11
  60. package/dist/domain/artifactGraph.js +25 -1
  61. package/dist/domain/artifactInventoryEvidence.js +18 -0
  62. package/dist/domain/changedBehavior.js +35 -10
  63. package/dist/domain/errors.js +384 -32
  64. package/dist/domain/evidence.js +4 -2
  65. package/dist/domain/evidenceBundle.js +28 -0
  66. package/dist/domain/hopperStartupFailure.js +55 -0
  67. package/dist/domain/investigationWorkspace.js +341 -0
  68. package/dist/domain/jsonValue.js +2 -0
  69. package/dist/domain/nativeInspection.js +3 -2
  70. package/dist/domain/permissionPolicy.js +157 -0
  71. package/dist/domain/processCapture.js +5 -4
  72. package/dist/domain/processComparison.js +6 -4
  73. package/dist/domain/reconstructionVerification.js +1 -1
  74. package/dist/domain/reconstructionVerificationSchemas.js +1 -0
  75. package/dist/domain/staticRuntimeCorrelation.js +5 -1
  76. package/dist/generatedPackageMetadata.js +9 -0
  77. package/dist/hopper/BridgeLauncher.js +51 -20
  78. package/dist/hopper/HopperClient.js +45 -3
  79. package/dist/hopper/HopperProvider.js +1 -0
  80. package/dist/identity.js +10 -3
  81. package/dist/logger.js +2 -2
  82. package/dist/main.js +139 -28
  83. package/dist/server/createServer.js +54 -5
  84. package/dist/server/mcpProgress.js +23 -0
  85. package/dist/server/promptCompletion.js +144 -0
  86. package/dist/server/registerArtifactComparisonTool.js +6 -2
  87. package/dist/server/registerBundleComparisonTool.js +6 -2
  88. package/dist/server/registerEnhancedTools.js +20 -7
  89. package/dist/server/registerEvidenceResources.js +302 -0
  90. package/dist/server/registerEvidenceTools.js +59 -8
  91. package/dist/server/registerFunctionComparisonTool.js +7 -3
  92. package/dist/server/registerInvestigationTools.js +111 -18
  93. package/dist/server/registerOfficialTools.js +25 -14
  94. package/dist/server/registerProcessComparisonTool.js +8 -10
  95. package/dist/server/registerPrompts.js +67 -0
  96. package/dist/server/registerSessionStatusTool.js +52 -0
  97. package/dist/server/registerSessionTools.js +178 -22
  98. package/dist/server/runDerivedOperation.js +35 -0
  99. package/dist/server/sessionEvidence.js +2 -8
  100. package/dist/server/sessionToolPolicies.js +1 -42
  101. package/dist/server/toolResult.js +34 -6
  102. package/dist/serverIdentity.js +70 -0
  103. package/install.sh +11 -11
  104. package/package.json +10 -6
  105. package/scripts/hopper-demo-x11.py +349 -0
  106. package/scripts/rea.mjs +6 -3
  107. package/skills/rea-analysis/SKILL.md +9 -2
@@ -0,0 +1,149 @@
1
+ import { createHash } from "node:crypto";
2
+ import canonicalize from "canonicalize";
3
+ import { z } from "zod";
4
+ import { evidenceBundleForTarget, evidenceBundleSchema, parseEvidenceBundle, } from "./evidenceBundle.js";
5
+ import { evidenceLocationSchema, providerSchema, } from "./evidence.js";
6
+ import { jsonObjectSchema, jsonValueSchema, } from "./jsonValue.js";
7
+ const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
8
+ const architectureSchema = z.enum(["x86", "x86_64", "arm", "arm64"]);
9
+ const formatSchema = z.enum([
10
+ "hopper",
11
+ "mach-o",
12
+ "elf",
13
+ "pe",
14
+ "zip",
15
+ "ipa",
16
+ "apk",
17
+ "asar",
18
+ "dmg",
19
+ "pkg",
20
+ "plist",
21
+ "javascript",
22
+ "source-map",
23
+ ]);
24
+ const subjectSchema = z.object({
25
+ path: z.string().min(1),
26
+ sha256: digestSchema,
27
+ format: z.enum([
28
+ ...formatSchema.options,
29
+ "directory",
30
+ "file",
31
+ "unknown",
32
+ "mach-o-universal",
33
+ "javascript-bundle",
34
+ "entitlements",
35
+ ]),
36
+ architecture: architectureSchema.nullable(),
37
+ });
38
+ const targetSchema = z.object({
39
+ sha256: digestSchema,
40
+ format: formatSchema,
41
+ architecture: architectureSchema.nullable(),
42
+ loader_args: z.array(z.string()),
43
+ });
44
+ const entrySchema = z.object({
45
+ query_id: z.string().regex(/^query_[a-f0-9]{64}$/u),
46
+ operation: z.string().min(1),
47
+ parameters: jsonObjectSchema,
48
+ execution: z.object({
49
+ result: jsonValueSchema,
50
+ raw_result: jsonValueSchema.nullable(),
51
+ provider: providerSchema,
52
+ limitations: z.array(z.string()),
53
+ locations: z.array(evidenceLocationSchema),
54
+ subject: subjectSchema.nullable(),
55
+ }),
56
+ });
57
+ /** Provider-neutral, deterministic cache of successful immutable analysis calls. */
58
+ const analysisSnapshotSchema = z.object({
59
+ snapshot_version: z.literal(1),
60
+ target: targetSchema,
61
+ entries: z.array(entrySchema).max(10_000),
62
+ evidence_bundle: evidenceBundleSchema,
63
+ });
64
+ const canonicalJson = (value) => {
65
+ const json = jsonValueSchema.parse(value);
66
+ const encoded = canonicalize(json);
67
+ if (encoded === undefined)
68
+ throw new TypeError("RFC 8785 canonicalization rejected snapshot data");
69
+ return encoded;
70
+ };
71
+ /** Project a binary into the path-free identity used for cache invalidation. */
72
+ export const snapshotTarget = (target) => ({
73
+ sha256: target.sha256,
74
+ format: target.format,
75
+ architecture: target.architecture ?? null,
76
+ loader_args: [...target.loaderArgs],
77
+ });
78
+ /** Compare an open binary with the immutable identity in a snapshot. */
79
+ export const snapshotMatchesTarget = (snapshot, target) => snapshot.sha256 === target.sha256 &&
80
+ snapshot.format === target.format &&
81
+ snapshot.architecture === (target.architecture ?? null) &&
82
+ JSON.stringify(snapshot.loader_args) === JSON.stringify(target.loaderArgs);
83
+ /** Find exact persisted CLI evidence without starting an analysis provider. */
84
+ export const snapshotEvidenceForQuery = (snapshot, query) => {
85
+ const { target, operation, parameters, provider } = query;
86
+ if (!snapshotMatchesTarget(snapshot.target, target))
87
+ return undefined;
88
+ const encodedParameters = canonicalJson(parameters);
89
+ return snapshot.evidence_bundle.records.find((record) => record.subject?.digest.sha256 === target.sha256 &&
90
+ record.operation === operation &&
91
+ record.provider.id === provider.id &&
92
+ record.provider.name === provider.name &&
93
+ record.provider.version === provider.version &&
94
+ canonicalJson(record.parameters) === encodedParameters);
95
+ };
96
+ /** Compute the stable lookup key for one provider-specific analysis query. */
97
+ export const analysisQueryId = (target, operation, parameters, provider) => `query_${createHash("sha256")
98
+ .update(canonicalJson({
99
+ target,
100
+ operation,
101
+ parameters,
102
+ provider: {
103
+ id: provider.id,
104
+ name: provider.name,
105
+ version: provider.version,
106
+ },
107
+ }))
108
+ .digest("hex")}`;
109
+ /** Create one serializable snapshot entry from a successful provider call. */
110
+ export const createAnalysisSnapshotEntry = (target, operation, parameters, execution) => ({
111
+ query_id: analysisQueryId(target, operation, parameters, execution.provider),
112
+ operation,
113
+ parameters: jsonObjectSchema.parse(parameters),
114
+ execution: {
115
+ result: execution.result,
116
+ raw_result: execution.rawResult,
117
+ provider: execution.provider,
118
+ limitations: [...execution.limitations],
119
+ locations: [...execution.locations],
120
+ subject: execution.subject === null
121
+ ? null
122
+ : {
123
+ ...execution.subject,
124
+ architecture: execution.subject.architecture ?? null,
125
+ },
126
+ },
127
+ });
128
+ /** Parse a snapshot and reject altered query IDs or non-canonical entry order. */
129
+ export const parseAnalysisSnapshot = (input) => {
130
+ const parsed = analysisSnapshotSchema.parse(input);
131
+ parseEvidenceBundle(parsed.evidence_bundle);
132
+ if (JSON.stringify(evidenceBundleForTarget(parsed.evidence_bundle, parsed.target.sha256)) !== JSON.stringify(parsed.evidence_bundle))
133
+ throw new TypeError("Analysis snapshot evidence contains records for another target");
134
+ const ids = new Set();
135
+ for (const entry of parsed.entries) {
136
+ const expected = analysisQueryId(parsed.target, entry.operation, entry.parameters, entry.execution.provider);
137
+ if (entry.query_id !== expected)
138
+ throw new TypeError("Analysis snapshot query identifier does not match");
139
+ if (ids.has(entry.query_id))
140
+ throw new TypeError("Analysis snapshot contains duplicate queries");
141
+ ids.add(entry.query_id);
142
+ }
143
+ const sorted = [...parsed.entries].sort((left, right) => left.query_id.localeCompare(right.query_id));
144
+ if (JSON.stringify(parsed.entries) !== JSON.stringify(sorted))
145
+ throw new TypeError("Analysis snapshot entries are not canonical");
146
+ return parsed;
147
+ };
148
+ /** Serialize a validated snapshot with byte-stable canonical entry ordering. */
149
+ export const serializeAnalysisSnapshot = (snapshot) => `${canonicalJson(parseAnalysisSnapshot(snapshot))}\n`;
@@ -11,8 +11,15 @@ const comparisonStatusSchema = z.enum([
11
11
  "changed",
12
12
  "truncated",
13
13
  "unknown",
14
+ "contradiction",
15
+ ]);
16
+ const changeKindSchema = z.enum([
17
+ "added",
18
+ "removed",
19
+ "changed",
20
+ "unknown",
21
+ "contradiction",
14
22
  ]);
15
- const changeKindSchema = z.enum(["added", "removed", "changed", "unknown"]);
16
23
  const comparisonDimensionSchema = z.enum([
17
24
  "content",
18
25
  "kind",
@@ -22,6 +29,7 @@ const comparisonDimensionSchema = z.enum([
22
29
  "relations",
23
30
  "metadata",
24
31
  "availability",
32
+ "integrity",
25
33
  ]);
26
34
  /** Strict Evidence-backed input for deterministic artifact comparison. */
27
35
  export const artifactComparisonInputSchema = z.object({
@@ -68,6 +76,7 @@ export const artifactComparisonResultSchema = z.object({
68
76
  removed: z.number().int().min(0),
69
77
  changed: z.number().int().min(0),
70
78
  unknown: z.number().int().min(0),
79
+ contradiction: z.number().int().min(0).default(0),
71
80
  }),
72
81
  changes: z.object({
73
82
  items: z.array(artifactChangeSchema).max(500),
@@ -84,8 +93,8 @@ export const compareArtifacts = (leftEvidence, rightEvidence, offset, limit) =>
84
93
  const right = parseArtifactInventoryEvidence(rightEvidence);
85
94
  const leftComplete = left.inventory.complete;
86
95
  const rightComplete = right.inventory.complete;
87
- const leftCovered = leftComplete && left.inventory.limitations.length === 0;
88
- const rightCovered = rightComplete && right.inventory.limitations.length === 0;
96
+ const leftCovered = leftComplete && !hasCoverageLimitation(left.inventory.limitations);
97
+ const rightCovered = rightComplete && !hasCoverageLimitation(right.inventory.limitations);
89
98
  const limitations = [
90
99
  ...left.inventory.limitations.map((item) => `Left: ${item}`),
91
100
  ...right.inventory.limitations.map((item) => `Right: ${item}`),
@@ -101,13 +110,15 @@ export const compareArtifacts = (leftEvidence, rightEvidence, offset, limit) =>
101
110
  return artifactComparisonResultSchema.parse({
102
111
  status: !leftComplete || !rightComplete
103
112
  ? "truncated"
104
- : !leftCovered || !rightCovered
105
- ? "unknown"
106
- : changes.some(({ classification }) => classification === "unknown")
113
+ : changes.some(({ classification }) => classification === "contradiction")
114
+ ? "contradiction"
115
+ : !leftCovered || !rightCovered
107
116
  ? "unknown"
108
- : changes.length === 0
109
- ? "unchanged"
110
- : "changed",
117
+ : changes.some(({ classification }) => classification === "unknown")
118
+ ? "unknown"
119
+ : changes.length === 0
120
+ ? "unchanged"
121
+ : "changed",
111
122
  left_manifest_id: left.inventory.manifest.manifest_id,
112
123
  right_manifest_id: right.inventory.manifest.manifest_id,
113
124
  summary,
@@ -172,6 +183,13 @@ const classifyPath = (input) => {
172
183
  classification: input.complete ? "removed" : "unknown",
173
184
  dimensions: [input.complete ? "content" : "availability"],
174
185
  };
186
+ if (input.leftOccurrence.hash_status === "mismatched" ||
187
+ input.rightOccurrence.hash_status === "mismatched")
188
+ return {
189
+ ...base,
190
+ classification: "contradiction",
191
+ dimensions: ["integrity"],
192
+ };
175
193
  if (input.leftOccurrence.hash_status !== "verified" ||
176
194
  input.rightOccurrence.hash_status !== "verified" ||
177
195
  input.leftNode === undefined ||
@@ -253,13 +271,20 @@ const relationsByPath = (inventory) => {
253
271
  return output;
254
272
  };
255
273
  const summarize = (left, right, changes, covered) => {
256
- const counts = { added: 0, removed: 0, changed: 0, unknown: 0 };
274
+ const counts = {
275
+ added: 0,
276
+ removed: 0,
277
+ changed: 0,
278
+ unknown: 0,
279
+ contradiction: 0,
280
+ };
257
281
  for (const change of changes)
258
282
  counts[change.classification] += 1;
259
283
  return {
260
284
  unchanged: covered
261
285
  ? Math.max(0, Math.min(left.occurrences.length, right.occurrences.length) -
262
286
  counts.changed -
287
+ counts.contradiction -
263
288
  counts.unknown)
264
289
  : 0,
265
290
  ...counts,
@@ -268,6 +293,8 @@ const summarize = (left, right, changes, covered) => {
268
293
  : Math.max(counts.unknown, Math.max(left.manifest.occurrence_count, right.manifest.occurrence_count) -
269
294
  counts.added -
270
295
  counts.removed -
271
- counts.changed),
296
+ counts.changed -
297
+ counts.contradiction),
272
298
  };
273
299
  };
300
+ const hasCoverageLimitation = (limitations) => limitations.some((limitation) => !/integrity contradiction\(s\) were recorded/u.test(limitation));
@@ -5,6 +5,7 @@ const occurrenceIdSchema = z.string().regex(/^occ_[a-f0-9]{64}$/u);
5
5
  const edgeIdSchema = z.string().regex(/^edge_[a-f0-9]{64}$/u);
6
6
  const manifestIdSchema = z.string().regex(/^agm_[a-f0-9]{64}$/u);
7
7
  const extractionIdSchema = z.string().regex(/^aex_[a-f0-9]{64}$/u);
8
+ const contradictionIdSchema = z.string().regex(/^ic_[a-f0-9]{64}$/u);
8
9
  const boundedRelativePathSchema = z
9
10
  .string()
10
11
  .min(1)
@@ -106,10 +107,29 @@ const artifactOccurrenceSchema = z.object({
106
107
  .nullable(),
107
108
  executable: z.boolean(),
108
109
  encrypted: z.boolean(),
109
- hash_status: z.enum(["verified", "not-hashed-limit", "unavailable"]),
110
+ hash_status: z.enum([
111
+ "verified",
112
+ "mismatched",
113
+ "not-hashed-limit",
114
+ "unavailable",
115
+ ]),
110
116
  source_location: artifactByteRangeSchema.nullable(),
111
117
  limitations: z.array(z.string()),
112
118
  });
119
+ /** Observed bytes that contradict a container's declared integrity identity. */
120
+ const integrityContradictionSchema = z.object({
121
+ contradiction_id: contradictionIdSchema,
122
+ occurrence_id: occurrenceIdSchema,
123
+ parent_artifact_id: artifactIdSchema,
124
+ logical_path: boundedRelativePathSchema,
125
+ declared_sha256: sha256Schema,
126
+ observed_sha256: sha256Schema,
127
+ entry_kind: z.enum(["file", "slice"]),
128
+ unpacked: z.boolean(),
129
+ trust: z.literal("observed-untrusted"),
130
+ provenance: z.literal("container-integrity-metadata-versus-observed-bytes"),
131
+ limitations: z.array(z.string()).min(1),
132
+ });
113
133
  /** Parent-child derivation relation between two content-addressed artifacts. */
114
134
  const artifactEdgeSchema = z.object({
115
135
  edge_id: edgeIdSchema,
@@ -157,6 +177,10 @@ export const artifactInventoryResultSchema = z.object({
157
177
  edges: artifactPageSchema(artifactEdgeSchema),
158
178
  limits: artifactTraversalLimitsSchema,
159
179
  provenance: z.array(artifactCommandSchema).max(256),
180
+ integrity_contradictions: z
181
+ .array(integrityContradictionSchema)
182
+ .max(100)
183
+ .default([]),
160
184
  limitations: z.array(z.string()),
161
185
  });
162
186
  /** One safely materialized artifact beneath caller-approved output root. */
@@ -39,6 +39,9 @@ const assembleInventorySet = (pages) => {
39
39
  for (const page of pages) {
40
40
  if (canonicalJson(page.inventory.manifest) !== manifestJson)
41
41
  throw new TypeError("Artifact inventory pages do not share one manifest");
42
+ if (canonicalJson(page.inventory.integrity_contradictions) !==
43
+ canonicalJson(first.inventory.integrity_contradictions))
44
+ throw new TypeError("Artifact inventory pages do not share integrity contradictions");
42
45
  for (const node of page.inventory.nodes.items)
43
46
  mergeExact(nodes, node.artifact_id, node, "node");
44
47
  for (const occurrence of page.inventory.occurrences.items) {
@@ -66,6 +69,7 @@ const orderedInventory = (first, pages, values) => {
66
69
  nodes,
67
70
  occurrences,
68
71
  edges,
72
+ integrityContradictions: first.integrity_contradictions,
69
73
  limitations: [
70
74
  ...new Set(pages.flatMap(({ inventory }) => inventory.limitations)),
71
75
  ].sort((left, right) => left.localeCompare(right)),
@@ -84,10 +88,24 @@ const validateCompleteInventory = (inventory) => {
84
88
  validateOccurrence(occurrence, inventory, nodeIds, occurrenceIds);
85
89
  for (const edge of inventory.edges)
86
90
  validateEdge(edge, nodeIds, occurrenceIds);
91
+ for (const contradiction of inventory.integrityContradictions) {
92
+ if (!nodeIds.has(contradiction.parent_artifact_id) ||
93
+ !occurrenceIds.has(contradiction.occurrence_id))
94
+ throw new TypeError("Integrity contradiction references a missing graph member");
95
+ const expectedId = `ic_${digestCanonical({
96
+ root_artifact_id: inventory.manifest.root_artifact_id,
97
+ logical_path: contradiction.logical_path,
98
+ declared_sha256: contradiction.declared_sha256,
99
+ observed_sha256: contradiction.observed_sha256,
100
+ })}`;
101
+ if (contradiction.contradiction_id !== expectedId)
102
+ throw new TypeError("Integrity contradiction ID does not match its identity");
103
+ }
87
104
  const graphSha256 = digestCanonical({
88
105
  nodes: inventory.nodes,
89
106
  occurrences: inventory.occurrences,
90
107
  edges: inventory.edges,
108
+ integrity_contradictions: inventory.integrityContradictions,
91
109
  });
92
110
  if (graphSha256 !== inventory.manifest.graph_sha256)
93
111
  throw new TypeError("Artifact graph commitment does not match its members");
@@ -3,13 +3,24 @@ import { artifactComparisonResultSchema } from "./artifactComparison.js";
3
3
  import { evidenceSchema, parseEvidence } from "./evidence.js";
4
4
  import { functionComparisonResultSchema } from "./functionComparison.js";
5
5
  import { comparisonStatusSchema, processCaptureComparisonSchema, } from "./processCapture.js";
6
+ import { crossVersionInvestigationInputSchema, investigationRunSummarySchema, } from "./investigationWorkspace.js";
6
7
  const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
7
8
  /** Strict bounded input for aggregating existing comparison Evidence. */
8
- export const changedBehaviorInputSchema = z.object({
9
- comparisons: z.array(evidenceSchema).min(1).max(100),
9
+ export const changedBehaviorInputSchema = z
10
+ .object({
11
+ comparisons: z.array(evidenceSchema).max(100).default([]),
12
+ investigation_run: crossVersionInvestigationInputSchema.optional(),
10
13
  offset: z.number().int().min(0).default(0),
11
14
  limit: z.number().int().min(1).max(100).default(100),
12
15
  unknown_registry_approved: z.literal(true).optional(),
16
+ })
17
+ .superRefine((input, context) => {
18
+ if (input.comparisons.length > 0 ===
19
+ (input.investigation_run !== undefined))
20
+ context.addIssue({
21
+ code: "custom",
22
+ message: "Supply either existing comparisons or one investigation_run, but not both",
23
+ });
13
24
  });
14
25
  const findingSchema = z.object({
15
26
  scope: z.enum(["runtime", "protocol", "resource", "static_candidate"]),
@@ -48,6 +59,7 @@ export const changedBehaviorResultSchema = z.object({
48
59
  }),
49
60
  evidence_links: z.array(evidenceIdSchema).min(3).max(20_100),
50
61
  limitations: z.array(z.string()),
62
+ investigation_run: investigationRunSummarySchema.optional(),
51
63
  });
52
64
  const EXPECTED_PROVIDERS = {
53
65
  compare_process_captures: {
@@ -113,6 +125,7 @@ export const findChangedBehavior = (comparisonsInput, offset, limit) => {
113
125
  ...(runtimeStatuses.length === 0
114
126
  ? ["No process comparison Evidence was supplied."]
115
127
  : []),
128
+ ...evidence.flatMap(artifactPaginationLimitations),
116
129
  ]),
117
130
  });
118
131
  };
@@ -169,10 +182,8 @@ const parseResult = (evidence) => {
169
182
  return;
170
183
  }
171
184
  const result = artifactComparisonResultSchema.parse(evidence.normalized_result);
172
- if (result.changes.offset !== 0 ||
173
- result.changes.next_offset !== null ||
174
- result.changes.items.length !== result.changes.total)
175
- throw new TypeError("Changed-behavior analysis requires complete artifact comparison pagination");
185
+ if (result.changes.offset !== 0)
186
+ throw new TypeError("Changed-behavior analysis requires artifact comparison pagination from offset zero");
176
187
  assertNestedLinks(evidence, result.changes.items.flatMap(({ evidence_links: links }) => links));
177
188
  };
178
189
  const assertNestedLinks = (evidence, nestedLinks) => {
@@ -236,13 +247,17 @@ const artifactFindings = (evidence) => {
236
247
  dimension: `artifact:${change.logical_path}`,
237
248
  scope: "static_candidate",
238
249
  status: change.classification,
239
- classification: change.classification === "unknown"
240
- ? "unresolved_branch"
241
- : "derived_relationship",
250
+ classification: change.classification === "contradiction"
251
+ ? "contradiction"
252
+ : change.classification === "unknown"
253
+ ? "unresolved_branch"
254
+ : "derived_relationship",
242
255
  limitations: result.limitations,
243
256
  links: change.evidence_links,
244
257
  }));
245
- return (result.status === "unknown" || result.status === "truncated") &&
258
+ return (result.status === "unknown" ||
259
+ result.status === "truncated" ||
260
+ result.changes.next_offset !== null) &&
246
261
  !changes.some(({ classification }) => classification === "unresolved_branch")
247
262
  ? [
248
263
  ...changes,
@@ -257,6 +272,16 @@ const artifactFindings = (evidence) => {
257
272
  ]
258
273
  : changes;
259
274
  };
275
+ const artifactPaginationLimitations = (evidence) => {
276
+ if (evidence.operation !== "compare_artifacts")
277
+ return [];
278
+ const result = artifactComparisonResultSchema.parse(evidence.normalized_result);
279
+ return result.changes.next_offset === null
280
+ ? []
281
+ : [
282
+ `Artifact comparison reports ${String(result.changes.items.length)} of ${String(result.changes.total)} changes.`,
283
+ ];
284
+ };
260
285
  const makeFinding = (input) => findingSchema.parse({
261
286
  scope: input.scope,
262
287
  dimension: input.dimension,