rea-agents 2.2.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 +57 -10
- package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
- 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/BinarySession.js +9 -2
- package/dist/application/BinarySessionStatus.js +40 -1
- package/dist/application/CapabilityInventory.js +26 -3
- 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/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/ProcessCaptureEnvironment.js +26 -0
- package/dist/application/ProcessCaptureJournal.js +95 -0
- package/dist/application/ProcessCaptureLifecycle.js +25 -13
- package/dist/application/ProcessCheckpoints.js +35 -4
- package/dist/application/ProcessCli.js +26 -4
- package/dist/application/ProcessHarness.js +64 -83
- package/dist/application/ProcessNormalization.js +9 -2
- package/dist/application/ProcessReactiveCoordinator.js +225 -0
- package/dist/application/ProcessReactiveEffects.js +141 -0
- package/dist/application/ProcessReactiveHarness.js +85 -0
- package/dist/application/ProcessReactiveObservations.js +105 -0
- package/dist/application/ProcessSampling.js +2 -0
- package/dist/application/SessionProviderRouter.js +7 -6
- package/dist/application/Setup.js +11 -10
- package/dist/application/SetupPlan.js +1 -1
- package/dist/application/SetupRegistrationEnvironment.js +12 -0
- package/dist/application/TerminalRenderer.js +3 -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 +81 -32
- 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 +30 -21
- package/dist/contracts/applicationToolContracts.js +54 -7
- package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
- package/dist/contracts/electronToolContracts.js +53 -32
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
- package/dist/contracts/processCaptureExample.js +1 -0
- package/dist/contracts/replayMachineExample.js +37 -0
- package/dist/contracts/toolContractExamples.js +2 -0
- package/dist/contracts/toolContracts.js +5 -1
- package/dist/contracts/toolEffects.js +3 -0
- package/dist/contracts/toolOutputSchemaGroups.js +2 -0
- package/dist/contracts/toolOutputSchemaPrimitives.js +43 -0
- package/dist/doctorRuntime.js +41 -0
- package/dist/domain/analysisErrorPresentation.js +5 -3
- package/dist/domain/analysisErrorProjection.js +7 -1
- package/dist/domain/analysisSnapshot.js +2 -0
- package/dist/domain/artifactGraph.js +2 -0
- package/dist/domain/binaryTarget.js +3 -6
- package/dist/domain/changedBehavior.js +5 -4
- package/dist/domain/completionLedgerGeneration.js +190 -0
- package/dist/domain/errors.js +4 -2
- package/dist/domain/evidence.js +2 -0
- 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 +100 -0
- package/dist/domain/processCaptureReactiveValidation.js +175 -0
- package/dist/domain/processCaptureValidation.js +103 -12
- package/dist/domain/processComparison.js +78 -25
- package/dist/domain/processObservation.js +162 -0
- package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
- package/dist/domain/processReactiveMatching.js +210 -0
- package/dist/domain/processReactiveRuntime.js +120 -0
- package/dist/domain/processReactiveScenario.js +361 -0
- package/dist/domain/processReactiveScenarioPreflight.js +120 -0
- package/dist/domain/processReactiveTransition.js +161 -0
- package/dist/domain/processScenario.js +33 -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 +18 -2
- package/dist/domain/replayMachineRun.js +216 -0
- package/dist/domain/replayMachineRuntime.js +88 -9
- package/dist/domain/staticRuntimeCorrelation.js +4 -2
- package/dist/domain/zipPackageFormat.js +20 -0
- package/dist/evaluation/CodexAgentEval.js +24 -0
- package/dist/generatedPackageMetadata.js +1 -1
- 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/main/transport.js +2 -0
- package/dist/mcpDoctor.js +310 -0
- package/dist/native/NativeMacOSProvider.js +1 -0
- package/dist/process/ProcessOwnership.js +200 -53
- package/dist/process/ProviderRunLineage.js +21 -0
- package/dist/server/createServer.js +4 -1
- package/dist/server/javascriptApplicationResult.js +22 -1
- 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/registerEvidenceResources.js +5 -1
- package/dist/server/registerProcessComparisonTool.js +17 -7
- package/dist/server/registerReplayMachineTool.js +20 -0
- package/dist/server/registerSessionTools.js +12 -7
- package/dist/server/sessionAvailabilityPolicy.js +2 -1
- package/dist/serverIdentity.js +19 -0
- package/package.json +62 -30
- 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 +4 -3
- package/skills/reverse-engineer-anything/references/javascript-applications.md +10 -0
- package/skills/reverse-engineer-anything/references/native-and-artifacts.md +2 -2
|
@@ -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) {
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { normalizeProcessElapsedTime, normalizeProcessText, } from "./ProcessNormalization.js";
|
|
2
|
+
/** Create one monotonic event journal for a process capture. */
|
|
3
|
+
export const createProcessCaptureJournal = () => {
|
|
4
|
+
const entries = [];
|
|
5
|
+
const listeners = new Set();
|
|
6
|
+
const notifications = [];
|
|
7
|
+
let delivering = false;
|
|
8
|
+
const deliverNotifications = () => {
|
|
9
|
+
if (delivering)
|
|
10
|
+
return;
|
|
11
|
+
delivering = true;
|
|
12
|
+
const failures = [];
|
|
13
|
+
try {
|
|
14
|
+
for (;;) {
|
|
15
|
+
const notification = notifications.shift();
|
|
16
|
+
if (notification === undefined)
|
|
17
|
+
break;
|
|
18
|
+
for (const listener of Array.from(listeners))
|
|
19
|
+
try {
|
|
20
|
+
listener(notification);
|
|
21
|
+
}
|
|
22
|
+
catch (cause) {
|
|
23
|
+
failures.push(cause);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
delivering = false;
|
|
29
|
+
}
|
|
30
|
+
if (failures.length > 0)
|
|
31
|
+
throw new AggregateError(failures, "process capture journal subscriber failed");
|
|
32
|
+
};
|
|
33
|
+
const record = (collection, index) => {
|
|
34
|
+
const entry = { capture_order: entries.length, collection, index };
|
|
35
|
+
entries.push(entry);
|
|
36
|
+
notifications.push(entry);
|
|
37
|
+
deliverNotifications();
|
|
38
|
+
return entry;
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
entries,
|
|
42
|
+
record,
|
|
43
|
+
recordEvent: (collection, index) => void record(collection, index),
|
|
44
|
+
subscribe: (listener) => {
|
|
45
|
+
listeners.add(listener);
|
|
46
|
+
return () => listeners.delete(listener);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
/** Schedule scenario interactions and record each observed dispatch atomically. */
|
|
51
|
+
export const scheduleScenarioInteractions = (options) => {
|
|
52
|
+
const { scenario, getTerminal, timers, interactions, renderer, started } = options;
|
|
53
|
+
for (const [eventIndex, event] of scenario.events.entries()) {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
options.dispatchedEventIndexes.add(eventIndex);
|
|
56
|
+
const terminal = getTerminal();
|
|
57
|
+
const dispatchedAt = Math.max(0, Date.now() - started);
|
|
58
|
+
let outcome = "dispatched";
|
|
59
|
+
if (terminal === undefined)
|
|
60
|
+
outcome = "target_exited";
|
|
61
|
+
else {
|
|
62
|
+
try {
|
|
63
|
+
if (event.type === "input")
|
|
64
|
+
terminal.write(event.data);
|
|
65
|
+
else if (event.type === "resize") {
|
|
66
|
+
terminal.resize(event.columns, event.rows);
|
|
67
|
+
renderer.resize(event.columns, event.rows, normalizeProcessElapsedTime(dispatchedAt, scenario.normalization.time_bucket_ms));
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
terminal.kill(event.signal);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
outcome = "failed";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const sequence = interactions.length;
|
|
77
|
+
interactions.push({
|
|
78
|
+
sequence,
|
|
79
|
+
scheduled_at_ms: event.at_ms,
|
|
80
|
+
dispatched_at_ms: dispatchedAt,
|
|
81
|
+
type: event.type,
|
|
82
|
+
data: event.type === "input"
|
|
83
|
+
? event.sensitive
|
|
84
|
+
? `<redacted-input:${String(Buffer.byteLength(event.data))}-bytes>`
|
|
85
|
+
: normalizeProcessText(event.data, scenario, "<no-temporary-root>", -1)
|
|
86
|
+
: event.type === "resize"
|
|
87
|
+
? `${String(event.columns)}x${String(event.rows)}`
|
|
88
|
+
: event.signal,
|
|
89
|
+
outcome,
|
|
90
|
+
});
|
|
91
|
+
options.recordEvent("interaction_events", sequence);
|
|
92
|
+
}, event.at_ms);
|
|
93
|
+
timers.add(timer);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
@@ -10,7 +10,7 @@ import { cleanupOwnedProcessGroup, observeOwnedProcessGroup, } from "../process/
|
|
|
10
10
|
import { classifyFilesystemEffects, ProcessCheckpoints, } from "./ProcessCheckpoints.js";
|
|
11
11
|
import { ProcessCaptureError } from "./ProcessCaptureError.js";
|
|
12
12
|
import { assertNotCancelled, assertRealPathAuthority, } from "./ProcessCaptureAuthority.js";
|
|
13
|
-
import { normalizeProcessSamples, normalizeProcessText, redactProtocolEvents, } from "./ProcessNormalization.js";
|
|
13
|
+
import { normalizeProcessElapsedTime, normalizeProcessSamples, normalizeProcessText, redactProtocolEvents, } from "./ProcessNormalization.js";
|
|
14
14
|
import { PROCESS_PROVIDER } from "./ProcessEvidence.js";
|
|
15
15
|
import { TerminalRenderer } from "./TerminalRenderer.js";
|
|
16
16
|
export const buildCaptureResult = (options) => ({
|
|
@@ -18,9 +18,7 @@ export const buildCaptureResult = (options) => ({
|
|
|
18
18
|
manifest: options.manifest,
|
|
19
19
|
normalization: options.scenario.normalization,
|
|
20
20
|
frames: options.frames,
|
|
21
|
-
rendered_frames:
|
|
22
|
-
.sort((left, right) => left.at_ms - right.at_ms || left.sequence - right.sequence)
|
|
23
|
-
.map((frame, sequence) => ({ ...frame, sequence })),
|
|
21
|
+
rendered_frames: options.renderedFrames,
|
|
24
22
|
interaction_events: options.interactions,
|
|
25
23
|
exit: {
|
|
26
24
|
code: options.exit.reason === "exited" && options.exit.exitCode >= 0
|
|
@@ -34,6 +32,9 @@ export const buildCaptureResult = (options) => ({
|
|
|
34
32
|
filesystem_checkpoints: options.checkpoints,
|
|
35
33
|
shim_events: options.shimEvents,
|
|
36
34
|
protocol_events: redactProtocolEvents(options.replay.events, options.scenario),
|
|
35
|
+
replay_transitions: options.replay.transitions,
|
|
36
|
+
reactive_run: options.reactiveRun,
|
|
37
|
+
event_journal: options.eventJournal,
|
|
37
38
|
files_before: options.before.files,
|
|
38
39
|
files_after: options.after.files,
|
|
39
40
|
filesystem_effects: classifyFilesystemEffects(options.before.files, options.after.files),
|
|
@@ -91,9 +92,11 @@ export const createRunManifest = async (scenario, startedAt, completedAt) => {
|
|
|
91
92
|
replay_plan_sha256: digestProcessCommitment(scenario.replay),
|
|
92
93
|
};
|
|
93
94
|
};
|
|
94
|
-
export const observeSettlement = async (runId, processGroupIds, settleMs) => {
|
|
95
|
-
if (process.platform === "win32")
|
|
95
|
+
export const observeSettlement = async (runId, processGroupIds, settleMs, recordEvent = () => undefined) => {
|
|
96
|
+
if (process.platform === "win32") {
|
|
97
|
+
recordEvent("lifecycle", 1);
|
|
96
98
|
return { state: "unverifiable", elapsed_ms: 0 };
|
|
99
|
+
}
|
|
97
100
|
const started = Date.now();
|
|
98
101
|
let consecutiveEmpty = 0;
|
|
99
102
|
let deadlineReached = false;
|
|
@@ -103,12 +106,16 @@ export const observeSettlement = async (runId, processGroupIds, settleMs) => {
|
|
|
103
106
|
leaderPid: processGroupId,
|
|
104
107
|
processGroupId,
|
|
105
108
|
})));
|
|
106
|
-
if (observations.some(({ state }) => state === "unverifiable"))
|
|
109
|
+
if (observations.some(({ state }) => state === "unverifiable")) {
|
|
110
|
+
recordEvent("lifecycle", 1);
|
|
107
111
|
return { state: "unverifiable", elapsed_ms: Date.now() - started };
|
|
112
|
+
}
|
|
108
113
|
if (observations.every(({ state }) => state === "empty")) {
|
|
109
114
|
consecutiveEmpty += 1;
|
|
110
|
-
if (consecutiveEmpty >= 2)
|
|
115
|
+
if (consecutiveEmpty >= 2) {
|
|
116
|
+
recordEvent("lifecycle", 1);
|
|
111
117
|
return { state: "quiesced", elapsed_ms: Date.now() - started };
|
|
118
|
+
}
|
|
112
119
|
}
|
|
113
120
|
else
|
|
114
121
|
consecutiveEmpty = 0;
|
|
@@ -117,19 +124,22 @@ export const observeSettlement = async (runId, processGroupIds, settleMs) => {
|
|
|
117
124
|
break;
|
|
118
125
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
119
126
|
}
|
|
127
|
+
recordEvent("lifecycle", 1);
|
|
120
128
|
return { state: "alive_at_deadline", elapsed_ms: Date.now() - started };
|
|
121
129
|
};
|
|
122
|
-
export const awaitTerminalExit = async ({ terminal, scenario, started, lastOutput, signal, timers, interactions, dispatchedEventIndexes, }) => new Promise((resolveExit) => {
|
|
130
|
+
export const awaitTerminalExit = async ({ terminal, scenario, started, lastOutput, signal, timers, interactions, dispatchedEventIndexes, recordEvent, }) => new Promise((resolveExit) => {
|
|
123
131
|
// The kill caused by a deadline is observed later as an ordinary PTY exit.
|
|
124
132
|
// Keep the initiating lifecycle reason so comparisons distinguish a target
|
|
125
133
|
// exit from harness-owned timeout, idle-timeout, and cancellation cleanup.
|
|
126
134
|
let reason = "exited";
|
|
127
135
|
terminal.onExit((exit) => {
|
|
136
|
+
recordEvent("lifecycle", 0);
|
|
128
137
|
for (const [eventIndex, event] of scenario.events.entries()) {
|
|
129
138
|
if (dispatchedEventIndexes.has(eventIndex))
|
|
130
139
|
continue;
|
|
140
|
+
const sequence = interactions.length;
|
|
131
141
|
interactions.push({
|
|
132
|
-
sequence
|
|
142
|
+
sequence,
|
|
133
143
|
scheduled_at_ms: event.at_ms,
|
|
134
144
|
dispatched_at_ms: Math.max(0, Date.now() - started),
|
|
135
145
|
type: event.type,
|
|
@@ -140,6 +150,7 @@ export const awaitTerminalExit = async ({ terminal, scenario, started, lastOutpu
|
|
|
140
150
|
: event.signal,
|
|
141
151
|
outcome: "target_exited",
|
|
142
152
|
});
|
|
153
|
+
recordEvent("interaction_events", sequence);
|
|
143
154
|
}
|
|
144
155
|
for (const timer of timers) {
|
|
145
156
|
clearTimeout(timer);
|
|
@@ -223,14 +234,15 @@ export const captureTerminalFrames = (options) => {
|
|
|
223
234
|
return;
|
|
224
235
|
}
|
|
225
236
|
outputBytes += bytes;
|
|
226
|
-
const atMs =
|
|
227
|
-
options.scenario.normalization.time_bucket_ms) * options.scenario.normalization.time_bucket_ms;
|
|
237
|
+
const atMs = normalizeProcessElapsedTime(Date.now() - options.started, options.scenario.normalization.time_bucket_ms);
|
|
228
238
|
const normalized = normalizeProcessText(data, options.scenario, options.temporaryRoot, options.terminal.pid);
|
|
239
|
+
const sequence = options.frames.length;
|
|
229
240
|
options.frames.push({
|
|
230
|
-
sequence
|
|
241
|
+
sequence,
|
|
231
242
|
at_ms: atMs,
|
|
232
243
|
data: normalized,
|
|
233
244
|
});
|
|
245
|
+
options.recordEvent("frames", sequence);
|
|
234
246
|
options.renderer.write(data, atMs);
|
|
235
247
|
options.checkpoints.observeTerminal(normalized);
|
|
236
248
|
});
|
|
@@ -22,17 +22,19 @@ export const classifyFilesystemEffects = (before, after) => {
|
|
|
22
22
|
export class ProcessCheckpoints {
|
|
23
23
|
scenario;
|
|
24
24
|
started;
|
|
25
|
-
signal;
|
|
26
25
|
#captures = [];
|
|
27
26
|
#captured = new Set();
|
|
28
27
|
#terminalCounts = new Map();
|
|
29
28
|
#terminalTails = new Map();
|
|
30
29
|
#timers = new Set();
|
|
30
|
+
#signal;
|
|
31
|
+
#recordEvent;
|
|
31
32
|
#pending = Promise.resolve();
|
|
32
|
-
constructor(scenario, started, before, signal) {
|
|
33
|
+
constructor(scenario, started, before, options = { signal: undefined }) {
|
|
33
34
|
this.scenario = scenario;
|
|
34
35
|
this.started = started;
|
|
35
|
-
this
|
|
36
|
+
this.#signal = options.signal;
|
|
37
|
+
this.#recordEvent = options.recordEvent ?? (() => undefined);
|
|
36
38
|
this.#captures.push({
|
|
37
39
|
name: "before",
|
|
38
40
|
at_ms: 0,
|
|
@@ -40,6 +42,7 @@ export class ProcessCheckpoints {
|
|
|
40
42
|
effects: [],
|
|
41
43
|
truncated: before.truncated,
|
|
42
44
|
});
|
|
45
|
+
this.#recordEvent("filesystem_checkpoints", 0);
|
|
43
46
|
this.#captured.add("before");
|
|
44
47
|
for (const checkpoint of scenario.checkpoints) {
|
|
45
48
|
if (checkpoint.trigger.type !== "time")
|
|
@@ -74,6 +77,14 @@ export class ProcessCheckpoints {
|
|
|
74
77
|
}
|
|
75
78
|
/** Queue one idempotent named snapshot. */
|
|
76
79
|
capture(name) {
|
|
80
|
+
this.#scheduleCapture(name, this.#signal);
|
|
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
|
+
}
|
|
87
|
+
#scheduleCapture(name, signal) {
|
|
77
88
|
if (this.#captured.has(name))
|
|
78
89
|
return;
|
|
79
90
|
this.#captured.add(name);
|
|
@@ -81,8 +92,9 @@ export class ProcessCheckpoints {
|
|
|
81
92
|
// together. Each effect set is intentionally relative to the immediately
|
|
82
93
|
// preceding checkpoint, which preserves transient create/delete behavior.
|
|
83
94
|
this.#pending = this.#pending.then(async () => {
|
|
84
|
-
const snapshot = await snapshotRoots(this.scenario,
|
|
95
|
+
const snapshot = await snapshotRoots(this.scenario, signal);
|
|
85
96
|
const previous = this.#captures.at(-1)?.files ?? [];
|
|
97
|
+
const index = this.#captures.length;
|
|
86
98
|
this.#captures.push({
|
|
87
99
|
name,
|
|
88
100
|
at_ms: Math.max(0, Date.now() - this.started),
|
|
@@ -90,14 +102,32 @@ export class ProcessCheckpoints {
|
|
|
90
102
|
effects: classifyFilesystemEffects(previous, snapshot.files),
|
|
91
103
|
truncated: snapshot.truncated,
|
|
92
104
|
});
|
|
105
|
+
this.#recordEvent("filesystem_checkpoints", index);
|
|
93
106
|
});
|
|
94
107
|
}
|
|
108
|
+
/** Queue one named snapshot and return its exact captured record. */
|
|
109
|
+
async captureAndRead(name, signal) {
|
|
110
|
+
const captureSignal = signal === undefined
|
|
111
|
+
? this.#signal
|
|
112
|
+
: this.#signal === undefined
|
|
113
|
+
? signal
|
|
114
|
+
: AbortSignal.any([this.#signal, signal]);
|
|
115
|
+
this.#scheduleCapture(name, captureSignal);
|
|
116
|
+
const scheduled = this.#pending;
|
|
117
|
+
await scheduled;
|
|
118
|
+
const index = this.#captures.findIndex(({ name: capturedName }) => capturedName === name);
|
|
119
|
+
const checkpoint = this.#captures[index];
|
|
120
|
+
if (index < 0 || checkpoint === undefined)
|
|
121
|
+
throw new Error(`filesystem checkpoint was not captured: ${name}`);
|
|
122
|
+
return { index, checkpoint: structuredClone(checkpoint) };
|
|
123
|
+
}
|
|
95
124
|
/** Finish pending snapshots and return them in observation order. */
|
|
96
125
|
async finish(after) {
|
|
97
126
|
for (const timer of this.#timers)
|
|
98
127
|
clearTimeout(timer);
|
|
99
128
|
await this.#pending;
|
|
100
129
|
const previous = this.#captures.at(-1)?.files ?? [];
|
|
130
|
+
const index = this.#captures.length;
|
|
101
131
|
this.#captures.push({
|
|
102
132
|
name: "after_settlement",
|
|
103
133
|
at_ms: Math.max(0, Date.now() - this.started),
|
|
@@ -105,6 +135,7 @@ export class ProcessCheckpoints {
|
|
|
105
135
|
effects: classifyFilesystemEffects(previous, after.files),
|
|
106
136
|
truncated: after.truncated,
|
|
107
137
|
});
|
|
138
|
+
this.#recordEvent("filesystem_checkpoints", index);
|
|
108
139
|
return structuredClone(this.#captures);
|
|
109
140
|
}
|
|
110
141
|
/** Cancel scheduled captures and await any snapshot already in progress. */
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { parseConfig } from "../config.js";
|
|
3
|
-
import { AnalysisError, AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "../domain/errors.js";
|
|
3
|
+
import { AnalysisError, AnalysisInputError, AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "../domain/errors.js";
|
|
4
4
|
import { createEvidence, parseEvidence } from "../domain/evidence.js";
|
|
5
5
|
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
6
|
+
import { projectInputIssues } from "../domain/inputIssueProjection.js";
|
|
7
|
+
import { processTraceSpecificationSchema } from "../domain/processTraceComparison.js";
|
|
6
8
|
import { compareProcessCaptures, LEGACY_PROCESS_CAPTURE_MESSAGE, parseProcessScenario, parseProcessCapture, } from "../domain/processCapture.js";
|
|
7
9
|
import { captureProcessScenario } from "./ProcessHarness.js";
|
|
8
10
|
import { processCapturePermissionRequest } from "./ProcessCapturePermission.js";
|
|
@@ -43,24 +45,37 @@ export const captureProcessScenarioFile = async (path) => {
|
|
|
43
45
|
}
|
|
44
46
|
};
|
|
45
47
|
/** Compare two capture Evidence files and emit derived comparison Evidence. */
|
|
46
|
-
export const compareProcessEvidenceFiles = async (leftPath, rightPath) => {
|
|
48
|
+
export const compareProcessEvidenceFiles = async (leftPath, rightPath, traceSpecPath) => {
|
|
47
49
|
try {
|
|
48
50
|
const left = parseCaptureEvidence(await readJson(leftPath));
|
|
49
51
|
const right = parseCaptureEvidence(await readJson(rightPath));
|
|
50
|
-
const
|
|
52
|
+
const traceSpecification = traceSpecPath === undefined
|
|
53
|
+
? undefined
|
|
54
|
+
: parseTraceSpecification(await readJson(traceSpecPath));
|
|
55
|
+
const comparison = traceSpecification === undefined
|
|
56
|
+
? compareProcessCaptures(left.capture, right.capture)
|
|
57
|
+
: compareProcessCaptures(left.capture, right.capture, {
|
|
58
|
+
traceSpecification,
|
|
59
|
+
});
|
|
51
60
|
return createEvidence(undefined, PROCESS_PROVIDER, {
|
|
52
|
-
predicateType:
|
|
61
|
+
predicateType: traceSpecification === undefined
|
|
62
|
+
? "rea.process-comparison/v3"
|
|
63
|
+
: "rea.process-comparison/v4",
|
|
53
64
|
operation: "compare_process_captures",
|
|
54
65
|
parameters: {
|
|
55
66
|
left_evidence_id: left.id,
|
|
56
67
|
right_evidence_id: right.id,
|
|
57
68
|
left_normalization: left.capture.normalization,
|
|
58
69
|
right_normalization: right.capture.normalization,
|
|
70
|
+
...(traceSpecification === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: { trace_spec: jsonValueSchema.parse(traceSpecification) }),
|
|
59
73
|
},
|
|
60
74
|
result: jsonValueSchema.parse(comparison),
|
|
61
75
|
confidence: "derived",
|
|
62
76
|
authority: "analyst-inference",
|
|
63
77
|
limitations: comparison.limitations,
|
|
78
|
+
locations: [...left.locations, ...right.locations],
|
|
64
79
|
evidenceLinks: [left.id, right.id],
|
|
65
80
|
});
|
|
66
81
|
}
|
|
@@ -88,12 +103,19 @@ const parseCaptureEvidence = (input) => {
|
|
|
88
103
|
return {
|
|
89
104
|
id: evidence.evidence_id,
|
|
90
105
|
capture: parseProcessCapture(evidence.normalized_result),
|
|
106
|
+
locations: evidence.locations,
|
|
91
107
|
};
|
|
92
108
|
}
|
|
93
109
|
catch {
|
|
94
110
|
throw invalidCaptureEvidence();
|
|
95
111
|
}
|
|
96
112
|
};
|
|
113
|
+
const parseTraceSpecification = (input) => {
|
|
114
|
+
const parsed = processTraceSpecificationSchema.safeParse(input);
|
|
115
|
+
if (parsed.success)
|
|
116
|
+
return parsed.data;
|
|
117
|
+
throw new AnalysisInputError("compare-process-captures", { cause: parsed.error }, projectInputIssues(parsed.error.issues, input));
|
|
118
|
+
};
|
|
97
119
|
const invalidCaptureEvidence = () => new ProcessCliFailure("invalid_input", "Capture evidence is malformed. Create new capture evidence, then try again.");
|
|
98
120
|
const readJson = async (path) => {
|
|
99
121
|
let bytes;
|