rea-agents 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/README.md +124 -20
  2. package/bridge/hopper_bridge.py +141 -13
  3. package/dist/application/AnalysisProvider.js +12 -1
  4. package/dist/application/ArtifactExtraction.js +166 -0
  5. package/dist/application/ArtifactGraphConstruction.js +257 -0
  6. package/dist/application/ArtifactInventory.js +253 -0
  7. package/dist/application/BinarySession.js +164 -14
  8. package/dist/application/CompositeProvider.js +73 -0
  9. package/dist/application/DirectAnalysis.js +27 -3
  10. package/dist/application/Doctor.js +35 -6
  11. package/dist/application/EnhancedTools.js +8 -7
  12. package/dist/application/EvidenceBundleCommands.js +29 -0
  13. package/dist/application/EvidenceBundleFiles.js +127 -0
  14. package/dist/application/EvidenceLedger.js +225 -18
  15. package/dist/application/FilesystemSnapshot.js +124 -0
  16. package/dist/application/LinuxHopper.js +186 -0
  17. package/dist/application/LoopbackReplay.js +195 -37
  18. package/dist/application/ProcessHarness.js +200 -191
  19. package/dist/application/ProcessNormalization.js +44 -0
  20. package/dist/application/ProcessOwnership.js +84 -0
  21. package/dist/application/ProcessSampling.js +284 -0
  22. package/dist/application/RealHopperAssertions.js +105 -0
  23. package/dist/application/ReferenceSourceImport.js +182 -0
  24. package/dist/application/ReferenceSourceImportEntries.js +122 -0
  25. package/dist/application/ReferenceSourceImportPolicy.js +73 -0
  26. package/dist/application/ReferenceSourceImportTypes.js +18 -0
  27. package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
  28. package/dist/application/Setup.js +192 -41
  29. package/dist/application/Uninstall.js +130 -0
  30. package/dist/application/runtime.js +8 -1
  31. package/dist/artifacts/ArtifactPaths.js +51 -0
  32. package/dist/artifacts/ArtifactProvider.js +130 -0
  33. package/dist/artifacts/ArtifactReader.js +9 -0
  34. package/dist/artifacts/AsarArtifactReader.js +62 -0
  35. package/dist/artifacts/DirectoryArtifactReader.js +107 -0
  36. package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
  37. package/dist/artifacts/SafeOutputTree.js +199 -0
  38. package/dist/artifacts/StreamBytes.js +10 -0
  39. package/dist/artifacts/ZipArtifactReader.js +109 -0
  40. package/dist/cli.js +153 -6
  41. package/dist/config.js +35 -1
  42. package/dist/contracts/artifactComparisonExample.js +95 -0
  43. package/dist/contracts/artifactToolContracts.js +84 -0
  44. package/dist/contracts/enhancedInputs.js +4 -0
  45. package/dist/contracts/functionComparisonExample.js +57 -0
  46. package/dist/contracts/investigationExamples.js +118 -0
  47. package/dist/contracts/nativeToolContracts.js +52 -0
  48. package/dist/contracts/processCaptureExample.js +25 -0
  49. package/dist/contracts/toolContractExamples.js +69 -0
  50. package/dist/contracts/toolContracts.js +99 -36
  51. package/dist/contracts/toolOutputSchemas.js +154 -82
  52. package/dist/contracts/unknownContractExamples.js +33 -0
  53. package/dist/domain/artifactComparison.js +273 -0
  54. package/dist/domain/artifactGraph.js +194 -0
  55. package/dist/domain/artifactInventoryEvidence.js +150 -0
  56. package/dist/domain/binaryTarget.js +55 -0
  57. package/dist/domain/bundleComparison.js +266 -0
  58. package/dist/domain/callPath.js +346 -0
  59. package/dist/domain/changedBehavior.js +294 -0
  60. package/dist/domain/errors.js +192 -4
  61. package/dist/domain/evidence.js +41 -10
  62. package/dist/domain/evidenceBundle.js +187 -6
  63. package/dist/domain/functionComparison.js +201 -0
  64. package/dist/domain/functionComparisonNormalization.js +112 -0
  65. package/dist/domain/functionComparisonResults.js +54 -0
  66. package/dist/domain/functionComparisonSchemas.js +82 -0
  67. package/dist/domain/functionDossierEvidence.js +171 -0
  68. package/dist/domain/hopperValues.js +14 -7
  69. package/dist/domain/nativeInspection.js +142 -0
  70. package/dist/domain/processCapture.js +152 -54
  71. package/dist/domain/processComparison.js +106 -0
  72. package/dist/domain/reconstructionUnknowns.js +90 -0
  73. package/dist/domain/reconstructionVerification.js +285 -0
  74. package/dist/domain/reconstructionVerificationSchemas.js +126 -0
  75. package/dist/domain/referenceSourceClassification.js +496 -0
  76. package/dist/domain/referenceSourceGraph.js +376 -0
  77. package/dist/domain/referenceSourceImportParsing.js +235 -0
  78. package/dist/domain/residualUnknown.js +239 -0
  79. package/dist/domain/staticRuntimeCorrelation.js +375 -0
  80. package/dist/hopper/BridgeLauncher.js +40 -3
  81. package/dist/hopper/HopperClient.js +14 -5
  82. package/dist/hopper/HopperProvider.js +57 -22
  83. package/dist/identity.js +1 -0
  84. package/dist/main.js +5 -1
  85. package/dist/native/CommandRunner.js +156 -0
  86. package/dist/native/NativeMacOSProvider.js +306 -0
  87. package/dist/native/NativeMachoInspection.js +135 -0
  88. package/dist/native/parsers/codesign.js +55 -0
  89. package/dist/native/parsers/demangle.js +26 -0
  90. package/dist/native/parsers/dyldInfo.js +25 -0
  91. package/dist/native/parsers/lipo.js +67 -0
  92. package/dist/native/parsers/otool.js +193 -0
  93. package/dist/native/parsers/plist.js +23 -0
  94. package/dist/reference/ReferenceSourceReader.js +73 -0
  95. package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
  96. package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
  97. package/dist/reference/ReferenceSourceReaderFile.js +119 -0
  98. package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
  99. package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
  100. package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
  101. package/dist/server/createServer.js +31 -5
  102. package/dist/server/recordDerivedEvidence.js +10 -0
  103. package/dist/server/registerArtifactComparisonTool.js +62 -0
  104. package/dist/server/registerArtifactTools.js +6 -0
  105. package/dist/server/registerBundleComparisonTool.js +47 -0
  106. package/dist/server/registerEnhancedTools.js +64 -12
  107. package/dist/server/registerEvidenceTools.js +36 -0
  108. package/dist/server/registerFunctionComparisonTool.js +68 -0
  109. package/dist/server/registerInvestigationTools.js +224 -0
  110. package/dist/server/registerNativeTools.js +6 -0
  111. package/dist/server/registerOfficialTools.js +47 -14
  112. package/dist/server/registerProcessComparisonTool.js +106 -0
  113. package/dist/server/registerSessionTools.js +179 -70
  114. package/dist/server/sessionEvidence.js +28 -0
  115. package/dist/server/sessionToolPolicies.js +64 -0
  116. package/dist/server/toolRegistrationOptions.js +7 -0
  117. package/dist/server/toolResult.js +8 -5
  118. package/install.sh +198 -0
  119. package/package.json +19 -2
  120. package/scripts/rea.mjs +5 -1
  121. package/skills/rea-analysis/SKILL.md +77 -2
  122. /package/dist/{application/HopperToolPort.js → domain/referenceSourcePolicy.js} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { AnalysisProtocolError, HopperProtocolError } from "./errors.js";
