rea-agents 2.1.0 → 2.3.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 (193) hide show
  1. package/README.md +84 -18
  2. package/dist/application/AndroidApplicationService.js +17 -0
  3. package/dist/application/AppleApplicationService.js +17 -0
  4. package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
  5. package/dist/application/BinarySession.js +9 -2
  6. package/dist/application/BinarySessionStatus.js +39 -1
  7. package/dist/application/CapabilityInventory.js +15 -0
  8. package/dist/application/ClientRegistrationStatus.js +11 -4
  9. package/dist/application/CommandShimReplay.js +6 -3
  10. package/dist/application/CompositeProvider.js +5 -2
  11. package/dist/application/DirectAnalysis.js +1 -20
  12. package/dist/application/DirectAnalysisStatus.js +83 -0
  13. package/dist/application/Doctor.js +34 -25
  14. package/dist/application/DoctorDiagnostics.js +1 -4
  15. package/dist/application/DoctorProjection.js +43 -0
  16. package/dist/application/DoctorScope.js +102 -0
  17. package/dist/application/EvidenceReferenceResolver.js +26 -0
  18. package/dist/application/InventoryProjectionEvidence.js +41 -0
  19. package/dist/application/InvestigationProviders.js +12 -0
  20. package/dist/application/JavaScriptApplicationEvidence.js +1 -1
  21. package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
  22. package/dist/application/JavaScriptApplicationService.js +3 -2
  23. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
  24. package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
  25. package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
  26. package/dist/application/JavaScriptModuleRelationships.js +25 -1
  27. package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
  28. package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
  29. package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
  30. package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
  31. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
  32. package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
  33. package/dist/application/JavaScriptSemanticTraceService.js +45 -0
  34. package/dist/application/LoopbackReplay.js +277 -133
  35. package/dist/application/LoopbackReplayRecorder.js +101 -0
  36. package/dist/application/PermissionAuthority.js +7 -3
  37. package/dist/application/ProcessCaptureJournal.js +95 -0
  38. package/dist/application/ProcessCaptureLifecycle.js +24 -13
  39. package/dist/application/ProcessCheckpoints.js +30 -4
  40. package/dist/application/ProcessCli.js +26 -4
  41. package/dist/application/ProcessHarness.js +20 -50
  42. package/dist/application/ProcessNormalization.js +3 -2
  43. package/dist/application/ProcessPairedExperiment.js +72 -0
  44. package/dist/application/ProcessReactiveCoordinator.js +218 -0
  45. package/dist/application/ProcessReactiveEffects.js +133 -0
  46. package/dist/application/ProcessReactiveObservations.js +20 -0
  47. package/dist/application/ProcessSampling.js +2 -0
  48. package/dist/application/SessionProviderRouter.js +7 -6
  49. package/dist/application/Setup.js +14 -13
  50. package/dist/application/SetupClientConfiguration.js +10 -6
  51. package/dist/application/SetupPlan.js +4 -3
  52. package/dist/application/SetupRegistrationEnvironment.js +12 -0
  53. package/dist/application/SetupSkill.js +44 -40
  54. package/dist/application/TerminalRenderer.js +3 -1
  55. package/dist/application/Uninstall.js +7 -1
  56. package/dist/application/Upgrade.js +16 -1
  57. package/dist/browser/CdpEndpoint.js +37 -4
  58. package/dist/catalogIdentity.js +16 -7
  59. package/dist/cli/coreAnalysisCommands.js +34 -2
  60. package/dist/cli/javascriptApplicationAnalysis.js +19 -0
  61. package/dist/cli/setupCommands.js +84 -30
  62. package/dist/cli/utilityCommands.js +10 -2
  63. package/dist/cliApplicationCommands.js +79 -16
  64. package/dist/cliCommandNames.js +3 -0
  65. package/dist/cliElectronCommands.js +23 -33
  66. package/dist/cliObservationOptions.js +1 -1
  67. package/dist/cliOutput.js +56 -0
  68. package/dist/cliProcessCommands.js +32 -1
  69. package/dist/cliSetup.js +55 -29
  70. package/dist/contracts/applicationToolContracts.js +54 -7
  71. package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
  72. package/dist/contracts/electronToolContracts.js +73 -4
  73. package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
  74. package/dist/contracts/managedToolContracts.js +7 -2
  75. package/dist/contracts/processCaptureExample.js +1 -0
  76. package/dist/contracts/replayMachineExample.js +37 -0
  77. package/dist/contracts/sessionStatusContract.js +26 -0
  78. package/dist/contracts/toolContractExamples.js +2 -0
  79. package/dist/contracts/toolContractTypes.js +12 -1
  80. package/dist/contracts/toolContracts.js +8 -11
  81. package/dist/contracts/toolEffects.js +3 -0
  82. package/dist/contracts/toolOutputSchemaGroups.js +53 -9
  83. package/dist/contracts/toolOutputSchemaPrimitives.js +73 -32
  84. package/dist/doctorRuntime.js +41 -0
  85. package/dist/domain/analysisErrorPresentation.js +5 -3
  86. package/dist/domain/analysisErrorProjection.js +7 -1
  87. package/dist/domain/androidApplication.js +206 -0
  88. package/dist/domain/appleApplication.js +221 -0
  89. package/dist/domain/boundedCartesianProjection.js +15 -0
  90. package/dist/domain/changedBehavior.js +5 -4
  91. package/dist/domain/completionLedgerGeneration.js +190 -0
  92. package/dist/domain/errors.js +4 -2
  93. package/dist/domain/evidenceCompletionLedger.js +113 -0
  94. package/dist/domain/hopperStartupFailure.js +9 -0
  95. package/dist/domain/javascriptApplicationAnalysis.js +44 -2
  96. package/dist/domain/javascriptExportShapeComparison.js +145 -0
  97. package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
  98. package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
  99. package/dist/domain/javascriptExportShapeSelection.js +132 -0
  100. package/dist/domain/javascriptExportShapeVariants.js +272 -0
  101. package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
  102. package/dist/domain/javascriptSemanticAnalysis.js +17 -36
  103. package/dist/domain/javascriptSemanticCallResolution.js +129 -0
  104. package/dist/domain/javascriptSemanticCalls.js +269 -0
  105. package/dist/domain/javascriptSemanticGraph.js +356 -0
  106. package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
  107. package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
  108. package/dist/domain/javascriptSemanticIr.js +14 -1
  109. package/dist/domain/javascriptSemanticPrimitives.js +36 -0
  110. package/dist/domain/javascriptSemanticProjection.js +36 -4
  111. package/dist/domain/javascriptSemanticProvenance.js +29 -0
  112. package/dist/domain/javascriptSemanticQuery.js +294 -0
  113. package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
  114. package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
  115. package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
  116. package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
  117. package/dist/domain/javascriptSemanticReturns.js +187 -0
  118. package/dist/domain/javascriptSemanticState.js +32 -0
  119. package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
  120. package/dist/domain/javascriptSemanticValues.js +54 -70
  121. package/dist/domain/processCapture.js +36 -0
  122. package/dist/domain/processCaptureValidation.js +101 -12
  123. package/dist/domain/processComparison.js +96 -24
  124. package/dist/domain/processObservation.js +153 -0
  125. package/dist/domain/processPairedExperiment.js +128 -0
  126. package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
  127. package/dist/domain/processReactiveMatching.js +210 -0
  128. package/dist/domain/processReactiveRuntime.js +109 -0
  129. package/dist/domain/processReactiveScenario.js +361 -0
  130. package/dist/domain/processReactiveScenarioPreflight.js +120 -0
  131. package/dist/domain/processReactiveTransition.js +159 -0
  132. package/dist/domain/processScenario.js +12 -0
  133. package/dist/domain/processTraceComparison.js +43 -0
  134. package/dist/domain/processTraceDimensionProjection.js +50 -0
  135. package/dist/domain/processTraceEvaluation.js +312 -0
  136. package/dist/domain/processTraceSpecification.js +373 -0
  137. package/dist/domain/reconstructionVerification.js +5 -4
  138. package/dist/domain/replayMachine.js +332 -0
  139. package/dist/domain/replayMachineRun.js +216 -0
  140. package/dist/domain/replayMachineRuntime.js +294 -0
  141. package/dist/domain/runtimeIdentification.js +210 -0
  142. package/dist/domain/staticRuntimeCorrelation.js +4 -2
  143. package/dist/evaluation/CodexAgentEval.js +156 -0
  144. package/dist/generatedPackageMetadata.js +2 -2
  145. package/dist/ghidra/GhidraClient.js +13 -2
  146. package/dist/ghidra/GhidraDiagnostics.js +1 -0
  147. package/dist/ghidra/GhidraLauncher.js +7 -3
  148. package/dist/ghidra/GhidraProvider.js +8 -1
  149. package/dist/hopper/BridgeLauncher.js +56 -14
  150. package/dist/hopper/HopperClient.js +23 -10
  151. package/dist/hopper/HopperDiagnostics.js +14 -0
  152. package/dist/hopper/HopperProcessDiagnostic.js +14 -0
  153. package/dist/hopper/HopperProvider.js +8 -1
  154. package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
  155. package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
  156. package/dist/identity.js +2 -1
  157. package/dist/main/transport.js +2 -0
  158. package/dist/mcpDoctor.js +310 -0
  159. package/dist/process/ProcessOwnership.js +200 -53
  160. package/dist/process/ProviderRunLineage.js +21 -0
  161. package/dist/server/createServer.js +8 -2
  162. package/dist/server/javascriptApplicationResult.js +106 -0
  163. package/dist/server/registerApplicationTools/characterization.js +3 -3
  164. package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
  165. package/dist/server/registerApplicationTools/compareVersions.js +12 -6
  166. package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
  167. package/dist/server/registerApplicationTools/coverage.js +3 -3
  168. package/dist/server/registerApplicationTools/helpers.js +37 -25
  169. package/dist/server/registerApplicationTools/traceFeature.js +9 -5
  170. package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
  171. package/dist/server/registerApplicationTools.js +4 -0
  172. package/dist/server/registerElectronTools.js +19 -5
  173. package/dist/server/registerEvidenceResources.js +7 -1
  174. package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
  175. package/dist/server/registerManagedTools.js +18 -1
  176. package/dist/server/registerProcessComparisonTool.js +17 -7
  177. package/dist/server/registerReplayMachineTool.js +20 -0
  178. package/dist/server/registerSessionStatusTool.js +95 -25
  179. package/dist/server/registerSessionTools.js +12 -7
  180. package/dist/server/sessionAvailabilityPolicy.js +2 -1
  181. package/dist/server/toolResult.js +10 -4
  182. package/dist/serverIdentity.js +19 -0
  183. package/package.json +62 -29
  184. package/scripts/hopper-demo-x11.py +459 -61
  185. package/scripts/prepack.mjs +55 -0
  186. package/scripts/prepare.mjs +28 -0
  187. package/scripts/rea.mjs +26 -5
  188. package/skills/reverse-engineer-anything/SKILL.md +69 -228
  189. package/skills/reverse-engineer-anything/references/controlled-replay.md +12 -0
  190. package/skills/reverse-engineer-anything/references/evidence-workflows.md +33 -0
  191. package/skills/reverse-engineer-anything/references/javascript-applications.md +35 -0
  192. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +36 -0
  193. package/skills/reverse-engineer-anything/references/runtime-observation.md +24 -0
