rea-agents 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +21 -2
  2. package/dist/application/ArtifactInventory.js +80 -44
  3. package/dist/application/CommandShimReplay.js +136 -0
  4. package/dist/application/EvidenceBundleFiles.js +9 -1
  5. package/dist/application/ProcessCaptureAuthority.js +25 -0
  6. package/dist/application/ProcessCaptureCapability.js +23 -0
  7. package/dist/application/ProcessCaptureError.js +5 -0
  8. package/dist/application/ProcessCaptureLifecycle.js +269 -0
  9. package/dist/application/ProcessCheckpoints.js +129 -0
  10. package/dist/application/ProcessCli.js +61 -0
  11. package/dist/application/ProcessEvidence.js +38 -0
  12. package/dist/application/ProcessHarness.js +252 -272
  13. package/dist/application/ProcessNormalization.js +10 -1
  14. package/dist/application/ProcessOwnership.js +39 -1
  15. package/dist/application/ProcessSampling.js +39 -23
  16. package/dist/application/Setup.js +51 -76
  17. package/dist/application/SetupSkill.js +44 -0
  18. package/dist/application/TerminalRenderer.js +92 -0
  19. package/dist/application/runtime.js +1 -1
  20. package/dist/artifacts/ArtifactProvider.js +18 -6
  21. package/dist/artifacts/ArtifactReader.js +3 -1
  22. package/dist/artifacts/AsarArtifactReader.js +8 -1
  23. package/dist/artifacts/DirectoryArtifactReader.js +1 -0
  24. package/dist/artifacts/MachOSliceArtifactReader.js +1 -0
  25. package/dist/artifacts/NativeDmgArtifactReader.js +151 -0
  26. package/dist/artifacts/ZipArtifactReader.js +1 -0
  27. package/dist/cli.js +29 -24
  28. package/dist/cliProcessCommands.js +19 -0
  29. package/dist/config.js +2 -0
  30. package/dist/contracts/artifactToolContracts.js +1 -0
  31. package/dist/contracts/investigationExamples.js +3 -3
  32. package/dist/contracts/processCaptureExample.js +50 -7
  33. package/dist/contracts/toolContracts.js +3 -2
  34. package/dist/domain/changedBehavior.js +2 -2
  35. package/dist/domain/errors.js +11 -3
  36. package/dist/domain/processCapture.js +99 -293
  37. package/dist/domain/processCaptureValidation.js +124 -0
  38. package/dist/domain/processComparison.js +176 -32
  39. package/dist/domain/processScenario.js +370 -0
  40. package/dist/domain/reconstructionVerification.js +3 -3
  41. package/dist/domain/staticRuntimeCorrelation.js +3 -2
  42. package/dist/identity.js +1 -0
  43. package/dist/server/registerProcessComparisonTool.js +28 -8
  44. package/dist/server/registerSessionTools.js +3 -25
  45. package/dist/server/sessionToolPolicies.js +1 -6
  46. package/package.json +5 -1
  47. package/scripts/prepare-node-pty.mjs +35 -0
  48. package/skills/rea-analysis/SKILL.md +19 -4
