rea-agents 2.2.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 (165) hide show
  1. package/README.md +56 -9
  2. package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
  3. package/dist/application/BinarySession.js +9 -2
  4. package/dist/application/BinarySessionStatus.js +39 -1
  5. package/dist/application/CapabilityInventory.js +15 -0
  6. package/dist/application/CommandShimReplay.js +6 -3
  7. package/dist/application/CompositeProvider.js +5 -2
  8. package/dist/application/DirectAnalysis.js +1 -20
  9. package/dist/application/DirectAnalysisStatus.js +83 -0
  10. package/dist/application/Doctor.js +34 -25
  11. package/dist/application/DoctorDiagnostics.js +1 -4
  12. package/dist/application/DoctorProjection.js +43 -0
  13. package/dist/application/DoctorScope.js +102 -0
  14. package/dist/application/EvidenceReferenceResolver.js +26 -0
  15. package/dist/application/JavaScriptApplicationEvidence.js +1 -1
  16. package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
  17. package/dist/application/JavaScriptApplicationService.js +3 -2
  18. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
  19. package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
  20. package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
  21. package/dist/application/JavaScriptModuleRelationships.js +25 -1
  22. package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
  23. package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
  24. package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
  25. package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
  26. package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
  27. package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
  28. package/dist/application/JavaScriptSemanticTraceService.js +45 -0
  29. package/dist/application/LoopbackReplay.js +277 -133
  30. package/dist/application/LoopbackReplayRecorder.js +101 -0
  31. package/dist/application/PermissionAuthority.js +7 -3
  32. package/dist/application/ProcessCaptureJournal.js +95 -0
  33. package/dist/application/ProcessCaptureLifecycle.js +24 -13
  34. package/dist/application/ProcessCheckpoints.js +30 -4
  35. package/dist/application/ProcessCli.js +26 -4
  36. package/dist/application/ProcessHarness.js +20 -50
  37. package/dist/application/ProcessNormalization.js +3 -2
  38. package/dist/application/ProcessReactiveCoordinator.js +218 -0
  39. package/dist/application/ProcessReactiveEffects.js +133 -0
  40. package/dist/application/ProcessReactiveObservations.js +20 -0
  41. package/dist/application/ProcessSampling.js +2 -0
  42. package/dist/application/SessionProviderRouter.js +7 -6
  43. package/dist/application/Setup.js +11 -10
  44. package/dist/application/SetupPlan.js +1 -1
  45. package/dist/application/SetupRegistrationEnvironment.js +12 -0
  46. package/dist/application/TerminalRenderer.js +3 -1
  47. package/dist/browser/CdpEndpoint.js +37 -4
  48. package/dist/catalogIdentity.js +16 -7
  49. package/dist/cli/coreAnalysisCommands.js +34 -2
  50. package/dist/cli/javascriptApplicationAnalysis.js +19 -0
  51. package/dist/cli/setupCommands.js +81 -32
  52. package/dist/cli/utilityCommands.js +10 -2
  53. package/dist/cliApplicationCommands.js +79 -16
  54. package/dist/cliCommandNames.js +3 -0
  55. package/dist/cliElectronCommands.js +23 -33
  56. package/dist/cliObservationOptions.js +1 -1
  57. package/dist/cliOutput.js +56 -0
  58. package/dist/cliProcessCommands.js +32 -1
  59. package/dist/cliSetup.js +30 -21
  60. package/dist/contracts/applicationToolContracts.js +54 -7
  61. package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
  62. package/dist/contracts/electronToolContracts.js +53 -32
  63. package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
  64. package/dist/contracts/processCaptureExample.js +1 -0
  65. package/dist/contracts/replayMachineExample.js +37 -0
  66. package/dist/contracts/toolContractExamples.js +2 -0
  67. package/dist/contracts/toolContracts.js +5 -1
  68. package/dist/contracts/toolEffects.js +3 -0
  69. package/dist/contracts/toolOutputSchemaGroups.js +2 -0
  70. package/dist/contracts/toolOutputSchemaPrimitives.js +40 -0
  71. package/dist/doctorRuntime.js +41 -0
  72. package/dist/domain/analysisErrorPresentation.js +5 -3
  73. package/dist/domain/analysisErrorProjection.js +7 -1
  74. package/dist/domain/changedBehavior.js +5 -4
  75. package/dist/domain/completionLedgerGeneration.js +190 -0
  76. package/dist/domain/errors.js +4 -2
  77. package/dist/domain/evidenceCompletionLedger.js +113 -0
  78. package/dist/domain/hopperStartupFailure.js +9 -0
  79. package/dist/domain/javascriptApplicationAnalysis.js +44 -2
  80. package/dist/domain/javascriptExportShapeComparison.js +145 -0
  81. package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
  82. package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
  83. package/dist/domain/javascriptExportShapeSelection.js +132 -0
  84. package/dist/domain/javascriptExportShapeVariants.js +272 -0
  85. package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
  86. package/dist/domain/javascriptSemanticAnalysis.js +17 -36
  87. package/dist/domain/javascriptSemanticCallResolution.js +129 -0
  88. package/dist/domain/javascriptSemanticCalls.js +269 -0
  89. package/dist/domain/javascriptSemanticGraph.js +356 -0
  90. package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
  91. package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
  92. package/dist/domain/javascriptSemanticIr.js +14 -1
  93. package/dist/domain/javascriptSemanticPrimitives.js +36 -0
  94. package/dist/domain/javascriptSemanticProjection.js +36 -4
  95. package/dist/domain/javascriptSemanticProvenance.js +29 -0
  96. package/dist/domain/javascriptSemanticQuery.js +294 -0
  97. package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
  98. package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
  99. package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
  100. package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
  101. package/dist/domain/javascriptSemanticReturns.js +187 -0
  102. package/dist/domain/javascriptSemanticState.js +32 -0
  103. package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
  104. package/dist/domain/javascriptSemanticValues.js +54 -70
  105. package/dist/domain/processCapture.js +36 -0
  106. package/dist/domain/processCaptureValidation.js +101 -12
  107. package/dist/domain/processComparison.js +78 -25
  108. package/dist/domain/processObservation.js +153 -0
  109. package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
  110. package/dist/domain/processReactiveMatching.js +210 -0
  111. package/dist/domain/processReactiveRuntime.js +109 -0
  112. package/dist/domain/processReactiveScenario.js +361 -0
  113. package/dist/domain/processReactiveScenarioPreflight.js +120 -0
  114. package/dist/domain/processReactiveTransition.js +159 -0
  115. package/dist/domain/processScenario.js +12 -0
  116. package/dist/domain/processTraceComparison.js +43 -0
  117. package/dist/domain/processTraceDimensionProjection.js +50 -0
  118. package/dist/domain/processTraceEvaluation.js +312 -0
  119. package/dist/domain/processTraceSpecification.js +373 -0
  120. package/dist/domain/reconstructionVerification.js +5 -4
  121. package/dist/domain/replayMachine.js +18 -2
  122. package/dist/domain/replayMachineRun.js +216 -0
  123. package/dist/domain/replayMachineRuntime.js +88 -9
  124. package/dist/domain/staticRuntimeCorrelation.js +4 -2
  125. package/dist/evaluation/CodexAgentEval.js +24 -0
  126. package/dist/generatedPackageMetadata.js +1 -1
  127. package/dist/ghidra/GhidraClient.js +13 -2
  128. package/dist/ghidra/GhidraDiagnostics.js +1 -0
  129. package/dist/ghidra/GhidraLauncher.js +7 -3
  130. package/dist/ghidra/GhidraProvider.js +8 -1
  131. package/dist/hopper/BridgeLauncher.js +56 -14
  132. package/dist/hopper/HopperClient.js +23 -10
  133. package/dist/hopper/HopperDiagnostics.js +14 -0
  134. package/dist/hopper/HopperProcessDiagnostic.js +14 -0
  135. package/dist/hopper/HopperProvider.js +8 -1
  136. package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
  137. package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
  138. package/dist/main/transport.js +2 -0
  139. package/dist/mcpDoctor.js +310 -0
  140. package/dist/process/ProcessOwnership.js +200 -53
  141. package/dist/process/ProviderRunLineage.js +21 -0
  142. package/dist/server/createServer.js +4 -1
  143. package/dist/server/javascriptApplicationResult.js +22 -1
  144. package/dist/server/registerApplicationTools/characterization.js +3 -3
  145. package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
  146. package/dist/server/registerApplicationTools/compareVersions.js +12 -6
  147. package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
  148. package/dist/server/registerApplicationTools/coverage.js +3 -3
  149. package/dist/server/registerApplicationTools/helpers.js +37 -25
  150. package/dist/server/registerApplicationTools/traceFeature.js +9 -5
  151. package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
  152. package/dist/server/registerApplicationTools.js +4 -0
  153. package/dist/server/registerEvidenceResources.js +5 -1
  154. package/dist/server/registerProcessComparisonTool.js +17 -7
  155. package/dist/server/registerReplayMachineTool.js +20 -0
  156. package/dist/server/registerSessionTools.js +12 -7
  157. package/dist/server/sessionAvailabilityPolicy.js +2 -1
  158. package/dist/serverIdentity.js +19 -0
  159. package/package.json +62 -30
  160. package/scripts/hopper-demo-x11.py +459 -61
  161. package/scripts/prepack.mjs +55 -0
  162. package/scripts/prepare.mjs +28 -0
  163. package/scripts/rea.mjs +26 -5
  164. package/skills/reverse-engineer-anything/SKILL.md +2 -2
  165. package/skills/reverse-engineer-anything/references/javascript-applications.md +10 -0
