rea-agents 1.0.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 (52) hide show
  1. package/README.md +49 -18
  2. package/dist/application/ArtifactInventory.js +80 -44
  3. package/dist/application/CommandShimReplay.js +136 -0
  4. package/dist/application/Doctor.js +4 -4
  5. package/dist/application/EvidenceBundleFiles.js +13 -4
  6. package/dist/application/MacHopper.js +175 -0
  7. package/dist/application/ProcessCaptureAuthority.js +25 -0
  8. package/dist/application/ProcessCaptureCapability.js +23 -0
  9. package/dist/application/ProcessCaptureError.js +5 -0
  10. package/dist/application/ProcessCaptureLifecycle.js +269 -0
  11. package/dist/application/ProcessCheckpoints.js +129 -0
  12. package/dist/application/ProcessCli.js +61 -0
  13. package/dist/application/ProcessEvidence.js +38 -0
  14. package/dist/application/ProcessHarness.js +252 -272
  15. package/dist/application/ProcessNormalization.js +10 -1
  16. package/dist/application/ProcessOwnership.js +39 -1
  17. package/dist/application/ProcessSampling.js +39 -23
  18. package/dist/application/Setup.js +104 -100
  19. package/dist/application/SetupSkill.js +44 -0
  20. package/dist/application/TerminalRenderer.js +92 -0
  21. package/dist/application/runtime.js +1 -1
  22. package/dist/artifacts/ArtifactProvider.js +18 -6
  23. package/dist/artifacts/ArtifactReader.js +3 -1
  24. package/dist/artifacts/AsarArtifactReader.js +8 -1
  25. package/dist/artifacts/DirectoryArtifactReader.js +1 -0
  26. package/dist/artifacts/MachOSliceArtifactReader.js +1 -0
  27. package/dist/artifacts/NativeDmgArtifactReader.js +151 -0
  28. package/dist/artifacts/ZipArtifactReader.js +1 -0
  29. package/dist/cli.js +60 -26
  30. package/dist/cliProcessCommands.js +19 -0
  31. package/dist/config.js +2 -0
  32. package/dist/contracts/artifactToolContracts.js +1 -0
  33. package/dist/contracts/investigationExamples.js +3 -3
  34. package/dist/contracts/processCaptureExample.js +50 -7
  35. package/dist/contracts/toolContracts.js +3 -2
  36. package/dist/domain/changedBehavior.js +2 -2
  37. package/dist/domain/errors.js +11 -3
  38. package/dist/domain/processCapture.js +99 -293
  39. package/dist/domain/processCaptureValidation.js +124 -0
  40. package/dist/domain/processComparison.js +176 -32
  41. package/dist/domain/processScenario.js +370 -0
  42. package/dist/domain/reconstructionVerification.js +3 -3
  43. package/dist/domain/runtimeVersion.js +8 -0
  44. package/dist/domain/staticRuntimeCorrelation.js +3 -2
  45. package/dist/identity.js +1 -0
  46. package/dist/server/registerProcessComparisonTool.js +28 -8
  47. package/dist/server/registerSessionTools.js +3 -25
  48. package/dist/server/sessionToolPolicies.js +1 -6
  49. package/install.sh +79 -164
  50. package/package.json +7 -4
  51. package/scripts/prepare-node-pty.mjs +35 -0
  52. package/skills/rea-analysis/SKILL.md +19 -4
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { validateProcessCapture } from "./processCapture.js";
2
3
  /** Comparison classification that never equates incomplete evidence. */