@@ -0,0 +1,332 @@
1
+ import { MultiDirectedGraph } from "graphology";
2
+ import { singleSource } from "graphology-shortest-path";
3
+ import canonicalize from "canonicalize";
4
+ import { z } from "zod";
5
+ const replayIdentifierSchema = z
6
+ .string()
7
+ .regex(/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u);
8
+ const replayPathSchema = z
9
+ .array(z.union([z.string().max(256), z.number().int().nonnegative()]))
10
+ .max(32);
11
+ const sensitiveHeaderNames = new Set([
12
+ "authorization",
13
+ "cookie",
14
+ "proxy-authorization",
15
+ "set-cookie",
16
+ ]);
17
+ const headerNameSchema = z
18
+ .string()
19
+ .min(1)
20
+ .max(256)
21
+ .regex(/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/u);
22
+ const headerValueSchema = z
23
+ .string()
24
+ .max(8_192)
25
+ .regex(/^[\t\x20-\x7e\x80-\xff]*$/u);
26
+ const replayHeaderMatchersSchema = z
27
+ .record(headerNameSchema, headerValueSchema)
28
+ .superRefine((headers, context) => {
29
+ const names = Object.keys(headers).map((name) => name.toLowerCase());
30
+ if (new Set(names).size !== names.length)
31
+ context.addIssue({
32
+ code: "custom",
33
+ message: "replay header matchers must be unique ignoring case",
34
+ input: headers,
35
+ });
36
+ if (names.some((name) => sensitiveHeaderNames.has(name)))
37
+ context.addIssue({
38
+ code: "custom",
39
+ message: "replay header matchers cannot persist authorization or cookie values; capture and guard through a secret alias instead",
40
+ input: headers,
41
+ });
42
+ })
43
+ .transform((headers) => Object.fromEntries(Object.entries(headers).map(([name, value]) => [
44
+ name.toLowerCase(),
45
+ value,
46
+ ])));
47
+ const replayValueSourceSchema = z.discriminatedUnion("source", [
48
+ z.strictObject({
49
+ source: z.literal("request_header"),
50
+ name: z
51
+ .string()
52
+ .min(1)
53
+ .transform((name) => name.toLowerCase()),
54
+ }),
55
+ z.strictObject({ source: z.literal("request_json"), path: replayPathSchema }),
56
+ z.strictObject({
57
+ source: z.literal("websocket_json"),
58
+ path: replayPathSchema,
59
+ }),
60
+ z.strictObject({ source: z.literal("action_json"), path: replayPathSchema }),
61
+ ]);
62
+ const replayTriggerSchema = z.discriminatedUnion("protocol", [
63
+ z.strictObject({
64
+ protocol: z.literal("http"),
65
+ method: z
66
+ .string()
67
+ .min(1)
68
+ .max(16)
69
+ .transform((method) => method.toUpperCase()),
70
+ path: z.string().startsWith("/"),
71
+ headers: replayHeaderMatchersSchema.default({}),
72
+ body: z.string().max(1_000_000).nullable().default(null),
73
+ }),
74
+ z.strictObject({
75
+ protocol: z.literal("websocket_connect"),
76
+ path: z.string().startsWith("/"),
77
+ headers: replayHeaderMatchersSchema.default({}),
78
+ }),
79
+ z.strictObject({
80
+ protocol: z.literal("websocket_message"),
81
+ path: z.string().startsWith("/"),
82
+ body: z.string().max(1_000_000).nullable().default(null),
83
+ }),
84
+ ]);
85
+ const replayActionSchema = z.discriminatedUnion("type", [
86
+ z.strictObject({
87
+ type: z.literal("http_response"),
88
+ status: z.number().int().min(100).max(599),
89
+ headers: z.record(headerNameSchema, headerValueSchema).default({}),
90
+ body: z.string().max(1_000_000),
91
+ }),
92
+ z.strictObject({
93
+ type: z.literal("websocket_send"),
94
+ data: z.string().max(1_000_000),
95
+ }),
96
+ z.strictObject({ type: z.literal("disconnect") }),
97
+ z.strictObject({
98
+ type: z.literal("delay"),
99
+ duration_ms: z.number().int().nonnegative().max(30_000),
100
+ }),
101
+ ]);
102
+ const replayTransitionSchema = z.strictObject({
103
+ id: replayIdentifierSchema,
104
+ from: replayIdentifierSchema,
105
+ to: replayIdentifierSchema,
106
+ priority: z.number().int().min(0).max(1_000).default(100),
107
+ trigger: replayTriggerSchema,
108
+ guards: z
109
+ .array(z.strictObject({
110
+ variable: replayIdentifierSchema,
111
+ value: replayValueSourceSchema,
112
+ }))
113
+ .max(32)
114
+ .default([]),
115
+ captures: z
116
+ .array(z.strictObject({
117
+ variable: replayIdentifierSchema,
118
+ value: replayValueSourceSchema,
119
+ sensitive: z.boolean().default(false),
120
+ }))
121
+ .max(32)
122
+ .default([]),
123
+ actions: z.array(replayActionSchema).min(1).max(32),
124
+ max_uses: z.number().int().min(1).max(10_000),
125
+ });
126
+ const transitionSignature = (transition) => {
127
+ const signature = canonicalize({
128
+ from: transition.from,
129
+ priority: transition.priority,
130
+ trigger: transition.trigger,
131
+ });
132
+ if (signature === undefined)
133
+ throw new TypeError("Replay transition signature is not canonical JSON");
134
+ return signature;
135
+ };
136
+ const replayMachineBaseSchema = z.strictObject({
137
+ initial_state: replayIdentifierSchema,
138
+ states: z
139
+ .array(z.strictObject({
140
+ name: replayIdentifierSchema,
141
+ terminal: z.boolean().default(false),
142
+ max_visits: z.number().int().min(1).max(100_000).default(100_000),
143
+ }))
144
+ .min(1)
145
+ .max(256),
146
+ transitions: z.array(replayTransitionSchema).min(1).max(2_000),
147
+ max_transitions: z.number().int().min(1).max(100_000),
148
+ limits: z
149
+ .strictObject({
150
+ connections: z.number().int().min(1).max(1_000).default(100),
151
+ messages: z.number().int().min(1).max(100_000).default(10_000),
152
+ bytes: z.number().int().min(1).max(10_000_000).default(1_000_000),
153
+ duration_ms: z.number().int().min(1).max(300_000).default(30_000),
154
+ })
155
+ .default({
156
+ connections: 100,
157
+ messages: 10_000,
158
+ bytes: 1_000_000,
159
+ duration_ms: 30_000,
160
+ }),
161
+ });
162
+ const validateReplayIdentities = (machine, context) => {
163
+ const names = machine.states.map(({ name }) => name);
164
+ const nameSet = new Set(names);
165
+ if (nameSet.size !== names.length)
166
+ context.addIssue({
167
+ code: "custom",
168
+ message: "replay machine state names must be unique",
169
+ path: ["states"],
170
+ input: machine,
171
+ });
172
+ if (!nameSet.has(machine.initial_state))
173
+ context.addIssue({
174
+ code: "custom",
175
+ message: "replay machine initial state is not declared",
176
+ path: ["initial_state"],
177
+ input: machine,
178
+ });
179
+ const transitionIds = machine.transitions.map(({ id }) => id);
180
+ if (new Set(transitionIds).size !== transitionIds.length)
181
+ context.addIssue({
182
+ code: "custom",
183
+ message: "replay machine transition IDs must be unique",
184
+ path: ["transitions"],
185
+ input: machine,
186
+ });
187
+ const signatures = machine.transitions.map(transitionSignature);
188
+ if (new Set(signatures).size !== signatures.length)
189
+ context.addIssue({
190
+ code: "custom",
191
+ message: "replay machine transitions with the same state, trigger, and priority are ambiguous",
192
+ path: ["transitions"],
193
+ input: machine,
194
+ });
195
+ return nameSet;
196
+ };
197
+ const validateReplayVariables = (machine, context) => {
198
+ const capturedVariables = new Map();
199
+ for (const [transitionIndex, transition] of machine.transitions.entries()) {
200
+ for (const capture of transition.captures) {
201
+ if (capture.sensitive && capture.value.source === "action_json")
202
+ context.addIssue({
203
+ code: "custom",
204
+ message: "sensitive replay captures cannot read literal action JSON persisted in the replay plan",
205
+ path: ["transitions", transitionIndex, "captures"],
206
+ input: machine,
207
+ });
208
+ const sensitivity = capturedVariables.get(capture.variable);
209
+ if (sensitivity !== undefined && sensitivity !== capture.sensitive)
210
+ context.addIssue({
211
+ code: "custom",
212
+ message: "a replay variable cannot change secret classification between captures",
213
+ path: ["transitions", transitionIndex, "captures"],
214
+ input: machine,
215
+ });
216
+ capturedVariables.set(capture.variable, capture.sensitive);
217
+ }
218
+ }
219
+ const availableByState = new Map([
220
+ [machine.initial_state, new Set()],
221
+ ]);
222
+ let changed = true;
223
+ while (changed) {
224
+ changed = false;
225
+ for (const transition of machine.transitions) {
226
+ const available = availableByState.get(transition.from);
227
+ if (available === undefined ||
228
+ transition.guards.some(({ variable }) => !available.has(variable)))
229
+ continue;
230
+ const nextAvailable = new Set(available);
231
+ for (const { variable } of transition.captures)
232
+ nextAvailable.add(variable);
233
+ const existing = availableByState.get(transition.to);
234
+ if (existing === undefined) {
235
+ availableByState.set(transition.to, nextAvailable);
236
+ changed = true;
237
+ continue;
238
+ }
239
+ for (const variable of nextAvailable)
240
+ if (!existing.has(variable)) {
241
+ existing.add(variable);
242
+ changed = true;
243
+ }
244
+ }
245
+ }
246
+ for (const [transitionIndex, transition] of machine.transitions.entries())
247
+ for (const [guardIndex, guard] of transition.guards.entries())
248
+ if (!availableByState.get(transition.from)?.has(guard.variable))
249
+ context.addIssue({
250
+ code: "custom",
251
+ message: "replay guard variable is not captured on an executable path before use",
252
+ path: ["transitions", transitionIndex, "guards", guardIndex],
253
+ input: machine,
254
+ });
255
+ };
256
+ const validateReplayActions = (machine, context) => {
257
+ for (const [index, transition] of machine.transitions.entries()) {
258
+ const httpResponses = transition.actions.filter(({ type }) => type === "http_response").length;
259
+ const websocketSends = transition.actions.some(({ type }) => type === "websocket_send");
260
+ const invalid = transition.trigger.protocol === "http"
261
+ ? httpResponses !== 1 || websocketSends
262
+ : httpResponses !== 0;
263
+ if (invalid)
264
+ context.addIssue({
265
+ code: "custom",
266
+ message: "replay actions must match the transition protocol and HTTP transitions require exactly one response",
267
+ path: ["transitions", index, "actions"],
268
+ input: machine,
269
+ });
270
+ }
271
+ };
272
+ const validateReplayGraph = (machine, names, context) => {
273
+ const graph = new MultiDirectedGraph();
274
+ for (const name of names)
275
+ graph.addNode(name);
276
+ for (const [index, transition] of machine.transitions.entries()) {
277
+ if (!names.has(transition.from) || !names.has(transition.to)) {
278
+ context.addIssue({
279
+ code: "custom",
280
+ message: "replay machine transition references an unknown state",
281
+ path: ["transitions", index],
282
+ input: machine,
283
+ });
284
+ continue;
285
+ }
286
+ graph.addDirectedEdgeWithKey(`${transition.id}:${String(index)}`, transition.from, transition.to);
287
+ }
288
+ if (!graph.hasNode(machine.initial_state))
289
+ return;
290
+ if (!machine.states.some(({ terminal }) => terminal))
291
+ context.addIssue({
292
+ code: "custom",
293
+ message: "replay machine must declare at least one terminal state",
294
+ path: ["states"],
295
+ input: machine,
296
+ });
297
+ const reachable = new Set(Object.keys(singleSource(graph, machine.initial_state)));
298
+ reachable.add(machine.initial_state);
299
+ for (const [index, state] of machine.states.entries()) {
300
+ if (!reachable.has(state.name))
301
+ context.addIssue({
302
+ code: "custom",
303
+ message: "replay machine state is unreachable from the initial state",
304
+ path: ["states", index],
305
+ input: machine,
306
+ });
307
+ if (state.terminal && graph.outDegree(state.name) > 0)
308
+ context.addIssue({
309
+ code: "custom",
310
+ message: "terminal replay machine state cannot have outgoing transitions",
311
+ path: ["states", index],
312
+ input: machine,
313
+ });
314
+ if (!state.terminal && graph.outDegree(state.name) === 0)
315
+ context.addIssue({
316
+ code: "custom",
317
+ message: "non-terminal replay machine state has no outgoing transition",
318
+ path: ["states", index],
319
+ input: machine,
320
+ });
321
+ }
322
+ };
323
+ /** Portable finite replay machine with no executable callbacks. */
324
+ export const replayMachineSchema = z
325
+ .object(replayMachineBaseSchema.shape)
326
+ .strict()
327
+ .superRefine((machine, context) => {
328
+ const names = validateReplayIdentities(machine, context);
329
+ validateReplayVariables(machine, context);
330
+ validateReplayActions(machine, context);
331
+ validateReplayGraph(machine, names, context);
332
+ });
@@ -0,0 +1,216 @@
1
+ import { z } from "zod";
2
+ import { replayMachineSchema } from "./replayMachine.js";
3
+ import { replayMachineEventSchema, ReplayMachineRuntime, } from "./replayMachineRuntime.js";
4
+ const refusalOutcomeSchema = z.enum([
5
+ "unmatched",
6
+ "invalid_state",
7
+ "guard_failed",
8
+ "transition_exhausted",
9
+ "invalid_capture",
10
+ "unexpected_reconnect",
11
+ "limit_exhausted",
12
+ ]);
13
+ const MAX_DIRECT_REPLAY_EVENTS = 10_000;
14
+ const MAX_DIRECT_REPLAY_ACTION_BYTES = 4 * 1_024 * 1_024;
15
+ const MAX_DIRECT_REPLAY_ACTION_FIELDS = 1_024;
16
+ const MAX_DIRECT_REPLAY_SENSITIVE_CAPTURES = 1_024;
17
+ const actionFieldCount = (machine) => machine.transitions.reduce((total, transition) => total +
18
+ transition.actions.reduce((fields, action) => fields +
19
+ (action.type === "http_response"
20
+ ? Object.keys(action.headers).length + 1
21
+ : action.type === "websocket_send"
22
+ ? 1
23
+ : 0), 0), 0);
24
+ const maximumSensitiveCapturesPerEvent = (machine) => Math.max(0, ...machine.transitions.map(({ captures }) => captures.filter(({ sensitive }) => sensitive).length));
25
+ /** Complete declarative input for one direct replay-machine run. */
26
+ export const replayMachineRunInputSchema = z
27
+ .strictObject({
28
+ machine: replayMachineSchema,
29
+ events: z.array(replayMachineEventSchema).max(MAX_DIRECT_REPLAY_EVENTS),
30
+ })
31
+ .superRefine(({ machine, events }, context) => {
32
+ const actionBytes = Buffer.byteLength(JSON.stringify(machine.transitions.map(({ id, actions }) => ({ id, actions }))));
33
+ if (actionBytes > MAX_DIRECT_REPLAY_ACTION_BYTES)
34
+ context.addIssue({
35
+ code: "custom",
36
+ path: ["machine", "transitions"],
37
+ message: "direct replay actions exceed the 4 MiB output boundary",
38
+ input: machine.transitions,
39
+ });
40
+ if (actionFieldCount(machine) > MAX_DIRECT_REPLAY_ACTION_FIELDS)
41
+ context.addIssue({
42
+ code: "custom",
43
+ path: ["machine", "transitions"],
44
+ message: "direct replay actions exceed the 1,024-field output boundary",
45
+ input: machine.transitions,
46
+ });
47
+ if (maximumSensitiveCapturesPerEvent(machine) * events.length >
48
+ MAX_DIRECT_REPLAY_SENSITIVE_CAPTURES)
49
+ context.addIssue({
50
+ code: "custom",
51
+ path: ["events"],
52
+ message: "direct replay events exceed the 1,024 sensitive-capture operation boundary",
53
+ input: events,
54
+ });
55
+ });
56
+ const replayMachineRunDecisionSchema = z.strictObject({
57
+ event_sequence: z.number().int().nonnegative(),
58
+ outcome: z.union([z.literal("matched"), refusalOutcomeSchema]),
59
+ transition_sequence: z.number().int().nonnegative().nullable(),
60
+ });
61
+ const replayRunActionSchema = z.discriminatedUnion("type", [
62
+ z.strictObject({
63
+ type: z.literal("http_response"),
64
+ status: z.number().int().min(100).max(599),
65
+ headers: z.record(z
66
+ .string()
67
+ .min(1)
68
+ .max(256)
69
+ .regex(/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/u), z.string().max(8_192)),
70
+ body: z.string().max(1_000_000),
71
+ }),
72
+ z.strictObject({
73
+ type: z.literal("websocket_send"),
74
+ data: z.string().max(1_000_000),
75
+ }),
76
+ z.strictObject({ type: z.literal("disconnect") }),
77
+ z.strictObject({
78
+ type: z.literal("delay"),
79
+ duration_ms: z.number().int().nonnegative().max(30_000),
80
+ }),
81
+ ]);
82
+ const replayRunIdentifierSchema = z
83
+ .string()
84
+ .regex(/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u);
85
+ const replayMachineRunJournalEntrySchema = z.strictObject({
86
+ sequence: z.number().int().nonnegative(),
87
+ at_ms: z.number().int().nonnegative(),
88
+ transition_id: replayRunIdentifierSchema,
89
+ state_before: replayRunIdentifierSchema,
90
+ state_after: replayRunIdentifierSchema,
91
+ captured_aliases: z
92
+ .array(z.strictObject({
93
+ name: replayRunIdentifierSchema,
94
+ sensitive: z.boolean(),
95
+ }))
96
+ .max(32),
97
+ });
98
+ /** Capture-value-free result of directly evaluating a finite replay machine. */
99
+ export const replayMachineRunOutputSchema = z.strictObject({
100
+ schema_version: z.literal(1),
101
+ initial_state: replayRunIdentifierSchema,
102
+ final_state: replayRunIdentifierSchema,
103
+ terminal: z.boolean(),
104
+ decisions: z
105
+ .array(replayMachineRunDecisionSchema)
106
+ .max(MAX_DIRECT_REPLAY_EVENTS),
107
+ transition_journal: z
108
+ .array(replayMachineRunJournalEntrySchema)
109
+ .max(MAX_DIRECT_REPLAY_EVENTS),
110
+ transition_actions: z
111
+ .array(z.strictObject({
112
+ transition_id: replayRunIdentifierSchema,
113
+ actions: z.array(replayRunActionSchema).max(32),
114
+ }))
115
+ .max(2_000),
116
+ limits: z.strictObject({
117
+ configured: z.strictObject({
118
+ transitions: z.number().int().positive(),
119
+ connections: z.number().int().positive(),
120
+ messages: z.number().int().positive(),
121
+ bytes: z.number().int().positive(),
122
+ duration_ms: z.number().int().positive(),
123
+ }),
124
+ usage: z.strictObject({
125
+ offered_events: z.number().int().nonnegative(),
126
+ admitted_events: z.number().int().nonnegative(),
127
+ transitions: z.number().int().nonnegative(),
128
+ connections: z.number().int().nonnegative(),
129
+ messages: z.number().int().nonnegative(),
130
+ bytes: z.number().int().nonnegative(),
131
+ last_at_ms: z.number().int().nonnegative(),
132
+ }),
133
+ }),
134
+ });
135
+ const redactAction = (runtime, action) => {
136
+ switch (action.type) {
137
+ case "http_response":
138
+ return {
139
+ ...action,
140
+ headers: Object.fromEntries(Object.entries(action.headers).map(([name, value]) => [
141
+ name,
142
+ runtime.redactField(value),
143
+ ])),
144
+ body: runtime.redactField(action.body),
145
+ };
146
+ case "websocket_send":
147
+ return { ...action, data: runtime.redactField(action.data) };
148
+ case "delay":
149
+ case "disconnect":
150
+ return action;
151
+ }
152
+ };
153
+ /** Evaluate every supplied event in order without opening sockets or processes. */
154
+ export const runReplayMachine = (input) => {
155
+ const runtime = new ReplayMachineRuntime(input.machine);
156
+ const decisions = [];
157
+ const transitionJournal = [];
158
+ const usedTransitionIds = new Set();
159
+ for (const [eventSequence, event] of input.events.entries()) {
160
+ const decision = runtime.dispatch(event);
161
+ decisions.push({
162
+ event_sequence: eventSequence,
163
+ outcome: decision.outcome,
164
+ transition_sequence: decision.transition?.sequence ?? null,
165
+ });
166
+ if (decision.outcome !== "matched")
167
+ continue;
168
+ usedTransitionIds.add(decision.transition.transition_id);
169
+ const definition = input.machine.transitions.find(({ id }) => id === decision.transition.transition_id);
170
+ if (definition === undefined)
171
+ throw new TypeError("Replay runtime selected an unknown transition");
172
+ transitionJournal.push({
173
+ sequence: decision.transition.sequence,
174
+ at_ms: decision.transition.at_ms,
175
+ transition_id: decision.transition.transition_id,
176
+ state_before: decision.transition.state_before,
177
+ state_after: decision.transition.state_after,
178
+ captured_aliases: definition.captures.map(({ variable, sensitive }) => ({
179
+ name: variable,
180
+ sensitive,
181
+ })),
182
+ });
183
+ }
184
+ const usage = runtime.usage;
185
+ const transitionActions = input.machine.transitions
186
+ .filter(({ id }) => usedTransitionIds.has(id))
187
+ .map(({ id, actions }) => ({
188
+ transition_id: id,
189
+ actions: actions.map((action) => redactAction(runtime, action)),
190
+ }));
191
+ return replayMachineRunOutputSchema.parse({
192
+ schema_version: 1,
193
+ initial_state: input.machine.initial_state,
194
+ final_state: runtime.state,
195
+ terminal: input.machine.states.find(({ name }) => name === runtime.state)
196
+ ?.terminal ?? false,
197
+ decisions,
198
+ transition_journal: transitionJournal,
199
+ transition_actions: transitionActions,
200
+ limits: {
201
+ configured: {
202
+ transitions: input.machine.max_transitions,
203
+ ...input.machine.limits,
204
+ },
205
+ usage: {
206
+ offered_events: input.events.length,
207
+ admitted_events: usage.events,
208
+ transitions: usage.transitions,
209
+ connections: usage.connections,
210
+ messages: usage.messages,
211
+ bytes: usage.bytes,
212
+ last_at_ms: usage.last_at_ms,
213
+ },
214
+ },
215
+ });
216
+ };