rea-agents 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/application/ArtifactExtraction.js +5 -1
- package/dist/application/ArtifactGraphConstruction.js +3 -15
- package/dist/application/ArtifactInventory/classify.js +5 -1
- package/dist/application/ArtifactInventory/reader.js +2 -0
- package/dist/application/BinarySessionStatus.js +1 -0
- package/dist/application/CapabilityInventory.js +11 -3
- package/dist/application/ProcessCaptureEnvironment.js +26 -0
- package/dist/application/ProcessCaptureLifecycle.js +1 -0
- package/dist/application/ProcessCheckpoints.js +5 -0
- package/dist/application/ProcessHarness.js +45 -34
- package/dist/application/ProcessNormalization.js +6 -0
- package/dist/application/ProcessReactiveCoordinator.js +10 -3
- package/dist/application/ProcessReactiveEffects.js +9 -1
- package/dist/application/ProcessReactiveHarness.js +85 -0
- package/dist/application/ProcessReactiveObservations.js +101 -16
- package/dist/contracts/toolOutputSchemaPrimitives.js +3 -0
- package/dist/domain/analysisSnapshot.js +2 -0
- package/dist/domain/artifactGraph.js +2 -0
- package/dist/domain/binaryTarget.js +3 -6
- package/dist/domain/evidence.js +2 -0
- package/dist/domain/processCapture.js +64 -0
- package/dist/domain/processCaptureReactiveValidation.js +175 -0
- package/dist/domain/processCaptureValidation.js +2 -0
- package/dist/domain/processObservation.js +15 -6
- package/dist/domain/processReactiveRuntime.js +21 -10
- package/dist/domain/processReactiveTransition.js +8 -6
- package/dist/domain/processScenario.js +21 -0
- package/dist/domain/zipPackageFormat.js +20 -0
- package/dist/generatedPackageMetadata.js +1 -1
- package/dist/native/NativeMacOSProvider.js +1 -0
- package/package.json +1 -1
- package/skills/reverse-engineer-anything/SKILL.md +3 -2
- package/skills/reverse-engineer-anything/references/native-and-artifacts.md +2 -2
package/README.md
CHANGED
|
@@ -376,7 +376,7 @@ REA handles the app analysis in steps 1–5. The agent performs step 6 with its
|
|
|
376
376
|
| Native inspection | 33 | procedures, pseudocode, assembly, strings, names, segments, callers, callees, xrefs, annotations |
|
|
377
377
|
| Investigation workflows | 10 | `binary_overview`, `analyze_function`, `batch_decompile`, `trace_feature`, call graphs, Swift and Objective-C discovery |
|
|
378
378
|
| Native macOS utilities | 5 | Mach-O metadata, code signatures, plists, architectures, Swift demangling; Hopper-free and provenance-bearing |
|
|
379
|
-
| Artifact graph | 2 | deterministic directory, ZIP/APK/IPA, and ASAR inventory; explicitly selected extraction into an absent owned tree
|
|
379
|
+
| Artifact graph | 2 | deterministic directory, ZIP/APK/IPA/MSIX/AppX, and ASAR inventory; explicitly selected extraction into an absent owned tree |
|
|
380
380
|
| Managed PE/CLI | 8 | PE/CLI identity, metadata members, CIL hashes, P/Invoke/native-boundary declarations and verification, application-graph projection, decompiler reconstruction import, token remapping, runtime-correlation plans, and version comparison |
|
|
381
381
|
| Browser observation | 8 | exact-origin CDP capture, bundle and source-map analysis, WebMCP discovery, session timelines, capture diff, and visual evidence |
|
|
382
382
|
| Electron analysis | 4 | passive root-confined observation, bounded static application mapping, and evidence-backed static/runtime reconciliation |
|
|
@@ -479,7 +479,7 @@ Setup detects Claude Code, Claude Desktop, Codex, Cursor, Gemini CLI, Windsurf,
|
|
|
479
479
|
"mcpServers": {
|
|
480
480
|
"rea": {
|
|
481
481
|
"command": "npx",
|
|
482
|
-
"args": ["-y", "rea-agents@2.
|
|
482
|
+
"args": ["-y", "rea-agents@2.4.0", "mcp"]
|
|
483
483
|
}
|
|
484
484
|
}
|
|
485
485
|
}
|
|
@@ -140,7 +140,11 @@ const createReader = async (path, format) => {
|
|
|
140
140
|
return new DirectoryArtifactReader(path);
|
|
141
141
|
if (format === "asar")
|
|
142
142
|
return new AsarArtifactReader(path);
|
|
143
|
-
if (format === "ipa" ||
|
|
143
|
+
if (format === "ipa" ||
|
|
144
|
+
format === "apk" ||
|
|
145
|
+
format === "msix" ||
|
|
146
|
+
format === "appx" ||
|
|
147
|
+
format === "zip")
|
|
144
148
|
return new ZipArtifactReader(path, format);
|
|
145
149
|
if (format === "mach-o")
|
|
146
150
|
return new MachOSliceArtifactReader(path);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { extname } from "node:path";
|
|
3
2
|
import canonicalize from "canonicalize";
|
|
3
|
+
import { zipPackageFormatForPath } from "../domain/zipPackageFormat.js";
|
|
4
4
|
/** Index the first occurrence for each canonical logical path. */
|
|
5
5
|
export const indexOccurrencesByPath = (occurrences) => {
|
|
6
6
|
const byPath = new Map();
|
|
@@ -74,7 +74,7 @@ export const createRootNode = (input) => createArtifactNode({
|
|
|
74
74
|
}),
|
|
75
75
|
size: input.digest?.bytes ?? 0,
|
|
76
76
|
kind: input.directory ||
|
|
77
|
-
["zip", "ipa", "apk", "asar", "dmg", "pkg"].includes(input.format)
|
|
77
|
+
["zip", "ipa", "apk", "msix", "appx", "asar", "dmg", "pkg"].includes(input.format)
|
|
78
78
|
? "container"
|
|
79
79
|
: classifyArtifactPath(input.path).kind,
|
|
80
80
|
format: input.format,
|
|
@@ -174,7 +174,7 @@ export const classifyArtifactPath = (path) => {
|
|
|
174
174
|
return { kind: "javascript", format: "javascript-bundle" };
|
|
175
175
|
if (lower.endsWith(".asar"))
|
|
176
176
|
return { kind: "container", format: "asar" };
|
|
177
|
-
const archiveFormat =
|
|
177
|
+
const archiveFormat = zipPackageFormatForPath(lower);
|
|
178
178
|
if (archiveFormat !== undefined)
|
|
179
179
|
return { kind: "container", format: archiveFormat };
|
|
180
180
|
if (/\.framework(?:\/|$)/u.test(lower))
|
|
@@ -251,16 +251,4 @@ const relationFor = (path) => {
|
|
|
251
251
|
return "embeds";
|
|
252
252
|
return "contains";
|
|
253
253
|
};
|
|
254
|
-
const archiveFormatForExtension = (extension) => {
|
|
255
|
-
switch (extension) {
|
|
256
|
-
case ".zip":
|
|
257
|
-
return "zip";
|
|
258
|
-
case ".ipa":
|
|
259
|
-
return "ipa";
|
|
260
|
-
case ".apk":
|
|
261
|
-
return "apk";
|
|
262
|
-
default:
|
|
263
|
-
return undefined;
|
|
264
|
-
}
|
|
265
|
-
};
|
|
266
254
|
const depth = (path) => path.split("/").length;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { open } from "node:fs/promises";
|
|
2
2
|
import { classifyArtifactPath } from "../ArtifactGraphConstruction.js";
|
|
3
|
+
import { zipPackageFormatForPath } from "../../domain/zipPackageFormat.js";
|
|
3
4
|
const classifyContainerExtension = (path) => {
|
|
4
5
|
const lower = path.toLowerCase();
|
|
5
|
-
|
|
6
|
+
const zipPackage = zipPackageFormatForPath(lower);
|
|
7
|
+
if (zipPackage !== undefined)
|
|
8
|
+
return zipPackage;
|
|
9
|
+
for (const format of ["asar", "dmg", "pkg"])
|
|
6
10
|
if (lower.endsWith(`.${format}`))
|
|
7
11
|
return format;
|
|
8
12
|
return undefined;
|
|
@@ -91,6 +91,7 @@ const capabilityStatus = (descriptor) => ({
|
|
|
91
91
|
operation: descriptor.operation,
|
|
92
92
|
available: descriptor.available,
|
|
93
93
|
reason: descriptor.reason,
|
|
94
|
+
availability_code: descriptor.availabilityCode ?? null,
|
|
94
95
|
input_contract_version: descriptor.inputContractVersion,
|
|
95
96
|
output_contract_version: descriptor.outputContractVersion,
|
|
96
97
|
pagination: descriptor.pagination,
|
|
@@ -8,6 +8,7 @@ const statusSchema = z.object({
|
|
|
8
8
|
operation: z.string(),
|
|
9
9
|
available: z.boolean(),
|
|
10
10
|
reason: z.string().nullable(),
|
|
11
|
+
availability_code: z.string().nullable().optional(),
|
|
11
12
|
})),
|
|
12
13
|
});
|
|
13
14
|
const ENHANCED_REQUIREMENTS = {
|
|
@@ -35,7 +36,14 @@ const ENHANCED_REQUIREMENTS = {
|
|
|
35
36
|
/** Build stable per-operation availability without hiding familiar tools. */
|
|
36
37
|
export const buildCapabilityInventory = (sessionStatus, policy) => {
|
|
37
38
|
const status = statusSchema.parse(sessionStatus);
|
|
38
|
-
const descriptors = new Map(status.capabilities.map((descriptor) => [
|
|
39
|
+
const descriptors = new Map(status.capabilities.map((descriptor) => [
|
|
40
|
+
descriptor.operation,
|
|
41
|
+
{
|
|
42
|
+
available: descriptor.available,
|
|
43
|
+
reason: descriptor.reason,
|
|
44
|
+
availability_code: descriptor.availability_code ?? null,
|
|
45
|
+
},
|
|
46
|
+
]));
|
|
39
47
|
return TOOL_CONTRACTS.map((contract) => {
|
|
40
48
|
const availability = availabilityFor({
|
|
41
49
|
name: contract.name,
|
|
@@ -157,7 +165,7 @@ const providerAvailability = ({ descriptors, kind, name, }) => {
|
|
|
157
165
|
remediation: "Install or configure a provider that declares this operation.",
|
|
158
166
|
};
|
|
159
167
|
if (!descriptor.available &&
|
|
160
|
-
descriptor.
|
|
168
|
+
descriptor.availability_code === "unsupported_host")
|
|
161
169
|
return {
|
|
162
170
|
reason: "unsupported_host",
|
|
163
171
|
remediation: descriptor.reason,
|
|
@@ -187,7 +195,7 @@ const composedAvailability = (name, descriptors) => {
|
|
|
187
195
|
.find((descriptor) => descriptor?.available === false);
|
|
188
196
|
if (unavailable !== undefined)
|
|
189
197
|
return {
|
|
190
|
-
reason: unavailable.
|
|
198
|
+
reason: unavailable.availability_code === "unsupported_host"
|
|
191
199
|
? "unsupported_host"
|
|
192
200
|
: "provider_unavailable",
|
|
193
201
|
remediation: unavailable.reason ?? "Choose another target or configured provider.",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Build the explicitly admitted environment for a captured process. */
|
|
2
|
+
export const makeProcessCaptureEnvironment = (options) => {
|
|
3
|
+
const { scenario, home, replay, shimReplay, runId } = options;
|
|
4
|
+
const environment = {
|
|
5
|
+
...scenario.environment,
|
|
6
|
+
HOME: home,
|
|
7
|
+
TERM: "xterm-256color",
|
|
8
|
+
REA_PROCESS_RUN_ID: runId,
|
|
9
|
+
};
|
|
10
|
+
for (const name of scenario.inherit_environment) {
|
|
11
|
+
const value = process.env[name];
|
|
12
|
+
if (value !== undefined)
|
|
13
|
+
environment[name] = value;
|
|
14
|
+
}
|
|
15
|
+
environment.REA_REPLAY_HTTP_URL = replay.httpUrl;
|
|
16
|
+
environment.REA_REPLAY_WEBSOCKET_URL = replay.websocketUrl;
|
|
17
|
+
environment.REA_SHIM_LEDGER_URL = shimReplay.url;
|
|
18
|
+
// PATH is evidence-producing input. Inheriting the host PATH here would let
|
|
19
|
+
// the authority and reconstruction probe different tools without recording
|
|
20
|
+
// that difference. Scenarios must opt in through environment or
|
|
21
|
+
// inherit_environment; deterministic shims always take precedence.
|
|
22
|
+
environment.PATH = [shimReplay.binPath, environment.PATH ?? ""]
|
|
23
|
+
.filter((part) => part.length > 0)
|
|
24
|
+
.join(":");
|
|
25
|
+
return environment;
|
|
26
|
+
};
|
|
@@ -33,6 +33,7 @@ export const buildCaptureResult = (options) => ({
|
|
|
33
33
|
shim_events: options.shimEvents,
|
|
34
34
|
protocol_events: redactProtocolEvents(options.replay.events, options.scenario),
|
|
35
35
|
replay_transitions: options.replay.transitions,
|
|
36
|
+
reactive_run: options.reactiveRun,
|
|
36
37
|
event_journal: options.eventJournal,
|
|
37
38
|
files_before: options.before.files,
|
|
38
39
|
files_after: options.after.files,
|
|
@@ -79,6 +79,11 @@ export class ProcessCheckpoints {
|
|
|
79
79
|
capture(name) {
|
|
80
80
|
this.#scheduleCapture(name, this.#signal);
|
|
81
81
|
}
|
|
82
|
+
/** Read one committed checkpoint for synchronous journal projection. */
|
|
83
|
+
at(index) {
|
|
84
|
+
const checkpoint = this.#captures[index];
|
|
85
|
+
return checkpoint === undefined ? undefined : structuredClone(checkpoint);
|
|
86
|
+
}
|
|
82
87
|
#scheduleCapture(name, signal) {
|
|
83
88
|
if (this.#captured.has(name))
|
|
84
89
|
return;
|
|
@@ -8,37 +8,14 @@ import { snapshotRoots } from "./FilesystemSnapshot.js";
|
|
|
8
8
|
import { ProcessCheckpoints } from "./ProcessCheckpoints.js";
|
|
9
9
|
import { TerminalRenderer } from "./TerminalRenderer.js";
|
|
10
10
|
import { startCommandShimReplay, } from "./CommandShimReplay.js";
|
|
11
|
-
import { normalizeProcessText } from "./ProcessNormalization.js";
|
|
11
|
+
import { normalizeProcessShimEvent, normalizeProcessText, } from "./ProcessNormalization.js";
|
|
12
12
|
import { selectCapturedProcessGroupIds } from "../process/ProcessOwnership.js";
|
|
13
13
|
import { awaitTerminalExit, buildCaptureResult, captureTerminalFrames, createRunManifest, observeSettlement, prepareProcessCapture, releaseProcessResources, resolveProcessResult, } from "./ProcessCaptureLifecycle.js";
|
|
14
14
|
import { assertNotCancelled } from "./ProcessCaptureAuthority.js";
|
|
15
15
|
import { createProcessCaptureJournal, scheduleScenarioInteractions, } from "./ProcessCaptureJournal.js";
|
|
16
|
+
import { assertSupportedReactiveScenario, projectProcessReactiveRun, startProcessReactiveHarness, } from "./ProcessReactiveHarness.js";
|
|
17
|
+
import { makeProcessCaptureEnvironment } from "./ProcessCaptureEnvironment.js";
|
|
16
18
|
export { probeProcessCaptureCapability } from "./ProcessCaptureCapability.js";
|
|
17
|
-
const makeEnvironment = (options) => {
|
|
18
|
-
const { scenario, home, replay, shimReplay, runId } = options;
|
|
19
|
-
const environment = {
|
|
20
|
-
...scenario.environment,
|
|
21
|
-
HOME: home,
|
|
22
|
-
TERM: "xterm-256color",
|
|
23
|
-
REA_PROCESS_RUN_ID: runId,
|
|
24
|
-
};
|
|
25
|
-
for (const name of scenario.inherit_environment) {
|
|
26
|
-
const value = process.env[name];
|
|
27
|
-
if (value !== undefined)
|
|
28
|
-
environment[name] = value;
|
|
29
|
-
}
|
|
30
|
-
environment.REA_REPLAY_HTTP_URL = replay.httpUrl;
|
|
31
|
-
environment.REA_REPLAY_WEBSOCKET_URL = replay.websocketUrl;
|
|
32
|
-
environment.REA_SHIM_LEDGER_URL = shimReplay.url;
|
|
33
|
-
// PATH is evidence-producing input. Inheriting the host PATH here would let
|
|
34
|
-
// the authority and reconstruction probe different tools without recording
|
|
35
|
-
// that difference. Scenarios must opt in through environment or
|
|
36
|
-
// inherit_environment; deterministic shims always take precedence.
|
|
37
|
-
environment.PATH = [shimReplay.binPath, environment.PATH ?? ""]
|
|
38
|
-
.filter((part) => part.length > 0)
|
|
39
|
-
.join(":");
|
|
40
|
-
return environment;
|
|
41
|
-
};
|
|
42
19
|
const cleanupFailedStartup = async (options) => {
|
|
43
20
|
options.terminal?.kill("SIGKILL");
|
|
44
21
|
const cleanupFailure = await releaseProcessResources({
|
|
@@ -61,6 +38,11 @@ const createTerminalRenderer = (scenario, temporaryRoot, terminalPid, recordEven
|
|
|
61
38
|
normalize: (value) => normalizeProcessText(value, scenario, temporaryRoot, terminalPid()),
|
|
62
39
|
recordEvent,
|
|
63
40
|
});
|
|
41
|
+
const reactiveCapture = (options, protocolEvents) => ({
|
|
42
|
+
...options,
|
|
43
|
+
processSamples: options.samples,
|
|
44
|
+
protocolEvents,
|
|
45
|
+
});
|
|
64
46
|
const startCaptureRuntime = async (options) => {
|
|
65
47
|
const { scenario } = options;
|
|
66
48
|
let replay;
|
|
@@ -68,6 +50,7 @@ const startCaptureRuntime = async (options) => {
|
|
|
68
50
|
let checkpoints;
|
|
69
51
|
let shimReplay;
|
|
70
52
|
let terminal;
|
|
53
|
+
let reactive;
|
|
71
54
|
try {
|
|
72
55
|
const { spawn } = await import("@lydell/node-pty");
|
|
73
56
|
replay = await startLoopbackReplay(scenario, options.recordEvent);
|
|
@@ -82,7 +65,7 @@ const startCaptureRuntime = async (options) => {
|
|
|
82
65
|
shimReplay = await startCommandShimReplay(scenario, options.temporaryRoot, started, options.recordEvent);
|
|
83
66
|
terminal = spawn(scenario.executable, [...scenario.arguments], {
|
|
84
67
|
cwd: scenario.working_directory,
|
|
85
|
-
env:
|
|
68
|
+
env: makeProcessCaptureEnvironment({ ...options, replay, shimReplay }),
|
|
86
69
|
cols: scenario.terminal.columns,
|
|
87
70
|
rows: scenario.terminal.rows,
|
|
88
71
|
name: "xterm-256color",
|
|
@@ -95,6 +78,15 @@ const startCaptureRuntime = async (options) => {
|
|
|
95
78
|
renderer,
|
|
96
79
|
checkpoints,
|
|
97
80
|
});
|
|
81
|
+
reactive = startProcessReactiveHarness({
|
|
82
|
+
capture: reactiveCapture(options, () => replay?.events ?? []),
|
|
83
|
+
scenario,
|
|
84
|
+
terminal: () => terminal,
|
|
85
|
+
renderer,
|
|
86
|
+
checkpoints,
|
|
87
|
+
shimReplay,
|
|
88
|
+
started,
|
|
89
|
+
});
|
|
98
90
|
scheduleScenarioInteractions({
|
|
99
91
|
...options,
|
|
100
92
|
getTerminal: () => terminal,
|
|
@@ -120,9 +112,12 @@ const startCaptureRuntime = async (options) => {
|
|
|
120
112
|
lastOutput: () => lastOutput,
|
|
121
113
|
framesTruncated,
|
|
122
114
|
stopSampler,
|
|
115
|
+
reactive,
|
|
123
116
|
};
|
|
124
117
|
}
|
|
125
118
|
catch (cause) {
|
|
119
|
+
reactive?.unsubscribe();
|
|
120
|
+
await reactive?.coordinator.close();
|
|
126
121
|
return cleanupFailedStartup({
|
|
127
122
|
cause,
|
|
128
123
|
timers: options.timers,
|
|
@@ -136,12 +131,9 @@ const startCaptureRuntime = async (options) => {
|
|
|
136
131
|
});
|
|
137
132
|
}
|
|
138
133
|
};
|
|
139
|
-
const normalizedShimEvents = (runtime, scenario, temporaryRoot) => runtime.shimReplay.events.map((event) => (
|
|
140
|
-
...event,
|
|
141
|
-
arguments: event.arguments.map((argument) => normalizeProcessText(argument, scenario, temporaryRoot, runtime.terminal.pid)),
|
|
142
|
-
working_directory: normalizeProcessText(event.working_directory, scenario, temporaryRoot, runtime.terminal.pid),
|
|
143
|
-
}));
|
|
134
|
+
const normalizedShimEvents = (runtime, scenario, temporaryRoot) => runtime.shimReplay.events.map((event) => normalizeProcessShimEvent(event, scenario, temporaryRoot, runtime.terminal.pid));
|
|
144
135
|
const finishProcessRun = async (options) => {
|
|
136
|
+
options.runtime?.reactive?.unsubscribe();
|
|
145
137
|
await options.stopSampler();
|
|
146
138
|
const cleanupFailure = await releaseProcessResources({
|
|
147
139
|
timers: options.timers,
|
|
@@ -156,10 +148,16 @@ const finishProcessRun = async (options) => {
|
|
|
156
148
|
? []
|
|
157
149
|
: selectCapturedProcessGroupIds(options.runtime.terminal.pid, options.samples),
|
|
158
150
|
});
|
|
151
|
+
if (cleanupFailure !== undefined)
|
|
152
|
+
await options.runtime?.reactive?.coordinator.submit({
|
|
153
|
+
kind: "cleanup_failed",
|
|
154
|
+
});
|
|
155
|
+
await options.runtime?.reactive?.coordinator.close();
|
|
159
156
|
let { capture, executionFailure } = options;
|
|
160
157
|
if (capture !== undefined) {
|
|
161
158
|
capture = {
|
|
162
159
|
...capture,
|
|
160
|
+
reactive_run: projectProcessReactiveRun(options.runtime?.reactive),
|
|
163
161
|
settlement: {
|
|
164
162
|
...capture.settlement,
|
|
165
163
|
cleanup_outcome: capture.settlement.state === "quiesced"
|
|
@@ -179,8 +177,17 @@ const completeCapture = async (options) => {
|
|
|
179
177
|
const { runtime, scenario } = options;
|
|
180
178
|
runtime.checkpoints.trigger("root_exit");
|
|
181
179
|
const { reason } = options.exit;
|
|
182
|
-
if (reason === "cancelled")
|
|
180
|
+
if (reason === "cancelled") {
|
|
181
|
+
if (runtime.reactive !== undefined) {
|
|
182
|
+
await runtime.reactive.coordinator.submit({ kind: "cancelled" });
|
|
183
|
+
runtime.reactive.unsubscribe();
|
|
184
|
+
}
|
|
183
185
|
throw processCaptureCancelled();
|
|
186
|
+
}
|
|
187
|
+
if (runtime.reactive !== undefined) {
|
|
188
|
+
await runtime.reactive.coordinator.submit({ kind: "target_lost" });
|
|
189
|
+
runtime.reactive.unsubscribe();
|
|
190
|
+
}
|
|
184
191
|
const settlement = await observeSettlement(options.runId, selectCapturedProcessGroupIds(runtime.terminal.pid, options.samples), scenario.settle_ms, options.recordEvent);
|
|
185
192
|
runtime.checkpoints.trigger("settled");
|
|
186
193
|
const samplingPartial = (await runtime.stopSampler()).partial;
|
|
@@ -203,6 +210,7 @@ const completeCapture = async (options) => {
|
|
|
203
210
|
exit: { ...options.exit, reason },
|
|
204
211
|
samples: options.samples,
|
|
205
212
|
replay: runtime.replay,
|
|
213
|
+
reactiveRun: projectProcessReactiveRun(runtime.reactive),
|
|
206
214
|
before: options.before,
|
|
207
215
|
after,
|
|
208
216
|
truncated,
|
|
@@ -220,10 +228,12 @@ const completeCapture = async (options) => {
|
|
|
220
228
|
};
|
|
221
229
|
/** Execute one authorized scenario and return bounded observations. */
|
|
222
230
|
const runProcessScenario = async (scenario, policy, signal) => {
|
|
231
|
+
assertSupportedReactiveScenario(scenario);
|
|
223
232
|
const { temporaryRoot, runId, home, before } = await prepareProcessCapture(scenario, policy, signal);
|
|
224
233
|
const frames = [];
|
|
225
234
|
const samples = [];
|
|
226
|
-
const
|
|
235
|
+
const journal = createProcessCaptureJournal();
|
|
236
|
+
const { entries: eventJournal, recordEvent } = journal;
|
|
227
237
|
let runtime;
|
|
228
238
|
const timers = new Set();
|
|
229
239
|
let capture;
|
|
@@ -244,6 +254,7 @@ const runProcessScenario = async (scenario, policy, signal) => {
|
|
|
244
254
|
timers,
|
|
245
255
|
dispatchedEventIndexes,
|
|
246
256
|
recordEvent,
|
|
257
|
+
journal,
|
|
247
258
|
...(signal === undefined ? {} : { signal }),
|
|
248
259
|
});
|
|
249
260
|
stopSampler = runtime.stopSampler;
|
|
@@ -54,6 +54,12 @@ export const normalizeProcessSamples = (samples, scenario, rootPid) => {
|
|
|
54
54
|
command: normalizeCommand(sample.command),
|
|
55
55
|
}));
|
|
56
56
|
};
|
|
57
|
+
/** Normalize one shim observation identically for live matching and capture. */
|
|
58
|
+
export const normalizeProcessShimEvent = (event, scenario, temporaryRoot, rootPid) => ({
|
|
59
|
+
...event,
|
|
60
|
+
arguments: event.arguments.map((argument) => normalizeProcessText(argument, scenario, temporaryRoot, rootPid)),
|
|
61
|
+
working_directory: normalizeProcessText(event.working_directory, scenario, temporaryRoot, rootPid),
|
|
62
|
+
});
|
|
57
63
|
const normalizePidTokens = (value, identifiers) => {
|
|
58
64
|
const tokens = [...new Set(identifiers)]
|
|
59
65
|
.filter((identifier) => Number.isSafeInteger(identifier) && identifier > 0)
|
|
@@ -153,7 +153,7 @@ export class ProcessReactiveCoordinator {
|
|
|
153
153
|
decision = commitProcessReactiveProposal(this.#scenario, decision, effectResults);
|
|
154
154
|
}
|
|
155
155
|
this.#snapshot = decision.snapshot;
|
|
156
|
-
this.#onDecision?.(decision);
|
|
156
|
+
this.#onDecision?.(decision, input);
|
|
157
157
|
if (this.#snapshot.status === "finished")
|
|
158
158
|
this.#clearTimers();
|
|
159
159
|
else if (this.#accepting &&
|
|
@@ -162,16 +162,23 @@ export class ProcessReactiveCoordinator {
|
|
|
162
162
|
this.#snapshot.state_entry_capture_order))
|
|
163
163
|
this.#scheduleStateDeadline();
|
|
164
164
|
for (const result of effectResults)
|
|
165
|
-
if (result.status === "succeeded"
|
|
165
|
+
if (result.status === "succeeded" &&
|
|
166
|
+
!this.#hasObservation(result.observation.event_id))
|
|
166
167
|
this.#enqueueOwned({
|
|
167
168
|
kind: "observation",
|
|
168
169
|
observation: result.observation,
|
|
169
170
|
});
|
|
170
171
|
}
|
|
172
|
+
#hasObservation(eventId) {
|
|
173
|
+
return (this.#snapshot.observations.some((observation) => observation.event_id === eventId) ||
|
|
174
|
+
this.#pendingInputs.some((input) => input.kind === "observation" &&
|
|
175
|
+
input.observation.event_id === eventId));
|
|
176
|
+
}
|
|
171
177
|
#interruptsEffects(input) {
|
|
172
178
|
if (input.kind === "cancelled" ||
|
|
173
179
|
input.kind === "cleanup_failed" ||
|
|
174
|
-
input.kind === "scenario_deadline"
|
|
180
|
+
input.kind === "scenario_deadline" ||
|
|
181
|
+
input.kind === "target_lost")
|
|
175
182
|
return true;
|
|
176
183
|
return (input.kind === "state_deadline" &&
|
|
177
184
|
input.state_id === this.#snapshot.active_state &&
|
|
@@ -109,7 +109,15 @@ const triggerSources = (trigger) => {
|
|
|
109
109
|
if (trigger.kind === "terminal_text")
|
|
110
110
|
return [];
|
|
111
111
|
if (trigger.kind === "event")
|
|
112
|
-
return [
|
|
112
|
+
return [
|
|
113
|
+
"terminal_raw",
|
|
114
|
+
"interaction",
|
|
115
|
+
"process",
|
|
116
|
+
"filesystem",
|
|
117
|
+
"http",
|
|
118
|
+
"websocket",
|
|
119
|
+
"shim",
|
|
120
|
+
].includes(trigger.source)
|
|
113
121
|
? []
|
|
114
122
|
: [trigger.source];
|
|
115
123
|
return trigger.kind === "repeat"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ProcessCaptureError } from "./ProcessCaptureError.js";
|
|
2
|
+
import { ProcessReactiveCoordinator } from "./ProcessReactiveCoordinator.js";
|
|
3
|
+
import { executeProcessReactiveEffects, unsupportedProcessReactiveFeatures, } from "./ProcessReactiveEffects.js";
|
|
4
|
+
import { subscribeProcessReactiveObservations } from "./ProcessReactiveObservations.js";
|
|
5
|
+
import { normalizeProcessSamples, normalizeProcessShimEvent, redactProtocolEvents, } from "./ProcessNormalization.js";
|
|
6
|
+
const controlKind = (input) => input.kind === "observation" ? undefined : input.kind;
|
|
7
|
+
/** Reject reactive declarations the current process adapter cannot execute. */
|
|
8
|
+
export const assertSupportedReactiveScenario = (scenario) => {
|
|
9
|
+
if (scenario.reactive === null)
|
|
10
|
+
return;
|
|
11
|
+
const unsupported = unsupportedProcessReactiveFeatures(scenario.reactive);
|
|
12
|
+
if (unsupported.length === 0)
|
|
13
|
+
return;
|
|
14
|
+
throw new ProcessCaptureError(`reactive scenario requests unsupported features: ${unsupported.join(", ")}`);
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Attach the serialized reactive runtime once the target PID is available.
|
|
18
|
+
*
|
|
19
|
+
* Subscription replays journal entries admitted during launcher startup before
|
|
20
|
+
* observing new records, so PID-dependent normalization cannot drop them.
|
|
21
|
+
*/
|
|
22
|
+
export const startProcessReactiveHarness = (options) => {
|
|
23
|
+
if (options.scenario.reactive === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
const controls = [];
|
|
26
|
+
const coordinator = new ProcessReactiveCoordinator({
|
|
27
|
+
scenario: options.scenario.reactive,
|
|
28
|
+
executor: {
|
|
29
|
+
execute: (actions, signal) => executeProcessReactiveEffects({
|
|
30
|
+
terminal: options.terminal,
|
|
31
|
+
renderer: options.renderer,
|
|
32
|
+
checkpoints: options.checkpoints,
|
|
33
|
+
interactions: options.capture.interactions,
|
|
34
|
+
journal: options.capture.journal,
|
|
35
|
+
startedAtMs: options.started,
|
|
36
|
+
now: Date.now,
|
|
37
|
+
}, actions, signal),
|
|
38
|
+
},
|
|
39
|
+
onDecision: (decision, input) => {
|
|
40
|
+
const kind = controlKind(input);
|
|
41
|
+
if (kind === undefined || decision.kind !== "finished")
|
|
42
|
+
return;
|
|
43
|
+
controls.push({
|
|
44
|
+
sequence: controls.length,
|
|
45
|
+
kind,
|
|
46
|
+
after_capture_order: decision.snapshot.observations.at(-1)?.capture_order ?? -1,
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
coordinator,
|
|
52
|
+
controls,
|
|
53
|
+
unsubscribe: subscribeProcessReactiveObservations({
|
|
54
|
+
journal: options.capture.journal,
|
|
55
|
+
frames: options.capture.frames,
|
|
56
|
+
interactions: options.capture.interactions,
|
|
57
|
+
checkpointAt: (index) => options.checkpoints.at(index),
|
|
58
|
+
processSampleAt: (index) => {
|
|
59
|
+
const rootPid = options.terminal()?.pid;
|
|
60
|
+
return rootPid === undefined
|
|
61
|
+
? undefined
|
|
62
|
+
: normalizeProcessSamples(options.capture.processSamples, options.scenario, rootPid)[index];
|
|
63
|
+
},
|
|
64
|
+
protocolEventAt: (index) => redactProtocolEvents(options.capture.protocolEvents().slice(index, index + 1), options.scenario)[0],
|
|
65
|
+
shimEventAt: (index) => {
|
|
66
|
+
const event = options.shimReplay.events[index];
|
|
67
|
+
const rootPid = options.terminal()?.pid;
|
|
68
|
+
return event === undefined || rootPid === undefined
|
|
69
|
+
? undefined
|
|
70
|
+
: normalizeProcessShimEvent(event, options.scenario, options.capture.temporaryRoot, rootPid);
|
|
71
|
+
},
|
|
72
|
+
sink: coordinator,
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
/** Project internal reducer state into the stable Process Capture shape. */
|
|
77
|
+
export const projectProcessReactiveRun = (harness) => harness === undefined
|
|
78
|
+
? null
|
|
79
|
+
: {
|
|
80
|
+
status: harness.coordinator.snapshot.status,
|
|
81
|
+
outcome: harness.coordinator.snapshot.outcome,
|
|
82
|
+
active_state: harness.coordinator.snapshot.active_state,
|
|
83
|
+
transitions: harness.coordinator.snapshot.transitions,
|
|
84
|
+
controls: harness.controls,
|
|
85
|
+
};
|
|
@@ -1,20 +1,105 @@
|
|
|
1
|
-
import { createProcessObservation } from "../domain/processObservation.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
if (frame === undefined)
|
|
8
|
-
return;
|
|
9
|
-
options.sink.enqueue({
|
|
10
|
-
kind: "observation",
|
|
11
|
-
observation: createProcessObservation({
|
|
1
|
+
import { createProcessObservation, processObservationSubject, } from "../domain/processObservation.js";
|
|
2
|
+
const terminalObservation = (options, entry) => {
|
|
3
|
+
const value = options.frames[entry.index];
|
|
4
|
+
return value === undefined
|
|
5
|
+
? undefined
|
|
6
|
+
: createProcessObservation({
|
|
12
7
|
source: "terminal_raw",
|
|
13
|
-
source_sequence:
|
|
14
|
-
captured_at_ms:
|
|
8
|
+
source_sequence: value.sequence,
|
|
9
|
+
captured_at_ms: value.at_ms,
|
|
15
10
|
subject_id: null,
|
|
16
11
|
location: entry,
|
|
17
|
-
payload:
|
|
18
|
-
})
|
|
12
|
+
payload: value,
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
const interactionObservation = (options, entry) => {
|
|
16
|
+
const value = options.interactions[entry.index];
|
|
17
|
+
return value === undefined
|
|
18
|
+
? undefined
|
|
19
|
+
: createProcessObservation({
|
|
20
|
+
source: "interaction",
|
|
21
|
+
source_sequence: value.sequence,
|
|
22
|
+
captured_at_ms: value.dispatched_at_ms,
|
|
23
|
+
subject_id: null,
|
|
24
|
+
location: entry,
|
|
25
|
+
payload: value,
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
const filesystemObservation = (options, entry) => {
|
|
29
|
+
const value = options.checkpointAt(entry.index);
|
|
30
|
+
return value === undefined
|
|
31
|
+
? undefined
|
|
32
|
+
: createProcessObservation({
|
|
33
|
+
source: "filesystem",
|
|
34
|
+
source_sequence: entry.index,
|
|
35
|
+
captured_at_ms: value.at_ms,
|
|
36
|
+
subject_id: processObservationSubject("filesystem", value),
|
|
37
|
+
location: entry,
|
|
38
|
+
payload: value,
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
const processObservation = (options, entry) => {
|
|
42
|
+
const value = options.processSampleAt(entry.index);
|
|
43
|
+
return value === undefined
|
|
44
|
+
? undefined
|
|
45
|
+
: createProcessObservation({
|
|
46
|
+
source: "process",
|
|
47
|
+
source_sequence: entry.index,
|
|
48
|
+
captured_at_ms: value.at_ms,
|
|
49
|
+
subject_id: processObservationSubject("process", value),
|
|
50
|
+
location: entry,
|
|
51
|
+
payload: value,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const protocolObservation = (options, entry) => {
|
|
55
|
+
const value = options.protocolEventAt(entry.index);
|
|
56
|
+
return value === undefined
|
|
57
|
+
? undefined
|
|
58
|
+
: createProcessObservation({
|
|
59
|
+
source: value.protocol,
|
|
60
|
+
source_sequence: value.sequence,
|
|
61
|
+
captured_at_ms: value.at_ms,
|
|
62
|
+
subject_id: processObservationSubject(value.protocol, value),
|
|
63
|
+
location: entry,
|
|
64
|
+
payload: value,
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const shimObservation = (options, entry) => {
|
|
68
|
+
const normalized = options.shimEventAt(entry.index);
|
|
69
|
+
if (normalized === undefined)
|
|
70
|
+
return undefined;
|
|
71
|
+
return createProcessObservation({
|
|
72
|
+
source: "shim",
|
|
73
|
+
source_sequence: normalized.sequence,
|
|
74
|
+
captured_at_ms: normalized.at_ms,
|
|
75
|
+
subject_id: processObservationSubject("shim", normalized),
|
|
76
|
+
location: entry,
|
|
77
|
+
payload: normalized,
|
|
19
78
|
});
|
|
20
|
-
}
|
|
79
|
+
};
|
|
80
|
+
const observationFor = (options, entry) => {
|
|
81
|
+
if (entry.collection === "frames")
|
|
82
|
+
return terminalObservation(options, entry);
|
|
83
|
+
if (entry.collection === "interaction_events")
|
|
84
|
+
return interactionObservation(options, entry);
|
|
85
|
+
if (entry.collection === "filesystem_checkpoints")
|
|
86
|
+
return filesystemObservation(options, entry);
|
|
87
|
+
if (entry.collection === "process_samples")
|
|
88
|
+
return processObservation(options, entry);
|
|
89
|
+
if (entry.collection === "protocol_events")
|
|
90
|
+
return protocolObservation(options, entry);
|
|
91
|
+
if (entry.collection === "shim_events")
|
|
92
|
+
return shimObservation(options, entry);
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
95
|
+
/** Subscribe the reactive coordinator to admitted normalized journal records. */
|
|
96
|
+
export const subscribeProcessReactiveObservations = (options) => {
|
|
97
|
+
const enqueue = (entry) => {
|
|
98
|
+
const observation = observationFor(options, entry);
|
|
99
|
+
if (observation !== undefined)
|
|
100
|
+
options.sink.enqueue({ kind: "observation", observation });
|
|
101
|
+
};
|
|
102
|
+
for (const entry of options.journal.entries)
|
|
103
|
+
enqueue(entry);
|
|
104
|
+
return options.journal.subscribe(enqueue);
|
|
105
|
+
};
|