3
4
  export const comparisonStatusSchema = z.enum([
4
5
  "unchanged",
@@ -13,19 +14,44 @@ export const processCaptureComparisonSchema = z
13
14
  .object({
14
15
  status: comparisonStatusSchema,
15
16
  terminal: comparisonStatusSchema,
17
+ interaction: comparisonStatusSchema,
16
18
  exit: comparisonStatusSchema,
17
19
  filesystem: comparisonStatusSchema,
18
20
  protocol: comparisonStatusSchema,
19
21
  process: comparisonStatusSchema,
22
+ shim: comparisonStatusSchema,
23
+ first_divergence: z.discriminatedUnion("status", [
24
+ z.object({ status: z.literal("none") }),
25
+ z.object({ status: z.literal("unknown"), reason: z.string() }),
26
+ z.object({
27
+ status: z.literal("found"),
28
+ dimension: z.enum([
29
+ "terminal",
30
+ "interaction",
31
+ "exit",
32
+ "filesystem",
33
+ "protocol",
34
+ "process",
35
+ "shim",
36
+ ]),
37
+ index: z.number().int().nonnegative(),
38
+ left_at_ms: z.number().int().nonnegative().nullable(),
39
+ right_at_ms: z.number().int().nonnegative().nullable(),
40
+ left: z.unknown().nullable(),
41
+ right: z.unknown().nullable(),
42
+ }),
43
+ ]),
20
44
  limitations: z.array(z.string()),
21
45
  })
22
46
  .superRefine((comparison, context) => {
23
47
  const dimensions = [
24
48
  comparison.terminal,
49
+ comparison.interaction,
25
50
  comparison.exit,
26
51
  comparison.filesystem,
27
52
  comparison.protocol,
28
53
  comparison.process,
54
+ comparison.shim,
29
55
  ];
30
56
  const expected = dimensions.includes("truncated")
31
57
  ? "truncated"
@@ -41,7 +67,28 @@ export const processCaptureComparisonSchema = z
41
67
  path: ["status"],
42
68
  });
43
69
  });
