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.
- package/README.md +84 -18
- package/dist/application/AndroidApplicationService.js +17 -0
- package/dist/application/AppleApplicationService.js +17 -0
- package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
- package/dist/application/BinarySession.js +9 -2
- package/dist/application/BinarySessionStatus.js +39 -1
- package/dist/application/CapabilityInventory.js +15 -0
- package/dist/application/ClientRegistrationStatus.js +11 -4
- package/dist/application/CommandShimReplay.js +6 -3
- package/dist/application/CompositeProvider.js +5 -2
- package/dist/application/DirectAnalysis.js +1 -20
- package/dist/application/DirectAnalysisStatus.js +83 -0
- package/dist/application/Doctor.js +34 -25
- package/dist/application/DoctorDiagnostics.js +1 -4
- package/dist/application/DoctorProjection.js +43 -0
- package/dist/application/DoctorScope.js +102 -0
- package/dist/application/EvidenceReferenceResolver.js +26 -0
- package/dist/application/InventoryProjectionEvidence.js +41 -0
- package/dist/application/InvestigationProviders.js +12 -0
- package/dist/application/JavaScriptApplicationEvidence.js +1 -1
- package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
- package/dist/application/JavaScriptApplicationService.js +3 -2
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
- package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
- package/dist/application/JavaScriptModuleRelationships.js +25 -1
- package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
- package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
- package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
- package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
- package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
- package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
- package/dist/application/JavaScriptSemanticTraceService.js +45 -0
- package/dist/application/LoopbackReplay.js +277 -133
- package/dist/application/LoopbackReplayRecorder.js +101 -0
- package/dist/application/PermissionAuthority.js +7 -3
- package/dist/application/ProcessCaptureJournal.js +95 -0
- package/dist/application/ProcessCaptureLifecycle.js +24 -13
- package/dist/application/ProcessCheckpoints.js +30 -4
- package/dist/application/ProcessCli.js +26 -4
- package/dist/application/ProcessHarness.js +20 -50
- package/dist/application/ProcessNormalization.js +3 -2
- package/dist/application/ProcessPairedExperiment.js +72 -0
- package/dist/application/ProcessReactiveCoordinator.js +218 -0
- package/dist/application/ProcessReactiveEffects.js +133 -0
- package/dist/application/ProcessReactiveObservations.js +20 -0
- package/dist/application/ProcessSampling.js +2 -0
- package/dist/application/SessionProviderRouter.js +7 -6
- package/dist/application/Setup.js +14 -13
- package/dist/application/SetupClientConfiguration.js +10 -6
- package/dist/application/SetupPlan.js +4 -3
- package/dist/application/SetupRegistrationEnvironment.js +12 -0
- package/dist/application/SetupSkill.js +44 -40
- package/dist/application/TerminalRenderer.js +3 -1
- package/dist/application/Uninstall.js +7 -1
- package/dist/application/Upgrade.js +16 -1
- package/dist/browser/CdpEndpoint.js +37 -4
- package/dist/catalogIdentity.js +16 -7
- package/dist/cli/coreAnalysisCommands.js +34 -2
- package/dist/cli/javascriptApplicationAnalysis.js +19 -0
- package/dist/cli/setupCommands.js +84 -30
- package/dist/cli/utilityCommands.js +10 -2
- package/dist/cliApplicationCommands.js +79 -16
- package/dist/cliCommandNames.js +3 -0
- package/dist/cliElectronCommands.js +23 -33
- package/dist/cliObservationOptions.js +1 -1
- package/dist/cliOutput.js +56 -0
- package/dist/cliProcessCommands.js +32 -1
- package/dist/cliSetup.js +55 -29
- package/dist/contracts/applicationToolContracts.js +54 -7
- package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
- package/dist/contracts/electronToolContracts.js +73 -4
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
- package/dist/contracts/managedToolContracts.js +7 -2
- package/dist/contracts/processCaptureExample.js +1 -0
- package/dist/contracts/replayMachineExample.js +37 -0
- package/dist/contracts/sessionStatusContract.js +26 -0
- package/dist/contracts/toolContractExamples.js +2 -0
- package/dist/contracts/toolContractTypes.js +12 -1
- package/dist/contracts/toolContracts.js +8 -11
- package/dist/contracts/toolEffects.js +3 -0
- package/dist/contracts/toolOutputSchemaGroups.js +53 -9
- package/dist/contracts/toolOutputSchemaPrimitives.js +73 -32
- package/dist/doctorRuntime.js +41 -0
- package/dist/domain/analysisErrorPresentation.js +5 -3
- package/dist/domain/analysisErrorProjection.js +7 -1
- package/dist/domain/androidApplication.js +206 -0
- package/dist/domain/appleApplication.js +221 -0
- package/dist/domain/boundedCartesianProjection.js +15 -0
- package/dist/domain/changedBehavior.js +5 -4
- package/dist/domain/completionLedgerGeneration.js +190 -0
- package/dist/domain/errors.js +4 -2
- package/dist/domain/evidenceCompletionLedger.js +113 -0
- package/dist/domain/hopperStartupFailure.js +9 -0
- package/dist/domain/javascriptApplicationAnalysis.js +44 -2
- package/dist/domain/javascriptExportShapeComparison.js +145 -0
- package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
- package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
- package/dist/domain/javascriptExportShapeSelection.js +132 -0
- package/dist/domain/javascriptExportShapeVariants.js +272 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
- package/dist/domain/javascriptSemanticAnalysis.js +17 -36
- package/dist/domain/javascriptSemanticCallResolution.js +129 -0
- package/dist/domain/javascriptSemanticCalls.js +269 -0
- package/dist/domain/javascriptSemanticGraph.js +356 -0
- package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
- package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
- package/dist/domain/javascriptSemanticIr.js +14 -1
- package/dist/domain/javascriptSemanticPrimitives.js +36 -0
- package/dist/domain/javascriptSemanticProjection.js +36 -4
- package/dist/domain/javascriptSemanticProvenance.js +29 -0
- package/dist/domain/javascriptSemanticQuery.js +294 -0
- package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
- package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
- package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
- package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
- package/dist/domain/javascriptSemanticReturns.js +187 -0
- package/dist/domain/javascriptSemanticState.js +32 -0
- package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
- package/dist/domain/javascriptSemanticValues.js +54 -70
- package/dist/domain/processCapture.js +36 -0
- package/dist/domain/processCaptureValidation.js +101 -12
- package/dist/domain/processComparison.js +96 -24
- package/dist/domain/processObservation.js +153 -0
- package/dist/domain/processPairedExperiment.js +128 -0
- package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
- package/dist/domain/processReactiveMatching.js +210 -0
- package/dist/domain/processReactiveRuntime.js +109 -0
- package/dist/domain/processReactiveScenario.js +361 -0
- package/dist/domain/processReactiveScenarioPreflight.js +120 -0
- package/dist/domain/processReactiveTransition.js +159 -0
- package/dist/domain/processScenario.js +12 -0
- package/dist/domain/processTraceComparison.js +43 -0
- package/dist/domain/processTraceDimensionProjection.js +50 -0
- package/dist/domain/processTraceEvaluation.js +312 -0
- package/dist/domain/processTraceSpecification.js +373 -0
- package/dist/domain/reconstructionVerification.js +5 -4
- package/dist/domain/replayMachine.js +332 -0
- package/dist/domain/replayMachineRun.js +216 -0
- package/dist/domain/replayMachineRuntime.js +294 -0
- package/dist/domain/runtimeIdentification.js +210 -0
- package/dist/domain/staticRuntimeCorrelation.js +4 -2
- package/dist/evaluation/CodexAgentEval.js +156 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/ghidra/GhidraClient.js +13 -2
- package/dist/ghidra/GhidraDiagnostics.js +1 -0
- package/dist/ghidra/GhidraLauncher.js +7 -3
- package/dist/ghidra/GhidraProvider.js +8 -1
- package/dist/hopper/BridgeLauncher.js +56 -14
- package/dist/hopper/HopperClient.js +23 -10
- package/dist/hopper/HopperDiagnostics.js +14 -0
- package/dist/hopper/HopperProcessDiagnostic.js +14 -0
- package/dist/hopper/HopperProvider.js +8 -1
- package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
- package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
- package/dist/identity.js +2 -1
- package/dist/main/transport.js +2 -0
- package/dist/mcpDoctor.js +310 -0
- package/dist/process/ProcessOwnership.js +200 -53
- package/dist/process/ProviderRunLineage.js +21 -0
- package/dist/server/createServer.js +8 -2
- package/dist/server/javascriptApplicationResult.js +106 -0
- package/dist/server/registerApplicationTools/characterization.js +3 -3
- package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
- package/dist/server/registerApplicationTools/compareVersions.js +12 -6
- package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
- package/dist/server/registerApplicationTools/coverage.js +3 -3
- package/dist/server/registerApplicationTools/helpers.js +37 -25
- package/dist/server/registerApplicationTools/traceFeature.js +9 -5
- package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
- package/dist/server/registerApplicationTools.js +4 -0
- package/dist/server/registerElectronTools.js +19 -5
- package/dist/server/registerEvidenceResources.js +7 -1
- package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
- package/dist/server/registerManagedTools.js +18 -1
- package/dist/server/registerProcessComparisonTool.js +17 -7
- package/dist/server/registerReplayMachineTool.js +20 -0
- package/dist/server/registerSessionStatusTool.js +95 -25
- package/dist/server/registerSessionTools.js +12 -7
- package/dist/server/sessionAvailabilityPolicy.js +2 -1
- package/dist/server/toolResult.js +10 -4
- package/dist/serverIdentity.js +19 -0
- package/package.json +62 -29
- package/scripts/hopper-demo-x11.py +459 -61
- package/scripts/prepack.mjs +55 -0
- package/scripts/prepare.mjs +28 -0
- package/scripts/rea.mjs +26 -5
- package/skills/reverse-engineer-anything/SKILL.md +69 -228
- package/skills/reverse-engineer-anything/references/controlled-replay.md +12 -0
- package/skills/reverse-engineer-anything/references/evidence-workflows.md +33 -0
- package/skills/reverse-engineer-anything/references/javascript-applications.md +35 -0
- package/skills/reverse-engineer-anything/references/native-and-artifacts.md +36 -0
- package/skills/reverse-engineer-anything/references/runtime-observation.md +24 -0
|
@@ -1,113 +1,144 @@
|
|
|
1
1
|
import { createServer, } from "node:http";
|
|
2
2
|
import { WebSocketServer } from "ws";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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:
|
|
76
|
-
outcome,
|
|
113
|
+
data: "<body-over-limit>",
|
|
114
|
+
outcome: "unmatched",
|
|
77
115
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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 (
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
353
|
+
port = await listenOnLoopback(server);
|
|
223
354
|
}
|
|
224
355
|
catch (cause) {
|
|
225
|
-
await
|
|
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
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|