@@ -1,113 +1,144 @@
1
1
  import { createServer, } from "node:http";
2
2
  import { WebSocketServer } from "ws";
3
- class ReplayRecorder {
4
- scenario;
5
- events = [];
6
- started = Date.now();
7
- truncated = false;
8
- constructor(scenario) {
9
- this.scenario = scenario;
3
+ import {} from "../domain/replayMachineRuntime.js";
4
+ import { closeReplayServer, listenOnLoopback, recordMachineEvent, readReplayRequestBody, ReplayRecorder, requestHeaders, waitForReplayDelay, } from "./LoopbackReplayRecorder.js";
5
+ const recordHttpResponse = (recorder, request, data, outcome) => recorder.record({
6
+ at_ms: recorder.atMs(),
7
+ protocol: "http",
8
+ direction: "response",
9
+ method: request.method,
10
+ path: request.path,
11
+ data,
12
+ outcome,
13
+ });
14
+ const handleMachineHttp = async (recorder, request) => {
15
+ const machine = recorder.machine;
16
+ if (machine === undefined)
17
+ return;
18
+ const decision = recorder.dispatchMachine({
19
+ protocol: "http",
20
+ connection: "not_applicable",
21
+ at_ms: request.rawAtMs,
22
+ recorded_at_ms: request.recordedAtMs,
23
+ method: request.method,
24
+ path: request.path,
25
+ headers: requestHeaders(request.request),
26
+ body: request.body,
27
+ });
28
+ recordMachineEvent(recorder, {
29
+ at_ms: request.recordedAtMs,
30
+ protocol: "http",
31
+ direction: "request",
32
+ method: request.method,
33
+ path: request.path,
34
+ data: request.body,
35
+ }, decision);
36
+ if (decision.outcome !== "matched") {
37
+ request.response.statusCode = decision.outcome === "unmatched" ? 404 : 409;
38
+ request.response.end();
39
+ return;
10
40
  }
11
- atMs() {
12
- return (Math.floor((Date.now() - this.started) /
13
- this.scenario.normalization.time_bucket_ms) * this.scenario.normalization.time_bucket_ms);
41
+ for (const action of decision.actions) {
42
+ if (action.type === "delay")
43
+ await waitForReplayDelay(action.duration_ms);
44
+ else if (action.type === "disconnect") {
45
+ request.response.destroy();
46
+ recordHttpResponse(recorder, request, "", "disconnected");
47
+ return;
48
+ }
49
+ else if (action.type === "http_response") {
50
+ request.response.statusCode = action.status;
51
+ for (const [name, value] of Object.entries(action.headers))
52
+ request.response.setHeader(name, value);
53
+ request.response.end(action.body);
54
+ recordHttpResponse(recorder, request, machine.redact(action.body), "matched");
55
+ }
14
56
  }
15
- record(event) {
16
- if (this.events.length < this.scenario.limits.protocol_events)
17
- this.events.push({ sequence: this.events.length, ...event });
18
- else
19
- this.truncated = true;
57
+ };
58
+ const handleStaticHttp = async (recorder, request, calls) => {
59
+ const index = recorder.scenario.replay.http.findIndex((route) => route.method !== request.method || route.path !== request.path
60
+ ? false
61
+ : (route.request_body === undefined ||
62
+ route.request_body === request.body) &&
63
+ Object.entries(route.request_headers).every(([name, value]) => request.request.headers[name.toLowerCase()] === value));
64
+ const route = recorder.scenario.replay.http[index];
65
+ const used = index < 0 ? 0 : (calls.get(index) ?? 0);
66
+ const outcome = route === undefined
67
+ ? "unmatched"
68
+ : used >= route.max_calls
69
+ ? "script_exhausted"
70
+ : "matched";
71
+ recorder.record({
72
+ at_ms: recorder.atMs(),
73
+ protocol: "http",
74
+ direction: "request",
75
+ method: request.method,
76
+ path: request.path,
77
+ data: request.body.length === 0 ? "" : "<redacted-request-body>",
78
+ outcome,
79
+ });
80
+ if (route === undefined || outcome === "script_exhausted") {
81
+ request.response.statusCode = route === undefined ? 404 : 409;
82
+ request.response.end();
83
+ return;
20
84
  }
21
- }
22
- const readRequestBody = async (request, limit) => {
23
- const chunks = [];
24
- let bytes = 0;
25
- for await (const chunk of request) {
26
- const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
27
- bytes += value.byteLength;
28
- if (bytes > limit)
29
- return undefined;
30
- chunks.push(value);
85
+ calls.set(index, used + 1);
86
+ if (route.delay_ms > 0)
87
+ await waitForReplayDelay(route.delay_ms);
88
+ if (route.disconnect) {
89
+ request.response.destroy();
90
+ recordHttpResponse(recorder, request, "", "disconnected");
91
+ return;
31
92
  }
32
- return Buffer.concat(chunks).toString("utf8");
93
+ request.response.statusCode = route.status;
94
+ for (const [name, value] of Object.entries(route.response_headers))
95
+ request.response.setHeader(name, value);
96
+ request.response.end(route.body);
97
+ recordHttpResponse(recorder, request, route.body, "matched");
33
98
  };
34
- const createHttpHandler = (recorder) => {
35
- const calls = new Map();
36
- return async (request, response) => {
37
- const method = request.method ?? "GET";
38
- const path = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
39
- const body = await readRequestBody(request, recorder.scenario.limits.protocol_body_bytes);
40
- if (body === undefined) {
41
- recorder.truncated = true;
42
- response.statusCode = 413;
43
- response.end();
44
- recorder.record({
45
- at_ms: recorder.atMs(),
46
- protocol: "http",
47
- direction: "request",
48
- method,
49
- path,
50
- data: "<body-over-limit>",
51
- outcome: "unmatched",
52
- });
53
- return;
54
- }
55
- const index = recorder.scenario.replay.http.findIndex((route) => {
56
- if (route.method !== method || route.path !== path)
57
- return false;
58
- if (route.request_body !== undefined && route.request_body !== body)
59
- return false;
60
- return Object.entries(route.request_headers).every(([name, value]) => request.headers[name.toLowerCase()] === value);
61
- });
62
- const route = recorder.scenario.replay.http[index];
63
- const used = index < 0 ? 0 : (calls.get(index) ?? 0);
64
- const outcome = route === undefined
65
- ? "unmatched"
66
- : used >= route.max_calls
67
- ? "script_exhausted"
68
- : "matched";
99
+ const handleHttpRequest = async (recorder, request, response, calls) => {
100
+ const body = await readReplayRequestBody(request, recorder.scenario.limits.protocol_body_bytes);
101
+ const method = request.method ?? "GET";
102
+ const path = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
103
+ if (body === undefined) {
104
+ recorder.truncated = true;
105
+ response.statusCode = 413;
106
+ response.end();
69
107
  recorder.record({
70
108
  at_ms: recorder.atMs(),
71
109
  protocol: "http",
72
110
  direction: "request",
73
111
  method,
74
112
  path,
75
- data: body.length === 0 ? "" : "<redacted-request-body>",
76
- outcome,
113
+ data: "<body-over-limit>",
114
+ outcome: "unmatched",
77
115
  });
78
- if (route === undefined || outcome === "script_exhausted") {
79
- response.statusCode = route === undefined ? 404 : 409;
80
- response.end();
81
- return;
82
- }
83
- calls.set(index, used + 1);
84
- if (route.delay_ms > 0)
85
- await new Promise((resolveDelay) => setTimeout(resolveDelay, route.delay_ms));
86
- if (route.disconnect) {
87
- response.destroy();
88
- recorder.record({
89
- at_ms: recorder.atMs(),
90
- protocol: "http",
91
- direction: "response",
92
- method,
93
- path,
94
- data: "",
95
- outcome: "disconnected",
96
- });
97
- return;
98
- }
99
- response.statusCode = route.status;
100
- for (const [name, value] of Object.entries(route.response_headers))
101
- response.setHeader(name, value);
102
- response.end(route.body);
103
- recorder.record({
104
- at_ms: recorder.atMs(),
105
- protocol: "http",
106
- direction: "response",
107
- method,
108
- path,
109
- data: route.body,
110
- outcome: "matched",
116
+ return;
117
+ }
118
+ const replayRequest = {
119
+ request,
120
+ response,
121
+ method,
122
+ path,
123
+ body,
124
+ rawAtMs: recorder.rawAtMs(),
125
+ recordedAtMs: recorder.atMs(),
126
+ };
127
+ await (recorder.machine === undefined
128
+ ? handleStaticHttp(recorder, replayRequest, calls)
129
+ : recorder.enqueueMachine(() => handleMachineHttp(recorder, replayRequest)));
130
+ };
131
+ const createHttpHandler = (recorder) => {
132
+ const calls = new Map();
133
+ return (request, response) => {
134
+ void handleHttpRequest(recorder, request, response, calls).catch(() => {
135
+ recorder.truncated = true;
136
+ if (response.headersSent)
137
+ response.destroy();
138
+ else {
139
+ response.statusCode = 500;
140
+ response.end();
141
+ }
111
142
  });
112
143
  };
113
144
  };
@@ -161,6 +192,119 @@ const sendWebSocketScript = async (client, connection, recorder) => {
161
192
  });
162
193
  }
163
194
  };
195
+ const runWebSocketActions = async (client, recorder, decision, path) => {
196
+ for (const action of decision.actions) {
197
+ if (action.type === "delay") {
198
+ await waitForReplayDelay(action.duration_ms);
199
+ continue;
200
+ }
201
+ if (action.type === "disconnect") {
202
+ client.close();
203
+ recorder.record({
204
+ at_ms: recorder.atMs(),
205
+ protocol: "websocket",
206
+ direction: "sent",
207
+ method: null,
208
+ path,
209
+ data: "",
210
+ outcome: "disconnected",
211
+ });
212
+ return;
213
+ }
214
+ if (action.type === "websocket_send" && client.readyState === 1) {
215
+ client.send(action.data);
216
+ recorder.record({
217
+ at_ms: recorder.atMs(),
218
+ protocol: "websocket",
219
+ direction: "sent",
220
+ method: null,
221
+ path,
222
+ data: recorder.machine?.redact(action.data) ?? action.data,
223
+ outcome: "matched",
224
+ });
225
+ }
226
+ }
227
+ };
228
+ const handleStaticWebSocket = (client, recorder, connection, path) => {
229
+ client.on("message", (value) => recorder.record({
230
+ at_ms: recorder.atMs(),
231
+ protocol: "websocket",
232
+ direction: "received",
233
+ method: null,
234
+ path,
235
+ data: value.toString(),
236
+ outcome: "matched",
237
+ }));
238
+ void sendWebSocketScript(client, connection, recorder);
239
+ };
240
+ const handleMachineWebSocket = (options) => {
241
+ const { client, request, recorder, connection, path } = options;
242
+ const machine = recorder.machine;
243
+ if (machine === undefined)
244
+ return;
245
+ void recorder
246
+ .enqueueMachine(async () => {
247
+ const connectedAt = recorder.atMs();
248
+ const connectionDecision = recorder.dispatchMachine({
249
+ protocol: "websocket_connect",
250
+ connection: connection === 1 ? "initial" : "reconnect",
251
+ at_ms: recorder.rawAtMs(),
252
+ recorded_at_ms: connectedAt,
253
+ method: null,
254
+ path,
255
+ headers: requestHeaders(request),
256
+ body: "",
257
+ });
258
+ recordMachineEvent(recorder, {
259
+ at_ms: connectedAt,
260
+ protocol: "websocket",
261
+ direction: "received",
262
+ method: null,
263
+ path,
264
+ data: "",
265
+ }, connectionDecision);
266
+ if (connectionDecision.outcome === "matched")
267
+ await runWebSocketActions(client, recorder, connectionDecision, path);
268
+ else
269
+ client.close();
270
+ })
271
+ .catch(() => {
272
+ recorder.truncated = true;
273
+ client.terminate();
274
+ });
275
+ client.on("message", (value) => {
276
+ const body = value.toString();
277
+ const rawAtMs = recorder.rawAtMs();
278
+ const recordedAtMs = recorder.atMs();
279
+ void recorder
280
+ .enqueueMachine(async () => {
281
+ const decision = recorder.dispatchMachine({
282
+ protocol: "websocket_message",
283
+ connection: connection === 1 ? "initial" : "reconnect",
284
+ at_ms: rawAtMs,
285
+ recorded_at_ms: recordedAtMs,
286
+ method: null,
287
+ path,
288
+ headers: {},
289
+ body,
290
+ });
291
+ recordMachineEvent(recorder, {
292
+ at_ms: recordedAtMs,
293
+ protocol: "websocket",
294
+ direction: "received",
295
+ method: null,
296
+ path,
297
+ data: body,
298
+ }, decision);
299
+ if (decision.outcome === "matched")
300
+ await runWebSocketActions(client, recorder, decision, path);
301
+ })
302
+ .catch(() => {
303
+ recorder.truncated = true;
304
+ client.terminate();
305
+ });
306
+ });
307
+ };
164
308
  const createWebSocketReplay = (server, recorder) => {
165
309
  let connections = 0;
166
310
  const websocket = new WebSocketServer({
@@ -169,13 +313,14 @@ const createWebSocketReplay = (server, recorder) => {
169
313
  clientTracking: true,
170
314
  });
171
315
  server.on("upgrade", (request, socket, head) => {
172
- if (new URL(request.url ?? "/", "http://127.0.0.1").pathname !== "/ws") {
316
+ if (recorder.machine === undefined &&
317
+ new URL(request.url ?? "/", "http://127.0.0.1").pathname !== "/ws") {
173
318
  socket.destroy();
174
319
  return;
175
320
  }
176
321
  websocket.handleUpgrade(request, socket, head, (client) => websocket.emit("connection", client, request));
177
322
  });
178
- websocket.on("connection", (client) => {
323
+ websocket.on("connection", (client, request) => {
179
324
  connections += 1;
180
325
  if (connections > recorder.scenario.limits.connections) {
181
326
  recorder.truncated = true;
@@ -183,60 +328,59 @@ const createWebSocketReplay = (server, recorder) => {
183
328
  return;
184
329
  }
185
330
  client.on("error", () => undefined);
186
- client.on("message", (value) => recorder.record({
187
- at_ms: recorder.atMs(),
188
- protocol: "websocket",
189
- direction: "received",
190
- method: null,
191
- path: "/ws",
192
- data: value.toString(),
193
- outcome: "matched",
194
- }));
195
- void sendWebSocketScript(client, connections, recorder);
331
+ const path = new URL(request.url ?? "/", "http://127.0.0.1").pathname;
332
+ if (recorder.machine === undefined)
333
+ handleStaticWebSocket(client, recorder, connections, path);
334
+ else
335
+ handleMachineWebSocket({
336
+ client,
337
+ request,
338
+ recorder,
339
+ connection: connections,
340
+ path,
341
+ });
196
342
  });
197
343
  return websocket;
198
344
  };
199
- const listen = async (server) => {
200
- await new Promise((resolveListen, rejectListen) => {
201
- server.once("error", rejectListen);
202
- server.listen(0, "127.0.0.1", () => {
203
- server.off("error", rejectListen);
204
- resolveListen();
205
- });
206
- });
207
- const address = server.address();
208
- if (address === null || typeof address === "string")
209
- throw new Error("loopback replay did not acquire a TCP port");
210
- return address.port;
211
- };
212
- const closeServer = (server) => new Promise((resolveClose, rejectClose) => {
213
- server.close((error) => error === undefined ? resolveClose() : rejectClose(error));
214
- });
215
345
  /** Start bounded HTTP and WebSocket replay endpoints on IPv4 loopback only. */
216
- export const startLoopbackReplay = async (scenario) => {
217
- const recorder = new ReplayRecorder(scenario);
346
+ export const startLoopbackReplay = async (scenario, recordEvent = () => undefined) => {
347
+ const recorder = new ReplayRecorder(scenario, recordEvent);
218
348
  const server = createServer(createHttpHandler(recorder));
219
349
  const websocket = createWebSocketReplay(server, recorder);
350
+ let closePromise;
220
351
  let port;
221
352
  try {
222
- port = await listen(server);
353
+ port = await listenOnLoopback(server);
223
354
  }
224
355
  catch (cause) {
225
- await closeServer(server).catch(() => undefined);
356
+ await closeReplayServer(server).catch(() => undefined);
226
357
  throw cause;
227
358
  }
228
359
  return {
229
360
  httpUrl: `http://127.0.0.1:${String(port)}`,
230
361
  websocketUrl: `ws://127.0.0.1:${String(port)}/ws`,
231
- events: recorder.events,
362
+ get events() {
363
+ return recorder.events.map((event) => ({ ...event }));
364
+ },
365
+ get transitions() {
366
+ return recorder.transitions.map((transition) => ({
367
+ ...transition,
368
+ sensitive_aliases: [...transition.sensitive_aliases],
369
+ }));
370
+ },
232
371
  get truncated() {
233
372
  return recorder.truncated;
234
373
  },
235
374
  async close() {
236
- for (const client of websocket.clients)
237
- client.terminate();
238
- await new Promise((resolveClose) => websocket.close(() => resolveClose()));
239
- await closeServer(server);
375
+ closePromise ??= (async () => {
376
+ recorder.stopMachineAdmission();
377
+ for (const client of websocket.clients)
378
+ client.terminate();
379
+ await new Promise((resolveClose) => websocket.close(() => resolveClose()));
380
+ await closeReplayServer(server);
381
+ await recorder.drainMachine();
382
+ })();
383
+ await closePromise;
240
384
  },
241
385
  };
242
386
  };
@@ -0,0 +1,101 @@
1
+ import { ReplayMachineRuntime, } from "../domain/replayMachineRuntime.js";
2
+ /** Bounded protocol and replay-machine observations for one process run. */
3
+ export class ReplayRecorder {
4
+ scenario;
5
+ recordEvent;
6
+ events = [];
7
+ started = Date.now();
8
+ machine;
9
+ #machineQueue = Promise.resolve();
10
+ #machineAdmissionOpen = true;
11
+ truncated = false;
12
+ constructor(scenario, recordEvent = () => undefined) {
13
+ this.scenario = scenario;
14
+ this.recordEvent = recordEvent;
15
+ this.machine =
16
+ scenario.replay.machine === null
17
+ ? undefined
18
+ : new ReplayMachineRuntime(scenario.replay.machine);
19
+ }
20
+ get transitions() {
21
+ return this.machine?.timeline ?? [];
22
+ }
23
+ atMs() {
24
+ return (Math.floor((Date.now() - this.started) /
25
+ this.scenario.normalization.time_bucket_ms) * this.scenario.normalization.time_bucket_ms);
26
+ }
27
+ rawAtMs() {
28
+ return Date.now() - this.started;
29
+ }
30
+ enqueueMachine(operation) {
31
+ if (!this.#machineAdmissionOpen)
32
+ return Promise.reject(new Error("replay machine is closing"));
33
+ const result = this.#machineQueue.then(operation);
34
+ this.#machineQueue = result.then(() => undefined, () => undefined);
35
+ return result;
36
+ }
37
+ stopMachineAdmission() {
38
+ this.#machineAdmissionOpen = false;
39
+ }
40
+ async drainMachine() {
41
+ await this.#machineQueue;
42
+ }
43
+ dispatchMachine(event) {
44
+ if (this.machine === undefined)
45
+ throw new Error("replay machine is not configured");
46
+ return this.machine.dispatch(event);
47
+ }
48
+ recordTransition(decision) {
49
+ if (decision.transition !== null)
50
+ this.recordEvent("replay_transitions", decision.transition.sequence);
51
+ }
52
+ record(event) {
53
+ if (this.events.length >= this.scenario.limits.protocol_events) {
54
+ this.truncated = true;
55
+ return;
56
+ }
57
+ const sequence = this.events.length;
58
+ this.events.push({ sequence, ...event });
59
+ this.recordEvent("protocol_events", sequence);
60
+ }
61
+ }
62
+ export const requestHeaders = (request) => Object.fromEntries(Object.entries(request.headers).flatMap(([name, value]) => value === undefined
63
+ ? []
64
+ : [[name, Array.isArray(value) ? value.join(", ") : value]]));
65
+ export const readReplayRequestBody = async (request, limit) => {
66
+ const chunks = [];
67
+ let bytes = 0;
68
+ for await (const chunk of request) {
69
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
70
+ bytes += value.byteLength;
71
+ if (bytes > limit)
72
+ return undefined;
73
+ chunks.push(value);
74
+ }
75
+ return Buffer.concat(chunks).toString("utf8");
76
+ };
77
+ export const recordMachineEvent = (recorder, event, decision) => {
78
+ recorder.record({
79
+ ...event,
80
+ data: recorder.machine?.redact(event.data) ?? event.data,
81
+ outcome: decision.outcome,
82
+ });
83
+ recorder.recordTransition(decision);
84
+ };
85
+ export const waitForReplayDelay = (durationMs) => new Promise((resolveDelay) => setTimeout(resolveDelay, durationMs));
86
+ export const listenOnLoopback = async (server) => {
87
+ await new Promise((resolveListen, rejectListen) => {
88
+ server.once("error", rejectListen);
89
+ server.listen(0, "127.0.0.1", () => {
90
+ server.off("error", rejectListen);
91
+ resolveListen();
92
+ });
93
+ });
94
+ const address = server.address();
95
+ if (address === null || typeof address === "string")
96
+ throw new Error("loopback replay did not acquire a TCP port");
97
+ return address.port;
98
+ };
99
+ export const closeReplayServer = (server) => new Promise((resolveClose, rejectClose) => {
100
+ server.close((error) => error === undefined ? resolveClose() : rejectClose(error));
101
+ });
@@ -74,13 +74,15 @@ const errorReason = (error) => isNotFound(error) ? "not_found" : "io";
74
74
  export class PermissionAuthority {
75
75
  policy;
76
76
  configuredAuthority;
77
- constructor(policy, configuredAuthority) {
77
+ outsideCeilingRestartRequired;
78
+ constructor(policy, configuredAuthority, outsideCeilingRestartRequired = false) {
78
79
  this.policy = policy;
79
80
  this.configuredAuthority = configuredAuthority;
81
+ this.outsideCeilingRestartRequired = outsideCeilingRestartRequired;
80
82
  }
81
83
  /** Create a connection-owned overlay for once and session grants. */
82
84
  createConnectionAuthority() {
83
- return new PermissionAuthority(createPermissionPolicy([]), this.configuredAuthority ?? this);
85
+ return new PermissionAuthority(createPermissionPolicy([]), this.configuredAuthority ?? this, true);
84
86
  }
85
87
  /** Canonicalize and authorize one exact operation before its side effect. */
86
88
  async authorize(request, access, options = {}) {
@@ -165,7 +167,9 @@ export class PermissionAuthority {
165
167
  const denied = evaluatePermission(effective, request);
166
168
  if (denied.allowed)
167
169
  return ok({ decision: denied, owner: this });
168
- return err(new PermissionRequiredError(request, denied.missing, ceilingFor(effective, request.capability), options.elicitationSupported ?? false, options.restartRequired ?? false));
170
+ const outsideAdministratorCeiling = denied.reason === "outside_administrator_ceiling";
171
+ return err(new PermissionRequiredError(request, denied.missing, ceilingFor(effective, request.capability), !outsideAdministratorCeiling && (options.elicitationSupported ?? false), options.restartRequired ??
172
+ (outsideAdministratorCeiling && this.outsideCeilingRestartRequired)));
169
173
  }
170
174
  /** Atomically replace optional persisted project grants. */
171
175
  async replaceProjectGrants(grants) {