44
- const stableFiles = (capture) => JSON.stringify([...capture.files_after].sort((left, right) => left.path.localeCompare(right.path)));
70
+ const terminalObservations = (capture) => [
71
+ ...capture.frames.map((frame) => ({ kind: "raw", ...frame })),
72
+ ...capture.rendered_frames.map((frame) => ({
73
+ kind: "rendered",
74
+ ...frame,
75
+ })),
76
+ ].sort((left, right) => {
77
+ const time = left.at_ms - right.at_ms;
78
+ if (time !== 0)
79
+ return time;
80
+ if (left.kind !== right.kind)
81
+ return left.kind === "raw" ? -1 : 1;
82
+ return left.sequence - right.sequence;
83
+ });
84
+ const filesystemObservations = (capture) => [
85
+ ...capture.filesystem_checkpoints,
86
+ {
87
+ name: "final",
88
+ files: capture.files_after,
89
+ effects: capture.filesystem_effects,
90
+ },
91
+ ];
45
92
  const classifyCollection = (left, right) => {
46
93
  if (JSON.stringify(left) === JSON.stringify(right))
47
94
  return "unchanged";
@@ -57,44 +104,141 @@ const comparisonStatus = (dimensions) => {
57
104
  return "changed";
58
105
  return dimensions.includes("unknown") ? "unknown" : "unchanged";
59
106
  };
60
- /** Compare bounded captures without equating missing or incompatible evidence. */
61
- export const compareProcessCaptures = (left, right) => {
62
- if (left.truncated || right.truncated) {
107
+ const eventTime = (value) => {
108
+ if (typeof value === "object" &&
109
+ value !== null &&
110
+ "at_ms" in value &&
111
+ typeof value.at_ms === "number" &&
112
+ Number.isSafeInteger(value.at_ms) &&
113
+ value.at_ms >= 0)
114
+ return value.at_ms;
115
+ return null;
116
+ };
117
+ const firstCollectionDivergence = (dimension, left, right) => {
118
+ const length = Math.max(left.length, right.length);
119
+ for (let index = 0; index < length; index += 1) {
120
+ const leftValue = left[index] ?? null;
121
+ const rightValue = right[index] ?? null;
122
+ if (JSON.stringify(leftValue) === JSON.stringify(rightValue))
123
+ continue;
63
124
  return {
64
- status: "truncated",
65
- terminal: "truncated",
66
- exit: "truncated",
67
- filesystem: "truncated",
68
- protocol: "truncated",
69
- process: "truncated",
70
- limitations: ["At least one capture is truncated."],
125
+ status: "found",
126
+ dimension,
127
+ index,
128
+ left_at_ms: eventTime(leftValue),
129
+ right_at_ms: eventTime(rightValue),
130
+ left: leftValue,
131
+ right: rightValue,
71
132
  };
72
133
  }
73
- const sameNormalization = JSON.stringify(left.normalization) === JSON.stringify(right.normalization);
134
+ return null;
135
+ };
136
+ const chooseFirstDivergence = (candidates) => {
137
+ const found = candidates.filter((candidate) => candidate !== null);
138
+ return (found.sort((left, right) => {
139
+ const leftTime = Math.min(left.left_at_ms ?? Number.MAX_SAFE_INTEGER, left.right_at_ms ?? Number.MAX_SAFE_INTEGER);
140
+ const rightTime = Math.min(right.left_at_ms ?? Number.MAX_SAFE_INTEGER, right.right_at_ms ?? Number.MAX_SAFE_INTEGER);
141
+ return leftTime - rightTime;
142
+ })[0] ?? { status: "none" });
143
+ };
144
+ const assertComparable = (left, right, options) => {
145
+ const invalid = [
146
+ ...validateProcessCapture(left),
147
+ ...validateProcessCapture(right),
148
+ ];
149
+ if (invalid.length > 0)
150
+ throw new TypeError(`Invalid Process Capture v4: ${invalid[0].path}`);
151
+ if (left.schema_version !== right.schema_version ||
152
+ left.manifest.comparison_contract_sha256 !==
153
+ right.manifest.comparison_contract_sha256)
154
+ throw new TypeError("Process captures have incompatible comparison contracts");
155
+ if (options.maxCaptureAgeMs === undefined)
156
+ return;
157
+ const maxCaptureAgeMs = options.maxCaptureAgeMs;
158
+ const now = (options.now ?? Date.now)();
159
+ if ([left, right].some(({ manifest }) => now - Date.parse(manifest.completed_at) > maxCaptureAgeMs))
160
+ throw new TypeError("Process capture exceeds max_capture_age_ms");
161
+ };
162
+ const truncatedComparison = () => ({
163
+ status: "truncated",
164
+ terminal: "truncated",
165
+ interaction: "truncated",
166
+ exit: "truncated",
167
+ filesystem: "truncated",
168
+ protocol: "truncated",
169
+ process: "truncated",
170
+ shim: "truncated",
171
+ first_divergence: {
172
+ status: "unknown",
173
+ reason: "At least one capture is truncated.",
174
+ },
175
+ limitations: ["At least one capture is truncated."],
176
+ });
177
+ const compareDimensions = (left, right, sameNormalization) => {
74
178
  const classify = (scope, leftValues, rightValues) => !sameNormalization || hasUnknown(left, scope) || hasUnknown(right, scope)
75
179
  ? "unknown"
76
180
  : classifyCollection(leftValues, rightValues);
77
- const terminal = classify("terminal", left.frames.map(({ data }) => data), right.frames.map(({ data }) => data));
78
- const exit = hasUnknown(left, "exit") || hasUnknown(right, "exit")
79
- ? "unknown"
80
- : JSON.stringify(left.exit) === JSON.stringify(right.exit)
81
- ? "unchanged"
82
- : "changed";
83
- const filesystem = !hasUnknown(left, "filesystem") &&
84
- !hasUnknown(right, "filesystem") &&
85
- sameNormalization &&
86
- stableFiles(left) === stableFiles(right)
87
- ? "unchanged"
88
- : classify("filesystem", left.filesystem_effects, right.filesystem_effects);
89
- const protocol = classify("protocol", left.protocol_events, right.protocol_events);
90
- const process = classify("process", left.process_samples, right.process_samples);
91
181
  return {
92
- status: comparisonStatus([terminal, exit, filesystem, protocol, process]),
93
- terminal,
94
- exit,
95
- filesystem,
96
- protocol,
97
- process,
182
+ terminal: classify("terminal", terminalObservations(left), terminalObservations(right)),
183
+ interaction: classify("terminal", left.interaction_events, right.interaction_events),
184
+ exit: hasUnknown(left, "exit") || hasUnknown(right, "exit")
185
+ ? "unknown"
186
+ : JSON.stringify({ exit: left.exit, settlement: left.settlement }) ===
187
+ JSON.stringify({ exit: right.exit, settlement: right.settlement })
188
+ ? "unchanged"
189
+ : "changed",
190
+ filesystem: classify("filesystem", filesystemObservations(left), filesystemObservations(right)),
191
+ protocol: classify("protocol", left.protocol_events, right.protocol_events),
192
+ process: classify("process", left.process_samples, right.process_samples),
193
+ shim: sameNormalization
194
+ ? classifyCollection(left.shim_events, right.shim_events)
195
+ : "unknown",
196
+ };
197
+ };
198
+ /** Compare bounded captures without equating missing or incompatible evidence. */
199
+ /**
200
+ * Compare two Process Capture v4 observations without treating absence as proof.
201
+ *
202
+ * Observed differences outrank residual unknowns. A capture may therefore be
203
+ * definitively changed in one dimension while completeness remains unknown.
204
+ */
205
+ export const compareProcessCaptures = (left, right, options = {}) => {
206
+ assertComparable(left, right, options);
207
+ if (left.truncated || right.truncated)
208
+ return truncatedComparison();
209
+ const sameNormalization = JSON.stringify(left.normalization) === JSON.stringify(right.normalization);
210
+ const dimensions = compareDimensions(left, right, sameNormalization);
211
+ const observedFirstDivergence = chooseFirstDivergence([
212
+ firstCollectionDivergence("terminal", terminalObservations(left), terminalObservations(right)),
213
+ firstCollectionDivergence("interaction", left.interaction_events, right.interaction_events),
214
+ firstCollectionDivergence("exit", [{ ...left.exit, settlement: left.settlement }], [{ ...right.exit, settlement: right.settlement }]),
215
+ firstCollectionDivergence("filesystem", filesystemObservations(left), filesystemObservations(right)),
216
+ firstCollectionDivergence("protocol", left.protocol_events, right.protocol_events),
217
+ firstCollectionDivergence("process", left.process_samples, right.process_samples),
218
+ firstCollectionDivergence("shim", left.shim_events, right.shim_events),
219
+ ]);
220
+ const firstDivergence = observedFirstDivergence.status === "found"
221
+ ? observedFirstDivergence
222
+ : !sameNormalization ||
223
+ left.residual_unknowns.length > 0 ||
224
+ right.residual_unknowns.length > 0
225
+ ? {
226
+ status: "unknown",
227
+ reason: "Residual unknowns prevent proving that no divergence occurred.",
228
+ }
229
+ : observedFirstDivergence;
230
+ return {
231
+ status: comparisonStatus([
232
+ dimensions.terminal,
233
+ dimensions.interaction,
234
+ dimensions.exit,
235
+ dimensions.filesystem,
236
+ dimensions.protocol,
237
+ dimensions.process,
238
+ dimensions.shim,
239
+ ]),
240
+ ...dimensions,
241
+ first_divergence: firstDivergence,
98
242
  limitations: [
99
243
  ...left.limitations,
100
244
  ...right.limitations,
@@ -0,0 +1,370 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ import canonicalize from "canonicalize";
4
+ import { z } from "zod";
5
+ const positiveBudget = z.number().int().positive();
6
+ const timedEventBase = { at_ms: z.number().int().nonnegative() };
7
+ const environmentName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/u);
8
+ const reservedEnvironment = new Set([
9
+ "HOME",
10
+ "TERM",
11
+ "REA_PROCESS_RUN_ID",
12
+ "REA_REPLAY_HTTP_URL",
13
+ "REA_REPLAY_WEBSOCKET_URL",
14
+ "REA_SHIM_LEDGER_URL",
15
+ ]);
16
+ export const normalizationSchema = z.object({
17
+ paths: z.boolean(),
18
+ pids: z.boolean(),
19
+ ports: z.boolean(),
20
+ time_bucket_ms: positiveBudget.max(60_000),
21
+ patterns: z.array(z.object({
22
+ pattern: z.string().max(500),
23
+ replacement: z.string().max(100),
24
+ })),
25
+ });
26
+ const checkpointNameSchema = z
27
+ .string()
28
+ .regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/u);
29
+ const commandNameSchema = z
30
+ .string()
31
+ .regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u);
32
+ const outputChunkSchema = z.object({
33
+ at_ms: z.number().int().nonnegative().max(300_000),
34
+ stream: z.enum(["stdout", "stderr"]),
35
+ data: z.string().max(1_000_000),
36
+ });
37
+ /** Actionable refusal shared by CLI, MCP, and Evidence import boundaries. */
38
+ export const LEGACY_PROCESS_CAPTURE_MESSAGE = "Process Capture v3 is unsupported. Re-run the scenario with this REA version using capture_process_scenario to produce Process Capture v4; captures cannot be upgraded because v4 requires new manifest and settlement evidence.";
39
+ /** Compute a canonical SHA-256 commitment independent of object key order. */
40
+ export const digestProcessCommitment = (value) => {
41
+ const serialized = canonicalize(value);
42
+ if (serialized === undefined)
43
+ throw new TypeError("Process capture commitment is not canonical JSON");
44
+ return createHash("sha256").update(serialized).digest("hex");
45
+ };
46
+ /** Build the secret-safe scenario projection committed by Process Capture v4. */
47
+ export const processScenarioCommitment = (scenario, executableSha256) => ({
48
+ ...scenario,
49
+ environment: Object.fromEntries(Object.entries(scenario.environment).map(([name, value]) => [
50
+ name,
51
+ scenario.secret_aliases.includes(name) ? "<redacted-secret>" : value,
52
+ ])),
53
+ unknown_registry_approved: scenario.unknown_registry_approved === true,
54
+ executable_sha256: executableSha256 ?? null,
55
+ });
56
+ /** Project observation settings shared by authority and reconstruction. */
57
+ export const processComparisonContract = (scenario) => ({
58
+ working_directory: scenario.working_directory,
59
+ inherit_environment: scenario.inherit_environment,
60
+ secret_aliases: scenario.secret_aliases,
61
+ network_access: scenario.network_access,
62
+ filesystem_roots: scenario.filesystem_roots,
63
+ terminal: scenario.terminal,
64
+ checkpoints: scenario.checkpoints,
65
+ events: scenario.events,
66
+ timeout_ms: scenario.timeout_ms,
67
+ idle_timeout_ms: scenario.idle_timeout_ms,
68
+ settle_ms: scenario.settle_ms,
69
+ limits: scenario.limits,
70
+ normalization: scenario.normalization,
71
+ command_shims: scenario.command_shims,
72
+ replay: scenario.replay,
73
+ });
74
+ /**
75
+ * Boundary schema for one explicitly approved, bounded process experiment.
76
+ * Defaults are part of the evidence contract and must remain deterministic.
77
+ */
78
+ export const processScenarioSchema = z
79
+ .object({
80
+ approved: z
81
+ .literal(true)
82
+ .describe("Explicit per-call acknowledgement that this operation launches the target"),
83
+ unknown_registry_approved: z
84
+ .literal(true)
85
+ .optional()
86
+ .describe("Explicit approval to record capture residuals durably"),
87
+ executable: z.string().startsWith("/"),
88
+ arguments: z.array(z.string()).max(256).default([]),
89
+ working_directory: z.string().startsWith("/"),
90
+ environment: z.record(environmentName, z.string()).default({}),
91
+ inherit_environment: z.array(environmentName).max(64).default([]),
92
+ secret_aliases: z.array(environmentName).max(64).default([]),
93
+ network_access: z.literal("host").default("host"),
94
+ filesystem_roots: z.array(z.string().startsWith("/")).max(16).default([]),
95
+ terminal: z
96
+ .object({
97
+ columns: z.number().int().min(1).max(1_000).default(80),
98
+ rows: z.number().int().min(1).max(1_000).default(24),
99
+ scrollback: z.number().int().min(0).max(10_000).default(1_000),
100
+ })
101
+ .default({ columns: 80, rows: 24, scrollback: 1_000 }),
102
+ checkpoints: z
103
+ .array(z.object({
104
+ name: checkpointNameSchema,
105
+ trigger: z.discriminatedUnion("type", [
106
+ z.object({
107
+ type: z.literal("time"),
108
+ at_ms: z.number().int().nonnegative().max(300_000),
109
+ }),
110
+ z.object({
111
+ type: z.literal("terminal_literal"),
112
+ value: z.string().min(1).max(10_000),
113
+ occurrence: z.number().int().positive().max(1_000).default(1),
114
+ }),
115
+ z.object({ type: z.literal("root_exit") }),
116
+ z.object({ type: z.literal("settled") }),
117
+ ]),
118
+ }))
119
+ .max(64)
120
+ .default([]),
121
+ command_shims: z
122
+ .array(z.object({
123
+ name: commandNameSchema,
124
+ routes: z
125
+ .array(z.object({
126
+ arguments: z.array(z.string()).max(256),
127
+ outputs: z.array(outputChunkSchema).max(1_000).default([]),
128
+ termination: z.discriminatedUnion("type", [
129
+ z.object({
130
+ type: z.literal("exit"),
131
+ code: z.number().int().min(0).max(255),
132
+ }),
133
+ z.object({
134
+ type: z.literal("signal"),
135
+ signal: z.enum(["SIGINT", "SIGTERM", "SIGKILL"]),
136
+ }),
137
+ ]),
138
+ max_calls: z.number().int().positive().max(100).default(1),
139
+ }))
140
+ .min(1)
141
+ .max(100),
142
+ }))
143
+ .max(32)
144
+ .default([]),
145
+ events: z
146
+ .array(z.discriminatedUnion("type", [
147
+ z.object({
148
+ ...timedEventBase,
149
+ type: z.literal("input"),
150
+ data: z.string(),
151
+ sensitive: z.boolean().default(false),
152
+ }),
153
+ z.object({
154
+ ...timedEventBase,
155
+ type: z.literal("resize"),
156
+ columns: z.number().int().min(1).max(1_000),
157
+ rows: z.number().int().min(1).max(1_000),
158
+ }),
159
+ z.object({
160
+ ...timedEventBase,
161
+ type: z.literal("signal"),
162
+ signal: z.enum(["SIGINT", "SIGTERM", "SIGKILL"]),
163
+ }),
164
+ ]))
165
+ .max(1_000)
166
+ .default([]),
167
+ timeout_ms: positiveBudget.max(300_000).default(30_000),
168
+ idle_timeout_ms: positiveBudget.max(300_000).default(30_000),
169
+ settle_ms: z.number().int().nonnegative().max(10_000).default(100),
170
+ limits: z
171
+ .object({
172
+ output_bytes: positiveBudget.max(10_000_000).default(1_000_000),
173
+ frames: positiveBudget.max(100_000).default(10_000),
174
+ files: positiveBudget.max(100_000).default(10_000),
175
+ file_bytes: positiveBudget.max(100_000_000).default(10_000_000),
176
+ processes: positiveBudget.max(10_000).default(1_000),
177
+ protocol_events: positiveBudget.max(100_000).default(10_000),
178
+ protocol_body_bytes: positiveBudget.max(10_000_000).default(1_000_000),
179
+ connections: positiveBudget.max(1_000).default(100),
180
+ filesystem_depth: z.number().int().min(1).max(64).default(16),
181
+ })
182
+ .default({
183
+ output_bytes: 1_000_000,
184
+ frames: 10_000,
185
+ files: 10_000,
186
+ file_bytes: 10_000_000,
187
+ processes: 1_000,
188
+ protocol_events: 10_000,
189
+ protocol_body_bytes: 1_000_000,
190
+ connections: 100,
191
+ filesystem_depth: 16,
192
+ }),
193
+ normalization: z
194
+ .object({
195
+ paths: z.boolean().default(true),
196
+ pids: z.boolean().default(true),
197
+ ports: z.boolean().default(true),
198
+ time_bucket_ms: positiveBudget.max(60_000).default(10),
199
+ patterns: z
200
+ .array(z.object({
201
+ pattern: z.string().max(500),
202
+ replacement: z.string().max(100),
203
+ }))
204
+ .max(32)
205
+ .default([]),
206
+ })
207
+ .default({
208
+ paths: true,
209
+ pids: true,
210
+ ports: true,
211
+ time_bucket_ms: 10,
212
+ patterns: [],
213
+ }),
214
+ replay: z
215
+ .object({
216
+ http: z
217
+ .array(z.object({
218
+ method: z.string().max(16),
219
+ path: z.string().startsWith("/"),
220
+ status: z.number().int().min(100).max(599),
221
+ body: z.string().max(1_000_000),
222
+ request_headers: z.record(z.string(), z.string()).default({}),
223
+ request_body: z.string().max(1_000_000).optional(),
224
+ response_headers: z.record(z.string(), z.string()).default({}),
225
+ delay_ms: z.number().int().nonnegative().max(30_000).default(0),
226
+ disconnect: z.boolean().default(false),
227
+ max_calls: z.number().int().min(1).max(100).default(1),
228
+ }))
229
+ .max(100)
230
+ .default([]),
231
+ websocket_messages: z
232
+ .array(z.string().max(1_000_000))
233
+ .max(100)
234
+ .default([]),
235
+ websocket_connections: z
236
+ .array(z.object({
237
+ messages: z
238
+ .array(z.object({
239
+ data: z.string().max(1_000_000),
240
+ delay_ms: z
241
+ .number()
242
+ .int()
243
+ .nonnegative()
244
+ .max(30_000)
245
+ .default(0),
246
+ }))
247
+ .max(100),
248
+ disconnect_after: z.boolean().default(false),
249
+ }))
250
+ .max(100)
251
+ .default([]),
252
+ })
253
+ .default({
254
+ http: [],
255
+ websocket_messages: [],
256
+ websocket_connections: [],
257
+ }),
258
+ })
259
+ .strict()
260
+ .superRefine((scenario, context) => {
261
+ for (let index = 1; index < scenario.events.length; index += 1) {
262
+ if (scenario.events[index].at_ms < scenario.events[index - 1].at_ms) {
263
+ context.addIssue({
264
+ code: "custom",
265
+ message: "events must be ordered by at_ms",
266
+ path: ["events", index],
267
+ });
268
+ }
269
+ }
270
+ const secrets = new Set(scenario.secret_aliases);
271
+ for (const alias of secrets) {
272
+ if (!(alias in scenario.environment)) {
273
+ context.addIssue({
274
+ code: "custom",
275
+ message: "secret alias has no environment value",
276
+ path: ["secret_aliases"],
277
+ });
278
+ }
279
+ }
280
+ const explicit = new Set(Object.keys(scenario.environment));
281
+ for (const name of [...explicit, ...scenario.inherit_environment]) {
282
+ if (reservedEnvironment.has(name)) {
283
+ context.addIssue({
284
+ code: "custom",
285
+ message: `${name} is reserved by the process adapter`,
286
+ path: ["environment", name],
287
+ });
288
+ }
289
+ }
290
+ for (const name of scenario.inherit_environment) {
291
+ if (explicit.has(name)) {
292
+ context.addIssue({
293
+ code: "custom",
294
+ message: "an environment name cannot be explicit and inherited",
295
+ path: ["inherit_environment"],
296
+ });
297
+ }
298
+ }
299
+ for (const event of scenario.events) {
300
+ if (event.at_ms > scenario.timeout_ms) {
301
+ context.addIssue({
302
+ code: "custom",
303
+ message: "event occurs after the scenario timeout",
304
+ path: ["events"],
305
+ });
306
+ }
307
+ }
308
+ const checkpointNames = scenario.checkpoints.map(({ name }) => name);
309
+ for (const [index, name] of checkpointNames.entries()) {
310
+ if (name === "before" || name === "after_settlement")
311
+ context.addIssue({
312
+ code: "custom",
313
+ message: "checkpoint name is reserved by the capture lifecycle",
314
+ path: ["checkpoints", index, "name"],
315
+ });
316
+ }
317
+ if (new Set(checkpointNames).size !== checkpointNames.length)
318
+ context.addIssue({
319
+ code: "custom",
320
+ message: "checkpoint names must be unique",
321
+ path: ["checkpoints"],
322
+ });
323
+ const shimNames = scenario.command_shims.map(({ name }) => name);
324
+ if (new Set(shimNames).size !== shimNames.length)
325
+ context.addIssue({
326
+ code: "custom",
327
+ message: "command shim names must be unique",
328
+ path: ["command_shims"],
329
+ });
330
+ });
331
+ /** Parse untrusted process scenario input and apply safe default budgets. */
332
+ export const parseProcessScenario = (input) => processScenarioSchema.parse(input);
333
+ const isWithin = (candidate, root) => resolve(candidate) === resolve(root) ||
334
+ resolve(candidate).startsWith(`${resolve(root)}${resolve(root).endsWith("/") ? "" : "/"}`);
335
+ /** Evaluate scenario authority before any process or filesystem side effect occurs. */
336
+ export const authorizeProcessScenario = (scenario, policy) => {
337
+ if (!policy.enabled)
338
+ return { allowed: false, reason: "process capture is disabled" };
339
+ if (scenario.network_access === "host" && !policy.allowExternalNetwork)
340
+ return {
341
+ allowed: false,
342
+ reason: "host network access is not approved by operator policy",
343
+ };
344
+ if (!policy.executableRoots.some((root) => isWithin(scenario.executable, root))) {
345
+ return { allowed: false, reason: "executable is outside approved roots" };
346
+ }
347
+ if (!policy.workingRoots.some((root) => isWithin(scenario.working_directory, root))) {
348
+ return {
349
+ allowed: false,
350
+ reason: "working directory is outside approved roots",
351
+ };
352
+ }
353
+ const requestedNames = [
354
+ ...Object.keys(scenario.environment),
355
+ ...scenario.inherit_environment,
356
+ ];
357
+ if (requestedNames.some((name) => !policy.allowedEnvironment.includes(name))) {
358
+ return {
359
+ allowed: false,
360
+ reason: "scenario requests an environment variable not allowed by policy",
361
+ };
362
+ }
363
+ if (scenario.filesystem_roots.some((path) => !policy.workingRoots.some((root) => isWithin(path, root)))) {
364
+ return {
365
+ allowed: false,
366
+ reason: "filesystem root is outside approved roots",
367
+ };
368
+ }
369
+ return { allowed: true };
370
+ };
@@ -18,7 +18,7 @@ const MAX_CANONICAL_BYTES = 32 * 1024 * 1024;
18
18
  const providers = {
19
19
  behavioral: {
20
20
  operation: "compare_process_captures",
21
- predicate: "rea.process-comparison/v1",
21
+ predicate: "rea.process-comparison/v3",
22
22
  id: "rea-process",
23
23
  name: "REA deterministic process harness",
24
24
  },
@@ -137,7 +137,7 @@ const evaluateClaim = (claim, records, heads) => {
137
137
  };
138
138
  const validateSourceKinds = (claim, sources) => {
139
139
  const expected = claim.kind === "behavioral"
140
- ? ["capture_process_scenario", "rea.process-capture/v2"]
140
+ ? ["capture_process_scenario", "rea.process-capture/v4"]
141
141
  : claim.kind === "structural-function"
142
142
  ? ["analyze_function", "rea.analysis/v2"]
143
143
  : ["inventory_artifact", "rea.analysis/v2"];
@@ -160,7 +160,7 @@ const validateComparisonIdentity = (claim, evidence) => {
160
160
  evidence.predicate_type !== expected.predicate ||
161
161
  evidence.provider.id !== expected.id ||
162
162
  evidence.provider.name !== expected.name ||
163
- evidence.provider.version !== "1" ||
163
+ evidence.provider.version !== (claim.kind === "behavioral" ? "3" : "1") ||
164
164
  evidence.confidence !== "derived" ||
165
165
  evidence.authority !== "analyst-inference" ||
166
166
  evidence.subject !== null)
@@ -0,0 +1,8 @@
1
+ /** Return whether a Node.js version satisfies REA's tested runtime families. */
2
+ export const supportsNodeVersion = (version) => {
3
+ const [majorText = "", minorText = ""] = version.split(".");
4
+ const major = Number.parseInt(majorText, 10);
5
+ const minor = Number.parseInt(minorText, 10);
6
+ return ((major === 22 && minor >= 19) ||
7
+ (major >= 24 && (major !== 24 || minor >= 11)));
8
+ };
@@ -138,7 +138,7 @@ const EXPECTED = {
138
138
  "REA function comparison",
139
139
  ],
140
140
  compare_process_captures: [
141
- "rea.process-comparison/v1",
141
+ "rea.process-comparison/v3",
142
142
  "rea-process",
143
143
  "REA deterministic process harness",
144
144
  ],
@@ -299,7 +299,8 @@ const assertComparisonIdentity = (evidence) => {
299
299
  evidence.predicate_type !== expected[0] ||
300
300
  evidence.provider.id !== expected[1] ||
301
301
  evidence.provider.name !== expected[2] ||
302
- evidence.provider.version !== "1" ||
302
+ evidence.provider.version !==
303
+ (evidence.operation === "compare_process_captures" ? "3" : "1") ||
303
304
  evidence.confidence !== "derived" ||
304
305
  evidence.authority !== "analyst-inference" ||
305
306
  evidence.subject !== null ||
package/dist/identity.js CHANGED
@@ -6,6 +6,7 @@
6
6
  export const PRODUCT_IDENTITY = {
7
7
  displayName: "REA",
8
8
  packageName: "rea-agents",
9
+ packageVersion: "1.1.0",
9
10
  cliBinary: "rea",
10
11
  mcpCommand: "npx -y rea-agents mcp",
11
12
  mcpServerKey: "rea",