@@ -0,0 +1,269 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { authorizeProcessScenario, digestProcessCommitment, processComparisonContract, processScenarioCommitment, } from "../domain/processCapture.js";
7
+ import { PRODUCT_IDENTITY } from "../identity.js";
8
+ import { snapshotRoots } from "./FilesystemSnapshot.js";
9
+ import { cleanupOwnedProcessGroup, observeOwnedProcessGroup, } from "./ProcessOwnership.js";
10
+ import { classifyFilesystemEffects, ProcessCheckpoints, } from "./ProcessCheckpoints.js";
11
+ import { ProcessCaptureError } from "./ProcessCaptureError.js";
12
+ import { assertNotCancelled, assertRealPathAuthority, } from "./ProcessCaptureAuthority.js";
13
+ import { normalizeProcessSamples, normalizeProcessText, redactProtocolEvents, } from "./ProcessNormalization.js";
14
+ import { PROCESS_PROVIDER } from "./ProcessEvidence.js";
15
+ import { TerminalRenderer } from "./TerminalRenderer.js";
16
+ export const buildCaptureResult = (options) => ({
17
+ schema_version: 4,
18
+ manifest: options.manifest,
19
+ normalization: options.scenario.normalization,
20
+ frames: options.frames,
21
+ rendered_frames: [...options.renderedFrames]
22
+ .sort((left, right) => left.at_ms - right.at_ms || left.sequence - right.sequence)
23
+ .map((frame, sequence) => ({ ...frame, sequence })),
24
+ interaction_events: options.interactions,
25
+ exit: {
26
+ code: options.exit.reason === "exited" && options.exit.exitCode >= 0
27
+ ? options.exit.exitCode
28
+ : null,
29
+ signal: options.exit.signal ?? null,
30
+ reason: options.exit.reason,
31
+ },
32
+ settlement: options.settlement,
33
+ process_samples: normalizeProcessSamples(options.samples, options.scenario, options.rootPid),
34
+ filesystem_checkpoints: options.checkpoints,
35
+ shim_events: options.shimEvents,
36
+ protocol_events: redactProtocolEvents(options.replay.events, options.scenario),
37
+ files_before: options.before.files,
38
+ files_after: options.after.files,
39
+ filesystem_effects: classifyFilesystemEffects(options.before.files, options.after.files),
40
+ truncated: options.truncated,
41
+ limitations: [
42
+ "Process trees are sampled and may omit short-lived descendants.",
43
+ ...(options.samplingPartial
44
+ ? ["Process-tree sampling ended with an incomplete observation."]
45
+ : []),
46
+ "Filesystem observations are before/after snapshots, not syscall traces.",
47
+ "The harness does not enforce external network isolation.",
48
+ ],
49
+ residual_unknowns: [
50
+ {
51
+ scope: "process",
52
+ reason: "Process trees are sampled and may omit short-lived descendants.",
53
+ },
54
+ {
55
+ scope: "network",
56
+ reason: "External network isolation is not enforced by this adapter.",
57
+ },
58
+ ],
59
+ cleanup: {
60
+ owned_process_group: "verified",
61
+ temporary_root: "removed",
62
+ },
63
+ });
64
+ const hashFile = async (path) => {
65
+ const hash = createHash("sha256");
66
+ for await (const chunk of createReadStream(path))
67
+ hash.update(chunk);
68
+ return hash.digest("hex");
69
+ };
70
+ export const createRunManifest = async (scenario, startedAt, completedAt) => {
71
+ const executableSha256 = await hashFile(scenario.executable);
72
+ const scenarioCommitment = processScenarioCommitment(scenario, executableSha256);
73
+ const comparisonContract = processComparisonContract(scenario);
74
+ return {
75
+ rea_version: PRODUCT_IDENTITY.packageVersion,
76
+ provider_version: PROCESS_PROVIDER.version,
77
+ platform: process.platform,
78
+ architecture: process.arch,
79
+ pty_backend: "node-pty",
80
+ started_at: startedAt.toISOString(),
81
+ completed_at: completedAt.toISOString(),
82
+ scenario: scenarioCommitment,
83
+ comparison_contract: comparisonContract,
84
+ shim_plan: scenario.command_shims,
85
+ replay_plan: scenario.replay,
86
+ full_scenario_sha256: digestProcessCommitment(scenarioCommitment),
87
+ comparison_contract_sha256: digestProcessCommitment(comparisonContract),
88
+ executable_sha256: executableSha256,
89
+ normalization_sha256: digestProcessCommitment(scenario.normalization),
90
+ shim_plan_sha256: digestProcessCommitment(scenario.command_shims),
91
+ replay_plan_sha256: digestProcessCommitment(scenario.replay),
92
+ };
93
+ };
94
+ export const observeSettlement = async (runId, processGroupIds, settleMs) => {
95
+ if (process.platform === "win32")
96
+ return { state: "unverifiable", elapsed_ms: 0 };
97
+ const started = Date.now();
98
+ let consecutiveEmpty = 0;
99
+ let deadlineReached = false;
100
+ while (!deadlineReached) {
101
+ const observations = await Promise.all([...new Set(processGroupIds)].map((processGroupId) => observeOwnedProcessGroup({
102
+ runId,
103
+ leaderPid: processGroupId,
104
+ processGroupId,
105
+ })));
106
+ if (observations.some(({ state }) => state === "unverifiable"))
107
+ return { state: "unverifiable", elapsed_ms: Date.now() - started };
108
+ if (observations.every(({ state }) => state === "empty")) {
109
+ consecutiveEmpty += 1;
110
+ if (consecutiveEmpty >= 2)
111
+ return { state: "quiesced", elapsed_ms: Date.now() - started };
112
+ }
113
+ else
114
+ consecutiveEmpty = 0;
115
+ deadlineReached = Date.now() - started >= settleMs;
116
+ if (deadlineReached)
117
+ break;
118
+ await new Promise((resolve) => setTimeout(resolve, 50));
119
+ }
120
+ return { state: "alive_at_deadline", elapsed_ms: Date.now() - started };
121
+ };
122
+ export const awaitTerminalExit = async ({ terminal, scenario, started, lastOutput, signal, timers, interactions, dispatchedEventIndexes, }) => new Promise((resolveExit) => {
123
+ // The kill caused by a deadline is observed later as an ordinary PTY exit.
124
+ // Keep the initiating lifecycle reason so comparisons distinguish a target
125
+ // exit from harness-owned timeout, idle-timeout, and cancellation cleanup.
126
+ let reason = "exited";
127
+ terminal.onExit((exit) => {
128
+ for (const [eventIndex, event] of scenario.events.entries()) {
129
+ if (dispatchedEventIndexes.has(eventIndex))
130
+ continue;
131
+ interactions.push({
132
+ sequence: interactions.length,
133
+ scheduled_at_ms: event.at_ms,
134
+ dispatched_at_ms: Math.max(0, Date.now() - started),
135
+ type: event.type,
136
+ data: event.type === "input"
137
+ ? "<not-dispatched>"
138
+ : event.type === "resize"
139
+ ? `${String(event.columns)}x${String(event.rows)}`
140
+ : event.signal,
141
+ outcome: "target_exited",
142
+ });
143
+ }
144
+ for (const timer of timers) {
145
+ clearTimeout(timer);
146
+ clearInterval(timer);
147
+ }
148
+ timers.clear();
149
+ resolveExit({ ...exit, reason });
150
+ });
151
+ const timeout = setInterval(() => {
152
+ if (signal?.aborted === true) {
153
+ reason = "cancelled";
154
+ terminal.kill("SIGKILL");
155
+ }
156
+ else if (Date.now() - started >= scenario.timeout_ms) {
157
+ reason = "timeout";
158
+ terminal.kill("SIGKILL");
159
+ }
160
+ else if (Date.now() - lastOutput() >= scenario.idle_timeout_ms) {
161
+ reason = "idle_timeout";
162
+ terminal.kill("SIGKILL");
163
+ }
164
+ }, 20);
165
+ timers.add(timeout);
166
+ });
167
+ export const releaseProcessResources = async (options) => {
168
+ for (const timer of options.timers)
169
+ clearTimeout(timer);
170
+ let failure;
171
+ try {
172
+ await options.replay?.close();
173
+ }
174
+ catch {
175
+ failure = "loopback replay cleanup failed";
176
+ }
177
+ try {
178
+ await options.shimReplay?.close();
179
+ }
180
+ catch {
181
+ failure ??= "command shim replay cleanup failed";
182
+ }
183
+ try {
184
+ await options.renderer?.dispose();
185
+ }
186
+ catch {
187
+ failure ??= "terminal renderer cleanup failed";
188
+ }
189
+ try {
190
+ await options.checkpoints?.dispose();
191
+ }
192
+ catch {
193
+ failure ??= "filesystem checkpoint cleanup failed";
194
+ }
195
+ if (options.terminal !== undefined && process.platform !== "win32") {
196
+ const processGroupIds = new Set([
197
+ options.terminal.pid,
198
+ ...options.sampledProcessGroupIds,
199
+ ]);
200
+ for (const processGroupId of processGroupIds) {
201
+ const cleaned = await cleanupOwnedProcessGroup({
202
+ runId: options.runId,
203
+ leaderPid: processGroupId,
204
+ processGroupId,
205
+ });
206
+ if (!cleaned.cleaned)
207
+ failure ??= cleaned.reason;
208
+ }
209
+ }
210
+ try {
211
+ await rm(options.temporaryRoot, { recursive: true, force: true });
212
+ }
213
+ catch {
214
+ failure ??= "temporary process root cleanup failed";
215
+ }
216
+ return failure;
217
+ };
218
+ export const captureTerminalFrames = (options) => {
219
+ let outputBytes = 0;
220
+ let truncated = false;
221
+ options.terminal.onData((data) => {
222
+ options.onFirstOutput();
223
+ options.onOutput();
224
+ const bytes = Buffer.byteLength(data);
225
+ if (options.frames.length >= options.scenario.limits.frames ||
226
+ outputBytes + bytes > options.scenario.limits.output_bytes) {
227
+ truncated = true;
228
+ return;
229
+ }
230
+ outputBytes += bytes;
231
+ const atMs = Math.floor((Date.now() - options.started) /
232
+ options.scenario.normalization.time_bucket_ms) * options.scenario.normalization.time_bucket_ms;
233
+ const normalized = normalizeProcessText(data, options.scenario, options.temporaryRoot, options.terminal.pid);
234
+ options.frames.push({
235
+ sequence: options.frames.length,
236
+ at_ms: atMs,
237
+ data: normalized,
238
+ });
239
+ options.renderer.write(data, atMs);
240
+ options.checkpoints.observeTerminal(normalized);
241
+ });
242
+ return () => truncated;
243
+ };
244
+ export const resolveProcessResult = (capture, executionFailure, cleanupFailure) => {
245
+ if (executionFailure instanceof ProcessCaptureError)
246
+ throw executionFailure;
247
+ if (executionFailure !== undefined)
248
+ throw new ProcessCaptureError("process capture failed", {
249
+ cause: executionFailure,
250
+ });
251
+ if (cleanupFailure !== undefined)
252
+ throw new ProcessCaptureError(cleanupFailure);
253
+ if (capture === undefined)
254
+ throw new ProcessCaptureError("process capture produced no result");
255
+ return capture;
256
+ };
257
+ export const prepareProcessCapture = async (scenario, policy, signal) => {
258
+ const decision = authorizeProcessScenario(scenario, policy);
259
+ if (!decision.allowed)
260
+ throw new ProcessCaptureError(decision.reason);
261
+ await assertRealPathAuthority(scenario, policy);
262
+ assertNotCancelled(signal);
263
+ const before = await snapshotRoots(scenario, signal);
264
+ const temporaryRoot = await mkdtemp(join(tmpdir(), "rea-process-"));
265
+ const runId = randomUUID();
266
+ const home = join(temporaryRoot, "home");
267
+ await import("node:fs/promises").then(({ mkdir }) => mkdir(home));
268
+ return { temporaryRoot, runId, home, before };
269
+ };
@@ -0,0 +1,129 @@
1
+ import { snapshotRoots } from "./FilesystemSnapshot.js";
2
+ /** Classify path-stable filesystem effects between two bounded states. */
3
+ export const classifyFilesystemEffects = (before, after) => {
4
+ const beforeByPath = new Map(before.map((file) => [file.path, file]));
5
+ const afterByPath = new Map(after.map((file) => [file.path, file]));
6
+ return [...new Set([...beforeByPath.keys(), ...afterByPath.keys()])]
7
+ .sort()
8
+ .map((path) => {
9
+ const beforeFile = beforeByPath.get(path) ?? null;
10
+ const afterFile = afterByPath.get(path) ?? null;
11
+ const status = beforeFile === null
12
+ ? "created"
13
+ : afterFile === null
14
+ ? "deleted"
15
+ : JSON.stringify(beforeFile) === JSON.stringify(afterFile)
16
+ ? "unchanged"
17
+ : "modified";
18
+ return { path, status, before: beforeFile, after: afterFile };
19
+ });
20
+ };
21
+ /** Coordinates named lifecycle filesystem observations without overlapping scans. */
22
+ export class ProcessCheckpoints {
23
+ scenario;
24
+ started;
25
+ signal;
26
+ #captures = [];
27
+ #captured = new Set();
28
+ #terminalCounts = new Map();
29
+ #terminalTails = new Map();
30
+ #timers = new Set();
31
+ #pending = Promise.resolve();
32
+ constructor(scenario, started, before, signal) {
33
+ this.scenario = scenario;
34
+ this.started = started;
35
+ this.signal = signal;
36
+ this.#captures.push({
37
+ name: "before",
38
+ at_ms: 0,
39
+ files: before.files,
40
+ effects: [],
41
+ truncated: before.truncated,
42
+ });
43
+ this.#captured.add("before");
44
+ for (const checkpoint of scenario.checkpoints) {
45
+ if (checkpoint.trigger.type !== "time")
46
+ continue;
47
+ const timer = setTimeout(() => this.capture(checkpoint.name), checkpoint.trigger.at_ms);
48
+ this.#timers.add(timer);
49
+ }
50
+ }
51
+ /** Observe normalized terminal text and fire matching literal checkpoints once. */
52
+ observeTerminal(data) {
53
+ for (const checkpoint of this.scenario.checkpoints) {
54
+ const trigger = checkpoint.trigger;
55
+ if (trigger.type !== "terminal_literal" ||
56
+ this.#captured.has(checkpoint.name))
57
+ continue;
58
+ const combined = `${this.#terminalTails.get(checkpoint.name) ?? ""}${data}`;
59
+ const count = (this.#terminalCounts.get(checkpoint.name) ?? 0) +
60
+ countOccurrences(combined, trigger.value);
61
+ this.#terminalCounts.set(checkpoint.name, count);
62
+ this.#terminalTails.set(checkpoint.name, trigger.value.length === 1
63
+ ? ""
64
+ : combined.slice(-(trigger.value.length - 1)));
65
+ if (count >= trigger.occurrence)
66
+ this.capture(checkpoint.name);
67
+ }
68
+ }
69
+ /** Capture every checkpoint assigned to the specified lifecycle trigger. */
70
+ trigger(type) {
71
+ for (const checkpoint of this.scenario.checkpoints)
72
+ if (checkpoint.trigger.type === type)
73
+ this.capture(checkpoint.name);
74
+ }
75
+ /** Queue one idempotent named snapshot. */
76
+ capture(name) {
77
+ if (this.#captured.has(name))
78
+ return;
79
+ this.#captured.add(name);
80
+ // Serialize scans even when time, terminal, and lifecycle triggers fire
81
+ // together. Each effect set is intentionally relative to the immediately
82
+ // preceding checkpoint, which preserves transient create/delete behavior.
83
+ this.#pending = this.#pending.then(async () => {
84
+ const snapshot = await snapshotRoots(this.scenario, this.signal);
85
+ const previous = this.#captures.at(-1)?.files ?? [];
86
+ this.#captures.push({
87
+ name,
88
+ at_ms: Math.max(0, Date.now() - this.started),
89
+ files: snapshot.files,
90
+ effects: classifyFilesystemEffects(previous, snapshot.files),
91
+ truncated: snapshot.truncated,
92
+ });
93
+ });
94
+ }
95
+ /** Finish pending snapshots and return them in observation order. */
96
+ async finish(after) {
97
+ for (const timer of this.#timers)
98
+ clearTimeout(timer);
99
+ await this.#pending;
100
+ const previous = this.#captures.at(-1)?.files ?? [];
101
+ this.#captures.push({
102
+ name: "after_settlement",
103
+ at_ms: Math.max(0, Date.now() - this.started),
104
+ files: after.files,
105
+ effects: classifyFilesystemEffects(previous, after.files),
106
+ truncated: after.truncated,
107
+ });
108
+ return this.#captures;
109
+ }
110
+ /** Cancel scheduled captures and await any snapshot already in progress. */
111
+ async dispose() {
112
+ for (const timer of this.#timers)
113
+ clearTimeout(timer);
114
+ this.#timers.clear();
115
+ await this.#pending;
116
+ }
117
+ }
118
+ const countOccurrences = (text, value) => {
119
+ let count = 0;
120
+ let offset = 0;
121
+ while (offset <= text.length - value.length) {
122
+ const found = text.indexOf(value, offset);
123
+ if (found < 0)
124
+ break;
125
+ count += 1;
126
+ offset = found + value.length;
127
+ }
128
+ return count;
129
+ };
@@ -0,0 +1,61 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parseConfig } from "../config.js";
3
+ import { createEvidence, parseEvidence } from "../domain/evidence.js";
4
+ import { jsonValueSchema } from "../domain/jsonValue.js";
5
+ import { compareProcessCaptures, LEGACY_PROCESS_CAPTURE_MESSAGE, parseProcessScenario, parseProcessCapture, } from "../domain/processCapture.js";
6
+ import { captureProcessScenario } from "./ProcessHarness.js";
7
+ import { PROCESS_PROVIDER, createProcessCaptureEvidence, } from "./ProcessEvidence.js";
8
+ const MAX_INPUT_BYTES = 64 * 1024 * 1024;
9
+ /** Capture one JSON scenario through the same policy and evidence contract as MCP. */
10
+ export const captureProcessScenarioFile = async (path) => {
11
+ const scenario = parseProcessScenario(await readJson(path));
12
+ const config = parseConfig(process.env);
13
+ if (!config.ok)
14
+ throw config.error;
15
+ const captured = await captureProcessScenario(scenario, config.value.processExecutionPolicy);
16
+ if (!captured.ok)
17
+ throw captured.error;
18
+ return createProcessCaptureEvidence(scenario, captured.value);
19
+ };
20
+ /** Compare two capture Evidence files and emit derived comparison Evidence. */
21
+ export const compareProcessEvidenceFiles = async (leftPath, rightPath) => {
22
+ const left = parseCaptureEvidence(await readJson(leftPath));
23
+ const right = parseCaptureEvidence(await readJson(rightPath));
24
+ const comparison = compareProcessCaptures(left.capture, right.capture);
25
+ return createEvidence(undefined, PROCESS_PROVIDER, {
26
+ predicateType: "rea.process-comparison/v3",
27
+ operation: "compare_process_captures",
28
+ parameters: {
29
+ left_evidence_id: left.id,
30
+ right_evidence_id: right.id,
31
+ left_normalization: left.capture.normalization,
32
+ right_normalization: right.capture.normalization,
33
+ },
34
+ result: jsonValueSchema.parse(comparison),
35
+ confidence: "derived",
36
+ authority: "analyst-inference",
37
+ limitations: comparison.limitations,
38
+ evidenceLinks: [left.id, right.id],
39
+ });
40
+ };
41
+ const parseCaptureEvidence = (input) => {
42
+ const evidence = parseEvidence(input);
43
+ if (evidence.operation !== "capture_process_scenario" ||
44
+ evidence.predicate_type !== "rea.process-capture/v4" ||
45
+ evidence.provider.id !== PROCESS_PROVIDER.id ||
46
+ evidence.provider.version !== PROCESS_PROVIDER.version) {
47
+ if (evidence.predicate_type === "rea.process-capture/v3")
48
+ throw new TypeError(LEGACY_PROCESS_CAPTURE_MESSAGE);
49
+ throw new TypeError("Expected REA Process Capture v4 Evidence");
50
+ }
51
+ return {
52
+ id: evidence.evidence_id,
53
+ capture: parseProcessCapture(evidence.normalized_result),
54
+ };
55
+ };
56
+ const readJson = async (path) => {
57
+ const bytes = await readFile(path);
58
+ if (bytes.length > MAX_INPUT_BYTES)
59
+ throw new TypeError("Process JSON input exceeds byte limit");
60
+ return JSON.parse(bytes.toString("utf8"));
61
+ };
@@ -0,0 +1,38 @@
1
+ import { createEvidence } from "../domain/evidence.js";
2
+ import { jsonValueSchema } from "../domain/jsonValue.js";
3
+ import { parseProcessCapture } from "../domain/processCapture.js";
4
+ /** Provider identity for controlled Process Capture v4 evidence. */
5
+ export const PROCESS_PROVIDER = {
6
+ id: "rea-process",
7
+ name: "REA deterministic process harness",
8
+ version: "3",
9
+ };
10
+ /** Project one process scenario into secret-free Evidence parameters. */
11
+ const processEvidenceParameters = (scenario) => ({
12
+ executable_name: scenario.executable.split("/").at(-1) ?? scenario.executable,
13
+ argument_count: scenario.arguments.length,
14
+ event_count: scenario.events.length,
15
+ filesystem_root_count: scenario.filesystem_roots.length,
16
+ checkpoint_count: scenario.checkpoints.length,
17
+ command_shim_count: scenario.command_shims.length,
18
+ normalization: scenario.normalization,
19
+ });
20
+ /** Create one canonical observed Process Capture v4 Evidence record. */
21
+ export const createProcessCaptureEvidence = (scenario, capture) => {
22
+ const validatedCapture = parseProcessCapture(capture);
23
+ return createEvidence(undefined, PROCESS_PROVIDER, {
24
+ predicateType: "rea.process-capture/v4",
25
+ operation: "capture_process_scenario",
26
+ parameters: processEvidenceParameters(scenario),
27
+ result: jsonValueSchema.parse(validatedCapture),
28
+ confidence: "observed",
29
+ authority: "controlled-replay",
30
+ environment: {
31
+ id: `${process.platform}-${process.arch}`,
32
+ platform: process.platform,
33
+ architecture: process.arch,
34
+ isolation: "process",
35
+ },
36
+ limitations: validatedCapture.limitations,
37
+ });
38
+ };