2
+ import { AnalysisOutputError, HopperProtocolError } from "./errors.js";
3
3
  import { err, ok } from "./result.js";
4
4
  const procedureMapSchema = z.record(z.string(), z.string());
5
5
  const addressedNamesSchema = z.array(z.object({ address: z.string(), name: z.string() }));
@@ -8,6 +8,9 @@ const segmentSchema = z.object({
8
8
  name: z.string().default(""),
9
9
  start: z.string().default(""),
10
10
  end: z.string().default(""),
11
+ readable: z.boolean().nullable().default(null),
12
+ writable: z.boolean().nullable().default(null),
13
+ executable: z.boolean().nullable().default(null),
11
14
  });
12
15
  const addressedPageSchema = z.object({
13
16
  items: z.array(z.object({ address: z.string(), value: z.string() })),
@@ -20,6 +23,12 @@ const unavailableSchema = z
20
23
  const procedureIdentitySchema = z
21
24
  .object({ address: z.string(), name: z.string() })
22
25
  .strict();
26
+ const localVariableSchema = z
27
+ .object({
28
+ description: z.string(),
29
+ provenance: z.literal("hopper-public-python-api"),
30
+ })
31
+ .strict();
23
32
  const boundedSchema = (item) => z
24
33
  .object({
25
34
  items: z.array(item),
@@ -61,14 +70,14 @@ const referenceEdgeSchema = z
61
70
  kind: unavailableSchema,
62
71
  })
63
72
  .strict();
64
- const functionDossierSchema = z
73
+ export const functionDossierSchema = z
65
74
  .object({
66
75
  procedure: z
67
76
  .object({
68
77
  address: z.string(),
69
78
  name: z.string(),
70
79
  signature: z.string().nullable(),
71
- locals: z.array(z.json()),
80
+ locals: z.array(localVariableSchema),
72
81
  })
73
82
  .strict(),
74
83
  pseudocode: z
@@ -81,7 +90,7 @@ const functionDossierSchema = z
81
90
  })
82
91
  .strict()
83
92
  .superRefine((value, context) => {
84
- if (value.returned_chars !== value.text.length) {
93
+ if (value.returned_chars !== [...value.text].length) {
85
94
  context.addIssue({
86
95
  code: "custom",
87
96
  message: "returned_chars must equal the text length",
@@ -146,9 +155,7 @@ export const parseFunctionDossier = (value) => {
146
155
  const parsed = functionDossierSchema.safeParse(value);
147
156
  return parsed.success
148
157
  ? ok(parsed.data)
149
- : err(new AnalysisProtocolError("Invalid analyze_function provider output", {
150
- cause: parsed.error,
151
- }));
158
+ : err(new AnalysisOutputError("analyze_function", "provider output did not match the dossier contract", { cause: parsed.error }));
152
159
  };
153
160
  /** Parse page entries and continuation metadata returned by list operations. */
154
161
  export const parseAddressedPage = (value) => {
@@ -0,0 +1,142 @@
1
+ import { z } from "zod";
2
+ const hexAddress = z.string().regex(/^0x[a-fA-F0-9]+$/u);
3
+ const coverage = (item) => z.object({
4
+ items: z.array(item),
5
+ total: z.number().int().min(0).nullable(),
6
+ exhaustive: z.boolean(),
7
+ limitations: z.array(z.string()),
8
+ });
9
+ const nativeCommandInvocationSchema = z.object({
10
+ tool: z.string().min(1),
11
+ command: z.array(z.string()),
12
+ tool_version: z.string().nullable(),
13
+ version_reason: z.string().nullable(),
14
+ executable_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
15
+ exit: z.object({
16
+ code: z.number().int().nullable(),
17
+ signal: z.string().nullable(),
18
+ }),
19
+ stdout_bytes: z.number().int().min(0),
20
+ stderr_bytes: z.number().int().min(0),
21
+ stdout_truncated: z.boolean(),
22
+ stderr_truncated: z.boolean(),
23
+ });
24
+ const architectureSchema = z.object({
25
+ name: z.string(),
26
+ cpu_type: z.string().nullable(),
27
+ cpu_subtype: z.string().nullable(),
28
+ file_offset: z.number().int().min(0).nullable(),
29
+ size: z.number().int().min(0).nullable(),
30
+ alignment: z.number().int().min(0).nullable(),
31
+ });
32
+ const permissionSchema = z.object({
33
+ read: z.boolean().nullable(),
34
+ write: z.boolean().nullable(),
35
+ execute: z.boolean().nullable(),
36
+ raw: z.string().nullable(),
37
+ });
38
+ const sectionSchema = z.object({
39
+ segment: z.string(),
40
+ name: z.string(),
41
+ address: hexAddress.nullable(),
42
+ size: z.number().int().min(0).nullable(),
43
+ file_offset: z.number().int().min(0).nullable(),
44
+ alignment: z.number().int().min(0).nullable(),
45
+ flags: z.array(z.string()),
46
+ });
47
+ const segmentSchema = z.object({
48
+ name: z.string(),
49
+ vm_address: hexAddress.nullable(),
50
+ vm_size: z.number().int().min(0).nullable(),
51
+ file_offset: z.number().int().min(0).nullable(),
52
+ file_size: z.number().int().min(0).nullable(),
53
+ maximum_permissions: permissionSchema,
54
+ initial_permissions: permissionSchema,
55
+ sections: coverage(sectionSchema),
56
+ });
57
+ const symbolSchema = z.object({
58
+ name: z.string(),
59
+ address: hexAddress.nullable(),
60
+ weak: z.boolean().nullable(),
61
+ reexport: z.boolean().nullable(),
62
+ source: z.string().nullable(),
63
+ });
64
+ const dependencySchema = z.object({
65
+ path: z.string(),
66
+ kind: z.string(),
67
+ current_version: z.string().nullable(),
68
+ compatibility_version: z.string().nullable(),
69
+ });
70
+ const loadCommandSchema = z.object({
71
+ index: z.number().int().min(0),
72
+ kind: z.string(),
73
+ file_offset: z.number().int().min(0).nullable(),
74
+ fields: z.record(z.string(), z.union([z.string(), z.number(), z.null()])),
75
+ });
76
+ const buildMetadataSchema = z.object({
77
+ platform: z.string().nullable(),
78
+ minimum_os: z.string().nullable(),
79
+ sdk: z.string().nullable(),
80
+ tools: z.array(z.object({ name: z.string(), version: z.string() })),
81
+ });
82
+ export const inspectMachoSchema = z.object({
83
+ format: z.literal("mach-o"),
84
+ endian: z.enum(["little", "big"]).nullable(),
85
+ word_size: z.union([z.literal(32), z.literal(64)]).nullable(),
86
+ file_type: z.string().nullable(),
87
+ flags: z.array(z.string()),
88
+ uuid: z.string().nullable(),
89
+ entrypoints: coverage(z.object({ file_offset: z.number().int().min(0) })),
90
+ architectures: coverage(architectureSchema),
91
+ build_metadata: coverage(buildMetadataSchema),
92
+ load_commands: coverage(loadCommandSchema),
93
+ dependencies: coverage(dependencySchema),
94
+ imports: coverage(symbolSchema),
95
+ exports: coverage(symbolSchema),
96
+ segments: coverage(segmentSchema),
97
+ provenance: z.array(nativeCommandInvocationSchema),
98
+ limitations: z.array(z.string()),
99
+ });
100
+ export const inspectSignatureSchema = z.object({
101
+ signed: z.boolean(),
102
+ identifier: z.string().nullable(),
103
+ team_identifier: z.string().nullable(),
104
+ format: z.string().nullable(),
105
+ cdhashes: z.array(z.string()),
106
+ hash_algorithms: z.array(z.string()),
107
+ authorities: z.array(z.string()),
108
+ designated_requirement: z.string().nullable(),
109
+ entitlements: z.unknown().nullable(),
110
+ timestamp: z.string().nullable(),
111
+ hardened_runtime: z.boolean().nullable(),
112
+ provenance: z.array(nativeCommandInvocationSchema),
113
+ limitations: z.array(z.string()),
114
+ });
115
+ export const inspectPlistSchema = z.object({
116
+ format: z.enum(["xml", "binary", "json", "unknown"]),
117
+ value: z.unknown(),
118
+ bundle: z.object({
119
+ identifier: z.string().nullable(),
120
+ executable: z.string().nullable(),
121
+ name: z.string().nullable(),
122
+ version: z.string().nullable(),
123
+ short_version: z.string().nullable(),
124
+ }),
125
+ source_path: z.string(),
126
+ provenance: z.array(nativeCommandInvocationSchema),
127
+ limitations: z.array(z.string()),
128
+ });
129
+ export const listArchitecturesSchema = z.object({
130
+ architectures: coverage(architectureSchema),
131
+ provenance: z.array(nativeCommandInvocationSchema),
132
+ limitations: z.array(z.string()),
133
+ });
134
+ export const demangleSwiftSchema = z.object({
135
+ symbols: z.array(z.object({
136
+ input: z.string(),
137
+ output: z.string(),
138
+ status: z.enum(["demangled", "unchanged", "invalid"]),
139
+ })),
140
+ provenance: z.array(nativeCommandInvocationSchema),
141
+ limitations: z.array(z.string()),
142
+ });
@@ -2,18 +2,41 @@ import { resolve } from "node:path";
2
2
  import { z } from "zod";
3
3
  const positiveBudget = z.number().int().positive();
4
4
  const timedEventBase = { at_ms: z.number().int().nonnegative() };
5
+ const environmentName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/u);
6
+ const reservedEnvironment = new Set([
7
+ "HOME",
8
+ "TERM",
9
+ "REA_PROCESS_RUN_ID",
10
+ "REA_REPLAY_HTTP_URL",
11
+ "REA_REPLAY_WEBSOCKET_URL",
12
+ ]);
13
+ const normalizationSchema = z.object({
14
+ paths: z.boolean(),
15
+ pids: z.boolean(),
16
+ ports: z.boolean(),
17
+ time_bucket_ms: positiveBudget.max(60_000),
18
+ patterns: z.array(z.object({
19
+ pattern: z.string().max(500),
20
+ replacement: z.string().max(100),
21
+ })),
22
+ });
5
23
  /** Exact boundary schema for bounded dynamic process scenarios. */
6
24
  export const processScenarioSchema = z
7
25
  .object({
8
26
  approved: z
9
27
  .literal(true)
10
28
  .describe("Explicit per-call acknowledgement that this operation launches the target"),
29
+ unknown_registry_approved: z
30
+ .literal(true)
31
+ .optional()
32
+ .describe("Explicit approval to record capture residuals durably"),
11
33
  executable: z.string().startsWith("/"),
12
34
  arguments: z.array(z.string()).max(256).default([]),
13
35
  working_directory: z.string().startsWith("/"),
14
- environment: z.record(z.string(), z.string()).default({}),
15
- inherit_environment: z.array(z.string()).max(64).default([]),
16
- secret_aliases: z.array(z.string()).max(64).default([]),
36
+ environment: z.record(environmentName, z.string()).default({}),
37
+ inherit_environment: z.array(environmentName).max(64).default([]),
38
+ secret_aliases: z.array(environmentName).max(64).default([]),
39
+ network_access: z.literal("host").default("host"),
17
40
  filesystem_roots: z.array(z.string().startsWith("/")).max(16).default([]),
18
41
  events: z
19
42
  .array(z.discriminatedUnion("type", [
@@ -46,6 +69,10 @@ export const processScenarioSchema = z
46
69
  files: positiveBudget.max(100_000).default(10_000),
47
70
  file_bytes: positiveBudget.max(100_000_000).default(10_000_000),
48
71
  processes: positiveBudget.max(10_000).default(1_000),
72
+ protocol_events: positiveBudget.max(100_000).default(10_000),
73
+ protocol_body_bytes: positiveBudget.max(10_000_000).default(1_000_000),
74
+ connections: positiveBudget.max(1_000).default(100),
75
+ filesystem_depth: z.number().int().min(1).max(64).default(16),
49
76
  })
50
77
  .default({
51
78
  output_bytes: 1_000_000,
@@ -53,6 +80,10 @@ export const processScenarioSchema = z
53
80
  files: 10_000,
54
81
  file_bytes: 10_000_000,
55
82
  processes: 1_000,
83
+ protocol_events: 10_000,
84
+ protocol_body_bytes: 1_000_000,
85
+ connections: 100,
86
+ filesystem_depth: 16,
56
87
  }),
57
88
  normalization: z
58
89
  .object({
@@ -83,6 +114,12 @@ export const processScenarioSchema = z
83
114
  path: z.string().startsWith("/"),
84
115
  status: z.number().int().min(100).max(599),
85
116
  body: z.string().max(1_000_000),
117
+ request_headers: z.record(z.string(), z.string()).default({}),
118
+ request_body: z.string().max(1_000_000).optional(),
119
+ response_headers: z.record(z.string(), z.string()).default({}),
120
+ delay_ms: z.number().int().nonnegative().max(30_000).default(0),
121
+ disconnect: z.boolean().default(false),
122
+ max_calls: z.number().int().min(1).max(100).default(1),
86
123
  }))
87
124
  .max(100)
88
125
  .default([]),
@@ -90,8 +127,29 @@ export const processScenarioSchema = z
90
127
  .array(z.string().max(1_000_000))
91
128
  .max(100)
92
129
  .default([]),
130
+ websocket_connections: z
131
+ .array(z.object({
132
+ messages: z
133
+ .array(z.object({
134
+ data: z.string().max(1_000_000),
135
+ delay_ms: z
136
+ .number()
137
+ .int()
138
+ .nonnegative()
139
+ .max(30_000)
140
+ .default(0),
141
+ }))
142
+ .max(100),
143
+ disconnect_after: z.boolean().default(false),
144
+ }))
145
+ .max(100)
146
+ .default([]),
93
147
  })
94
- .default({ http: [], websocket_messages: [] }),
148
+ .default({
149
+ http: [],
150
+ websocket_messages: [],
151
+ websocket_connections: [],
152
+ }),
95
153
  })
96
154
  .strict()
97
155
  .superRefine((scenario, context) => {
@@ -114,6 +172,34 @@ export const processScenarioSchema = z
114
172
  });
115
173
  }
116
174
  }
175
+ const explicit = new Set(Object.keys(scenario.environment));
176
+ for (const name of [...explicit, ...scenario.inherit_environment]) {
177
+ if (reservedEnvironment.has(name)) {
178
+ context.addIssue({
179
+ code: "custom",
180
+ message: `${name} is reserved by the process adapter`,
181
+ path: ["environment", name],
182
+ });
183
+ }
184
+ }
185
+ for (const name of scenario.inherit_environment) {
186
+ if (explicit.has(name)) {
187
+ context.addIssue({
188
+ code: "custom",
189
+ message: "an environment name cannot be explicit and inherited",
190
+ path: ["inherit_environment"],
191
+ });
192
+ }
193
+ }
194
+ for (const event of scenario.events) {
195
+ if (event.at_ms > scenario.timeout_ms) {
196
+ context.addIssue({
197
+ code: "custom",
198
+ message: "event occurs after the scenario timeout",
199
+ path: ["events"],
200
+ });
201
+ }
202
+ }
117
203
  });
118
204
  /** Parse untrusted process scenario input and apply safe default budgets. */
119
205
  export const parseProcessScenario = (input) => processScenarioSchema.parse(input);
@@ -123,6 +209,11 @@ const isWithin = (candidate, root) => resolve(candidate) === resolve(root) ||
123
209
  export const authorizeProcessScenario = (scenario, policy) => {
124
210
  if (!policy.enabled)
125
211
  return { allowed: false, reason: "process capture is disabled" };
212
+ if (scenario.network_access === "host" && !policy.allowExternalNetwork)
213
+ return {
214
+ allowed: false,
215
+ reason: "host network access is not approved by operator policy",
216
+ };
126
217
  if (!policy.executableRoots.some((root) => isWithin(scenario.executable, root))) {
127
218
  return { allowed: false, reason: "executable is outside approved roots" };
128
219
  }
@@ -152,7 +243,8 @@ export const authorizeProcessScenario = (scenario, policy) => {
152
243
  };
153
244
  /** Exact serialized shape of a bounded process capture. */
154
245
  export const processCaptureSchema = z.object({
155
- schema_version: z.literal(1),
246
+ schema_version: z.literal(2),
247
+ normalization: normalizationSchema,
156
248
  frames: z.array(z.object({
157
249
  sequence: z.number().int().nonnegative(),
158
250
  at_ms: z.number().int().nonnegative(),
@@ -161,6 +253,7 @@ export const processCaptureSchema = z.object({
161
253
  exit: z.object({
162
254
  code: z.number().int().nullable(),
163
255
  signal: z.number().int().nullable(),
256
+ reason: z.enum(["exited", "timeout", "idle_timeout"]),
164
257
  }),
165
258
  process_samples: z.array(z.object({
166
259
  at_ms: z.number().int().nonnegative(),
@@ -170,11 +263,18 @@ export const processCaptureSchema = z.object({
170
263
  })),
171
264
  protocol_events: z.array(z.object({
172
265
  sequence: z.number().int().nonnegative(),
266
+ at_ms: z.number().int().nonnegative(),
173
267
  protocol: z.enum(["http", "websocket"]),
174
268
  direction: z.enum(["request", "response", "received", "sent"]),
175
269
  method: z.string().nullable(),
176
270
  path: z.string().nullable(),
177
271
  data: z.string(),
272
+ outcome: z.enum([
273
+ "matched",
274
+ "unmatched",
275
+ "script_exhausted",
276
+ "disconnected",
277
+ ]),
178
278
  })),
179
279
  files_before: z.array(z.object({
180
280
  path: z.string(),
@@ -198,55 +298,53 @@ export const processCaptureSchema = z.object({
198
298
  .nullable(),
199
299
  symlink_target: z.string().nullable(),
200
300
  })),
301
+ filesystem_effects: z.array(z.object({
302
+ path: z.string(),
303
+ status: z.enum(["created", "modified", "deleted", "unchanged"]),
304
+ before: z
305
+ .object({
306
+ path: z.string(),
307
+ type: z.enum(["file", "directory", "symlink", "other"]),
308
+ mode: z.number().int().nonnegative(),
309
+ size: z.number().int().nonnegative(),
310
+ sha256: z
311
+ .string()
312
+ .regex(/^[a-f0-9]{64}$/u)
313
+ .nullable(),
314
+ symlink_target: z.string().nullable(),
315
+ })
316
+ .nullable(),
317
+ after: z
318
+ .object({
319
+ path: z.string(),
320
+ type: z.enum(["file", "directory", "symlink", "other"]),
321
+ mode: z.number().int().nonnegative(),
322
+ size: z.number().int().nonnegative(),
323
+ sha256: z
324
+ .string()
325
+ .regex(/^[a-f0-9]{64}$/u)
326
+ .nullable(),
327
+ symlink_target: z.string().nullable(),
328
+ })
329
+ .nullable(),
330
+ })),
201
331
  truncated: z.boolean(),
202
332
  limitations: z.array(z.string()),
333
+ residual_unknowns: z.array(z.object({
334
+ scope: z.enum([
335
+ "terminal",
336
+ "exit",
337
+ "process",
338
+ "filesystem",
339
+ "protocol",
340
+ "cleanup",
341
+ "network",
342
+ ]),
343
+ reason: z.string(),
344
+ })),
345
+ cleanup: z.object({
346
+ owned_process_group: z.literal("verified"),
347
+ temporary_root: z.literal("removed"),
348
+ }),
203
349
  });
204
- const stableFiles = (files) => JSON.stringify([...files].sort((left, right) => left.path.localeCompare(right.path)));
205
- /** Compare bounded captures without claiming equality for incomplete observations. */
206
- export const compareProcessCaptures = (left, right) => {
207
- if (left.truncated || right.truncated) {
208
- return {
209
- status: "truncated",
210
- terminal: "truncated",
211
- exit: "truncated",
212
- filesystem: "truncated",
213
- protocol: "truncated",
214
- process: "truncated",
215
- limitations: ["At least one capture is truncated."],
216
- };
217
- }
218
- const terminal = left.frames.map(({ data }) => data).join("") ===
219
- right.frames.map(({ data }) => data).join("")
220
- ? "unchanged"
221
- : "changed";
222
- const exit = JSON.stringify(left.exit) === JSON.stringify(right.exit)
223
- ? "unchanged"
224
- : "changed";
225
- const filesystem = stableFiles(left.files_after) === stableFiles(right.files_after)
226
- ? "unchanged"
227
- : "changed";
228
- const protocol = JSON.stringify(left.protocol_events) ===
229
- JSON.stringify(right.protocol_events)
230
- ? "unchanged"
231
- : "changed";
232
- const process = JSON.stringify(left.process_samples.map(({ command }) => command)) ===
233
- JSON.stringify(right.process_samples.map(({ command }) => command))
234
- ? "unchanged"
235
- : "changed";
236
- const status = terminal === "unchanged" &&
237
- exit === "unchanged" &&
238
- filesystem === "unchanged" &&
239
- protocol === "unchanged" &&
240
- process === "unchanged"
241
- ? "unchanged"
242
- : "changed";
243
- return {
244
- status,
245
- terminal,
246
- exit,
247
- filesystem,
248
- protocol,
249
- process,
250
- limitations: [...left.limitations, ...right.limitations],
251
- };
252
- };
350
+ export { compareProcessCaptures, comparisonStatusSchema, processCaptureComparisonSchema, } from "./processComparison.js";
@@ -0,0 +1,106 @@
1
+ import { z } from "zod";
2
+ /** Comparison classification that never equates incomplete evidence. */
3
+ export const comparisonStatusSchema = z.enum([
4
+ "unchanged",
5
+ "added",
6
+ "removed",
7
+ "changed",
8
+ "truncated",
9
+ "unknown",
10
+ ]);
11
+ /** Pure normalized comparison between two captures. */
12
+ export const processCaptureComparisonSchema = z
13
+ .object({
14
+ status: comparisonStatusSchema,
15
+ terminal: comparisonStatusSchema,
16
+ exit: comparisonStatusSchema,
17
+ filesystem: comparisonStatusSchema,
18
+ protocol: comparisonStatusSchema,
19
+ process: comparisonStatusSchema,
20
+ limitations: z.array(z.string()),
21
+ })
22
+ .superRefine((comparison, context) => {
23
+ const dimensions = [
24
+ comparison.terminal,
25
+ comparison.exit,
26
+ comparison.filesystem,
27
+ comparison.protocol,
28
+ comparison.process,
29
+ ];
30
+ const expected = dimensions.includes("truncated")
31
+ ? "truncated"
32
+ : dimensions.some((status) => ["added", "removed", "changed"].includes(status))
33
+ ? "changed"
34
+ : dimensions.includes("unknown")
35
+ ? "unknown"
36
+ : "unchanged";
37
+ if (comparison.status !== expected)
38
+ context.addIssue({
39
+ code: "custom",
40
+ message: "Process comparison status contradicts its dimensions",
41
+ path: ["status"],
42
+ });
43
+ });
44
+ const stableFiles = (capture) => JSON.stringify([...capture.files_after].sort((left, right) => left.path.localeCompare(right.path)));
45
+ const classifyCollection = (left, right) => {
46
+ if (JSON.stringify(left) === JSON.stringify(right))
47
+ return "unchanged";
48
+ if (left.length === 0)
49
+ return "added";
50
+ if (right.length === 0)
51
+ return "removed";
52
+ return "changed";
53
+ };
54
+ const hasUnknown = (capture, scope) => capture.residual_unknowns.some((item) => item.scope === scope);
55
+ const comparisonStatus = (dimensions) => {
56
+ if (dimensions.some((status) => ["changed", "added", "removed"].includes(status)))
57
+ return "changed";
58
+ return dimensions.includes("unknown") ? "unknown" : "unchanged";
59
+ };
60
+ /** Compare bounded captures without equating missing or incompatible evidence. */
61
+ export const compareProcessCaptures = (left, right) => {
62
+ if (left.truncated || right.truncated) {
63
+ return {
64
+ status: "truncated",
65
+ terminal: "truncated",
66
+ exit: "truncated",
67
+ filesystem: "truncated",
68
+ protocol: "truncated",
69
+ process: "truncated",
70
+ limitations: ["At least one capture is truncated."],
71
+ };
72
+ }
73
+ const sameNormalization = JSON.stringify(left.normalization) === JSON.stringify(right.normalization);
74
+ const classify = (scope, leftValues, rightValues) => !sameNormalization || hasUnknown(left, scope) || hasUnknown(right, scope)
75
+ ? "unknown"
76
+ : classifyCollection(leftValues, rightValues);
77
+ const terminal = classify("terminal", left.frames.map(({ data }) => data), right.frames.map(({ data }) => data));
78
+ const exit = hasUnknown(left, "exit") || hasUnknown(right, "exit")
79
+ ? "unknown"
80
+ : JSON.stringify(left.exit) === JSON.stringify(right.exit)
81
+ ? "unchanged"
82
+ : "changed";
83
+ const filesystem = !hasUnknown(left, "filesystem") &&
84
+ !hasUnknown(right, "filesystem") &&
85
+ sameNormalization &&
86
+ stableFiles(left) === stableFiles(right)
87
+ ? "unchanged"
88
+ : classify("filesystem", left.filesystem_effects, right.filesystem_effects);
89
+ const protocol = classify("protocol", left.protocol_events, right.protocol_events);
90
+ const process = classify("process", left.process_samples, right.process_samples);
91
+ return {
92
+ status: comparisonStatus([terminal, exit, filesystem, protocol, process]),
93
+ terminal,
94
+ exit,
95
+ filesystem,
96
+ protocol,
97
+ process,
98
+ limitations: [
99
+ ...left.limitations,
100
+ ...right.limitations,
101
+ ...left.residual_unknowns.map(({ reason }) => reason),
102
+ ...right.residual_unknowns.map(({ reason }) => reason),
103
+ ...(sameNormalization ? [] : ["Capture normalization rules differ."]),
104
+ ],
105
+ };
106
+ };