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.
- package/README.md +21 -2
- package/dist/application/ArtifactInventory.js +80 -44
- package/dist/application/CommandShimReplay.js +136 -0
- package/dist/application/EvidenceBundleFiles.js +9 -1
- package/dist/application/ProcessCaptureAuthority.js +25 -0
- package/dist/application/ProcessCaptureCapability.js +23 -0
- package/dist/application/ProcessCaptureError.js +5 -0
- package/dist/application/ProcessCaptureLifecycle.js +269 -0
- package/dist/application/ProcessCheckpoints.js +129 -0
- package/dist/application/ProcessCli.js +61 -0
- package/dist/application/ProcessEvidence.js +38 -0
- package/dist/application/ProcessHarness.js +252 -272
- package/dist/application/ProcessNormalization.js +10 -1
- package/dist/application/ProcessOwnership.js +39 -1
- package/dist/application/ProcessSampling.js +39 -23
- package/dist/application/Setup.js +51 -76
- package/dist/application/SetupSkill.js +44 -0
- package/dist/application/TerminalRenderer.js +92 -0
- package/dist/application/runtime.js +1 -1
- package/dist/artifacts/ArtifactProvider.js +18 -6
- package/dist/artifacts/ArtifactReader.js +3 -1
- package/dist/artifacts/AsarArtifactReader.js +8 -1
- package/dist/artifacts/DirectoryArtifactReader.js +1 -0
- package/dist/artifacts/MachOSliceArtifactReader.js +1 -0
- package/dist/artifacts/NativeDmgArtifactReader.js +151 -0
- package/dist/artifacts/ZipArtifactReader.js +1 -0
- package/dist/cli.js +29 -24
- package/dist/cliProcessCommands.js +19 -0
- package/dist/config.js +2 -0
- package/dist/contracts/artifactToolContracts.js +1 -0
- package/dist/contracts/investigationExamples.js +3 -3
- package/dist/contracts/processCaptureExample.js +50 -7
- package/dist/contracts/toolContracts.js +3 -2
- package/dist/domain/changedBehavior.js +2 -2
- package/dist/domain/errors.js +11 -3
- package/dist/domain/processCapture.js +99 -293
- package/dist/domain/processCaptureValidation.js +124 -0
- package/dist/domain/processComparison.js +176 -32
- package/dist/domain/processScenario.js +370 -0
- package/dist/domain/reconstructionVerification.js +3 -3
- package/dist/domain/staticRuntimeCorrelation.js +3 -2
- package/dist/identity.js +1 -0
- package/dist/server/registerProcessComparisonTool.js +28 -8
- package/dist/server/registerSessionTools.js +3 -25
- package/dist/server/sessionToolPolicies.js +1 -6
- package/package.json +5 -1
- package/scripts/prepare-node-pty.mjs +35 -0
- package/skills/rea-analysis/SKILL.md +19 -4
|
@@ -1,265 +1,105 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
1
|
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
])
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
pids: z.boolean(),
|
|
16
|
-
ports: z.boolean(),
|
|
17
|
-
time_bucket_ms: positiveBudget.max(60_000),
|
|
18
|
-
patterns: z.array(z.object({
|
|
19
|
-
pattern: z.string().max(500),
|
|
20
|
-
replacement: z.string().max(100),
|
|
21
|
-
})),
|
|
2
|
+
import { LEGACY_PROCESS_CAPTURE_MESSAGE, normalizationSchema, } from "./processScenario.js";
|
|
3
|
+
export * from "./processScenario.js";
|
|
4
|
+
const fileStateSchema = z.object({
|
|
5
|
+
path: z.string(),
|
|
6
|
+
type: z.enum(["file", "directory", "symlink", "other"]),
|
|
7
|
+
mode: z.number().int().nonnegative(),
|
|
8
|
+
size: z.number().int().nonnegative(),
|
|
9
|
+
sha256: z
|
|
10
|
+
.string()
|
|
11
|
+
.regex(/^[a-f0-9]{64}$/u)
|
|
12
|
+
.nullable(),
|
|
13
|
+
symlink_target: z.string().nullable(),
|
|
22
14
|
});
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.describe("Explicit per-call acknowledgement that this operation launches the target"),
|
|
29
|
-
unknown_registry_approved: z
|
|
30
|
-
.literal(true)
|
|
31
|
-
.optional()
|
|
32
|
-
.describe("Explicit approval to record capture residuals durably"),
|
|
33
|
-
executable: z.string().startsWith("/"),
|
|
34
|
-
arguments: z.array(z.string()).max(256).default([]),
|
|
35
|
-
working_directory: z.string().startsWith("/"),
|
|
36
|
-
environment: z.record(environmentName, z.string()).default({}),
|
|
37
|
-
inherit_environment: z.array(environmentName).max(64).default([]),
|
|
38
|
-
secret_aliases: z.array(environmentName).max(64).default([]),
|
|
39
|
-
network_access: z.literal("host").default("host"),
|
|
40
|
-
filesystem_roots: z.array(z.string().startsWith("/")).max(16).default([]),
|
|
41
|
-
events: z
|
|
42
|
-
.array(z.discriminatedUnion("type", [
|
|
43
|
-
z.object({
|
|
44
|
-
...timedEventBase,
|
|
45
|
-
type: z.literal("input"),
|
|
46
|
-
data: z.string(),
|
|
47
|
-
}),
|
|
48
|
-
z.object({
|
|
49
|
-
...timedEventBase,
|
|
50
|
-
type: z.literal("resize"),
|
|
51
|
-
columns: z.number().int().min(1).max(1_000),
|
|
52
|
-
rows: z.number().int().min(1).max(1_000),
|
|
53
|
-
}),
|
|
54
|
-
z.object({
|
|
55
|
-
...timedEventBase,
|
|
56
|
-
type: z.literal("signal"),
|
|
57
|
-
signal: z.enum(["SIGINT", "SIGTERM", "SIGKILL"]),
|
|
58
|
-
}),
|
|
59
|
-
]))
|
|
60
|
-
.max(1_000)
|
|
61
|
-
.default([]),
|
|
62
|
-
timeout_ms: positiveBudget.max(300_000).default(30_000),
|
|
63
|
-
idle_timeout_ms: positiveBudget.max(300_000).default(30_000),
|
|
64
|
-
settle_ms: z.number().int().nonnegative().max(10_000).default(100),
|
|
65
|
-
limits: z
|
|
66
|
-
.object({
|
|
67
|
-
output_bytes: positiveBudget.max(10_000_000).default(1_000_000),
|
|
68
|
-
frames: positiveBudget.max(100_000).default(10_000),
|
|
69
|
-
files: positiveBudget.max(100_000).default(10_000),
|
|
70
|
-
file_bytes: positiveBudget.max(100_000_000).default(10_000_000),
|
|
71
|
-
processes: positiveBudget.max(10_000).default(1_000),
|
|
72
|
-
protocol_events: positiveBudget.max(100_000).default(10_000),
|
|
73
|
-
protocol_body_bytes: positiveBudget.max(10_000_000).default(1_000_000),
|
|
74
|
-
connections: positiveBudget.max(1_000).default(100),
|
|
75
|
-
filesystem_depth: z.number().int().min(1).max(64).default(16),
|
|
76
|
-
})
|
|
77
|
-
.default({
|
|
78
|
-
output_bytes: 1_000_000,
|
|
79
|
-
frames: 10_000,
|
|
80
|
-
files: 10_000,
|
|
81
|
-
file_bytes: 10_000_000,
|
|
82
|
-
processes: 1_000,
|
|
83
|
-
protocol_events: 10_000,
|
|
84
|
-
protocol_body_bytes: 1_000_000,
|
|
85
|
-
connections: 100,
|
|
86
|
-
filesystem_depth: 16,
|
|
87
|
-
}),
|
|
88
|
-
normalization: z
|
|
89
|
-
.object({
|
|
90
|
-
paths: z.boolean().default(true),
|
|
91
|
-
pids: z.boolean().default(true),
|
|
92
|
-
ports: z.boolean().default(true),
|
|
93
|
-
time_bucket_ms: positiveBudget.max(60_000).default(10),
|
|
94
|
-
patterns: z
|
|
95
|
-
.array(z.object({
|
|
96
|
-
pattern: z.string().max(500),
|
|
97
|
-
replacement: z.string().max(100),
|
|
98
|
-
}))
|
|
99
|
-
.max(32)
|
|
100
|
-
.default([]),
|
|
101
|
-
})
|
|
102
|
-
.default({
|
|
103
|
-
paths: true,
|
|
104
|
-
pids: true,
|
|
105
|
-
ports: true,
|
|
106
|
-
time_bucket_ms: 10,
|
|
107
|
-
patterns: [],
|
|
108
|
-
}),
|
|
109
|
-
replay: z
|
|
110
|
-
.object({
|
|
111
|
-
http: z
|
|
112
|
-
.array(z.object({
|
|
113
|
-
method: z.string().max(16),
|
|
114
|
-
path: z.string().startsWith("/"),
|
|
115
|
-
status: z.number().int().min(100).max(599),
|
|
116
|
-
body: z.string().max(1_000_000),
|
|
117
|
-
request_headers: z.record(z.string(), z.string()).default({}),
|
|
118
|
-
request_body: z.string().max(1_000_000).optional(),
|
|
119
|
-
response_headers: z.record(z.string(), z.string()).default({}),
|
|
120
|
-
delay_ms: z.number().int().nonnegative().max(30_000).default(0),
|
|
121
|
-
disconnect: z.boolean().default(false),
|
|
122
|
-
max_calls: z.number().int().min(1).max(100).default(1),
|
|
123
|
-
}))
|
|
124
|
-
.max(100)
|
|
125
|
-
.default([]),
|
|
126
|
-
websocket_messages: z
|
|
127
|
-
.array(z.string().max(1_000_000))
|
|
128
|
-
.max(100)
|
|
129
|
-
.default([]),
|
|
130
|
-
websocket_connections: z
|
|
131
|
-
.array(z.object({
|
|
132
|
-
messages: z
|
|
133
|
-
.array(z.object({
|
|
134
|
-
data: z.string().max(1_000_000),
|
|
135
|
-
delay_ms: z
|
|
136
|
-
.number()
|
|
137
|
-
.int()
|
|
138
|
-
.nonnegative()
|
|
139
|
-
.max(30_000)
|
|
140
|
-
.default(0),
|
|
141
|
-
}))
|
|
142
|
-
.max(100),
|
|
143
|
-
disconnect_after: z.boolean().default(false),
|
|
144
|
-
}))
|
|
145
|
-
.max(100)
|
|
146
|
-
.default([]),
|
|
147
|
-
})
|
|
148
|
-
.default({
|
|
149
|
-
http: [],
|
|
150
|
-
websocket_messages: [],
|
|
151
|
-
websocket_connections: [],
|
|
152
|
-
}),
|
|
153
|
-
})
|
|
154
|
-
.strict()
|
|
155
|
-
.superRefine((scenario, context) => {
|
|
156
|
-
for (let index = 1; index < scenario.events.length; index += 1) {
|
|
157
|
-
if (scenario.events[index].at_ms < scenario.events[index - 1].at_ms) {
|
|
158
|
-
context.addIssue({
|
|
159
|
-
code: "custom",
|
|
160
|
-
message: "events must be ordered by at_ms",
|
|
161
|
-
path: ["events", index],
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const secrets = new Set(scenario.secret_aliases);
|
|
166
|
-
for (const alias of secrets) {
|
|
167
|
-
if (!(alias in scenario.environment)) {
|
|
168
|
-
context.addIssue({
|
|
169
|
-
code: "custom",
|
|
170
|
-
message: "secret alias has no environment value",
|
|
171
|
-
path: ["secret_aliases"],
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const explicit = new Set(Object.keys(scenario.environment));
|
|
176
|
-
for (const name of [...explicit, ...scenario.inherit_environment]) {
|
|
177
|
-
if (reservedEnvironment.has(name)) {
|
|
178
|
-
context.addIssue({
|
|
179
|
-
code: "custom",
|
|
180
|
-
message: `${name} is reserved by the process adapter`,
|
|
181
|
-
path: ["environment", name],
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
for (const name of scenario.inherit_environment) {
|
|
186
|
-
if (explicit.has(name)) {
|
|
187
|
-
context.addIssue({
|
|
188
|
-
code: "custom",
|
|
189
|
-
message: "an environment name cannot be explicit and inherited",
|
|
190
|
-
path: ["inherit_environment"],
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
for (const event of scenario.events) {
|
|
195
|
-
if (event.at_ms > scenario.timeout_ms) {
|
|
196
|
-
context.addIssue({
|
|
197
|
-
code: "custom",
|
|
198
|
-
message: "event occurs after the scenario timeout",
|
|
199
|
-
path: ["events"],
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
}
|
|
15
|
+
const fileEffectSchema = z.object({
|
|
16
|
+
path: z.string(),
|
|
17
|
+
status: z.enum(["created", "modified", "deleted", "unchanged"]),
|
|
18
|
+
before: fileStateSchema.nullable(),
|
|
19
|
+
after: fileStateSchema.nullable(),
|
|
203
20
|
});
|
|
204
|
-
/** Parse untrusted process scenario input and apply safe default budgets. */
|
|
205
|
-
export const parseProcessScenario = (input) => processScenarioSchema.parse(input);
|
|
206
|
-
const isWithin = (candidate, root) => resolve(candidate) === resolve(root) ||
|
|
207
|
-
resolve(candidate).startsWith(`${resolve(root)}${resolve(root).endsWith("/") ? "" : "/"}`);
|
|
208
|
-
/** Evaluate scenario authority before any process or filesystem side effect occurs. */
|
|
209
|
-
export const authorizeProcessScenario = (scenario, policy) => {
|
|
210
|
-
if (!policy.enabled)
|
|
211
|
-
return { allowed: false, reason: "process capture is disabled" };
|
|
212
|
-
if (scenario.network_access === "host" && !policy.allowExternalNetwork)
|
|
213
|
-
return {
|
|
214
|
-
allowed: false,
|
|
215
|
-
reason: "host network access is not approved by operator policy",
|
|
216
|
-
};
|
|
217
|
-
if (!policy.executableRoots.some((root) => isWithin(scenario.executable, root))) {
|
|
218
|
-
return { allowed: false, reason: "executable is outside approved roots" };
|
|
219
|
-
}
|
|
220
|
-
if (!policy.workingRoots.some((root) => isWithin(scenario.working_directory, root))) {
|
|
221
|
-
return {
|
|
222
|
-
allowed: false,
|
|
223
|
-
reason: "working directory is outside approved roots",
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
const requestedNames = [
|
|
227
|
-
...Object.keys(scenario.environment),
|
|
228
|
-
...scenario.inherit_environment,
|
|
229
|
-
];
|
|
230
|
-
if (requestedNames.some((name) => !policy.allowedEnvironment.includes(name))) {
|
|
231
|
-
return {
|
|
232
|
-
allowed: false,
|
|
233
|
-
reason: "scenario requests an environment variable not allowed by policy",
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
if (scenario.filesystem_roots.some((path) => !policy.workingRoots.some((root) => isWithin(path, root)))) {
|
|
237
|
-
return {
|
|
238
|
-
allowed: false,
|
|
239
|
-
reason: "filesystem root is outside approved roots",
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
return { allowed: true };
|
|
243
|
-
};
|
|
244
21
|
/** Exact serialized shape of a bounded process capture. */
|
|
245
22
|
export const processCaptureSchema = z.object({
|
|
246
|
-
schema_version: z.literal(
|
|
23
|
+
schema_version: z.literal(4, {
|
|
24
|
+
error: LEGACY_PROCESS_CAPTURE_MESSAGE,
|
|
25
|
+
}),
|
|
26
|
+
manifest: z.object({
|
|
27
|
+
rea_version: z.string().min(1),
|
|
28
|
+
provider_version: z.string().min(1),
|
|
29
|
+
platform: z.string().min(1),
|
|
30
|
+
architecture: z.string().min(1),
|
|
31
|
+
pty_backend: z.literal("node-pty"),
|
|
32
|
+
started_at: z.iso.datetime(),
|
|
33
|
+
completed_at: z.iso.datetime(),
|
|
34
|
+
scenario: z.record(z.string(), z.unknown()),
|
|
35
|
+
comparison_contract: z.record(z.string(), z.unknown()),
|
|
36
|
+
shim_plan: z.array(z.unknown()),
|
|
37
|
+
replay_plan: z.record(z.string(), z.unknown()),
|
|
38
|
+
full_scenario_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
39
|
+
comparison_contract_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
40
|
+
executable_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
41
|
+
normalization_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
42
|
+
shim_plan_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
43
|
+
replay_plan_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
44
|
+
}),
|
|
247
45
|
normalization: normalizationSchema,
|
|
248
46
|
frames: z.array(z.object({
|
|
249
47
|
sequence: z.number().int().nonnegative(),
|
|
250
48
|
at_ms: z.number().int().nonnegative(),
|
|
251
49
|
data: z.string(),
|
|
252
50
|
})),
|
|
51
|
+
rendered_frames: z.array(z.object({
|
|
52
|
+
sequence: z.number().int().nonnegative(),
|
|
53
|
+
at_ms: z.number().int().nonnegative(),
|
|
54
|
+
columns: z.number().int().positive(),
|
|
55
|
+
rows: z.number().int().positive(),
|
|
56
|
+
cursor_x: z.number().int().nonnegative(),
|
|
57
|
+
cursor_y: z.number().int().nonnegative(),
|
|
58
|
+
active_buffer: z.enum(["normal", "alternate"]),
|
|
59
|
+
lines: z.array(z.string()),
|
|
60
|
+
serialized_state: z.string(),
|
|
61
|
+
})),
|
|
62
|
+
interaction_events: z.array(z.object({
|
|
63
|
+
sequence: z.number().int().nonnegative(),
|
|
64
|
+
scheduled_at_ms: z.number().int().nonnegative(),
|
|
65
|
+
dispatched_at_ms: z.number().int().nonnegative(),
|
|
66
|
+
type: z.enum(["input", "resize", "signal"]),
|
|
67
|
+
data: z.string(),
|
|
68
|
+
outcome: z.enum(["dispatched", "target_exited", "failed"]),
|
|
69
|
+
})),
|
|
253
70
|
exit: z.object({
|
|
254
71
|
code: z.number().int().nullable(),
|
|
255
72
|
signal: z.number().int().nullable(),
|
|
256
73
|
reason: z.enum(["exited", "timeout", "idle_timeout"]),
|
|
257
74
|
}),
|
|
75
|
+
settlement: z.object({
|
|
76
|
+
state: z.enum(["quiesced", "alive_at_deadline", "unverifiable"]),
|
|
77
|
+
elapsed_ms: z.number().int().nonnegative(),
|
|
78
|
+
cleanup_outcome: z.enum(["not_required", "cleaned", "failed"]),
|
|
79
|
+
}),
|
|
258
80
|
process_samples: z.array(z.object({
|
|
259
81
|
at_ms: z.number().int().nonnegative(),
|
|
260
82
|
pid: z.number().int().positive(),
|
|
261
83
|
parent_pid: z.number().int().nonnegative(),
|
|
262
84
|
command: z.string(),
|
|
85
|
+
process_group_id: z.number().int().positive().nullable(),
|
|
86
|
+
session_id: z.number().int().nonnegative().nullable(),
|
|
87
|
+
})),
|
|
88
|
+
filesystem_checkpoints: z.array(z.object({
|
|
89
|
+
name: z.string(),
|
|
90
|
+
at_ms: z.number().int().nonnegative(),
|
|
91
|
+
files: z.array(fileStateSchema),
|
|
92
|
+
effects: z.array(fileEffectSchema),
|
|
93
|
+
truncated: z.boolean(),
|
|
94
|
+
})),
|
|
95
|
+
shim_events: z.array(z.object({
|
|
96
|
+
sequence: z.number().int().nonnegative(),
|
|
97
|
+
at_ms: z.number().int().nonnegative(),
|
|
98
|
+
command: z.string(),
|
|
99
|
+
route_index: z.number().int().nonnegative().nullable(),
|
|
100
|
+
arguments: z.array(z.string()),
|
|
101
|
+
working_directory: z.string(),
|
|
102
|
+
outcome: z.enum(["matched", "unmatched", "exhausted"]),
|
|
263
103
|
})),
|
|
264
104
|
protocol_events: z.array(z.object({
|
|
265
105
|
sequence: z.number().int().nonnegative(),
|
|
@@ -276,58 +116,9 @@ export const processCaptureSchema = z.object({
|
|
|
276
116
|
"disconnected",
|
|
277
117
|
]),
|
|
278
118
|
})),
|
|
279
|
-
files_before: z.array(
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
mode: z.number().int().nonnegative(),
|
|
283
|
-
size: z.number().int().nonnegative(),
|
|
284
|
-
sha256: z
|
|
285
|
-
.string()
|
|
286
|
-
.regex(/^[a-f0-9]{64}$/u)
|
|
287
|
-
.nullable(),
|
|
288
|
-
symlink_target: z.string().nullable(),
|
|
289
|
-
})),
|
|
290
|
-
files_after: z.array(z.object({
|
|
291
|
-
path: z.string(),
|
|
292
|
-
type: z.enum(["file", "directory", "symlink", "other"]),
|
|
293
|
-
mode: z.number().int().nonnegative(),
|
|
294
|
-
size: z.number().int().nonnegative(),
|
|
295
|
-
sha256: z
|
|
296
|
-
.string()
|
|
297
|
-
.regex(/^[a-f0-9]{64}$/u)
|
|
298
|
-
.nullable(),
|
|
299
|
-
symlink_target: z.string().nullable(),
|
|
300
|
-
})),
|
|
301
|
-
filesystem_effects: z.array(z.object({
|
|
302
|
-
path: z.string(),
|
|
303
|
-
status: z.enum(["created", "modified", "deleted", "unchanged"]),
|
|
304
|
-
before: z
|
|
305
|
-
.object({
|
|
306
|
-
path: z.string(),
|
|
307
|
-
type: z.enum(["file", "directory", "symlink", "other"]),
|
|
308
|
-
mode: z.number().int().nonnegative(),
|
|
309
|
-
size: z.number().int().nonnegative(),
|
|
310
|
-
sha256: z
|
|
311
|
-
.string()
|
|
312
|
-
.regex(/^[a-f0-9]{64}$/u)
|
|
313
|
-
.nullable(),
|
|
314
|
-
symlink_target: z.string().nullable(),
|
|
315
|
-
})
|
|
316
|
-
.nullable(),
|
|
317
|
-
after: z
|
|
318
|
-
.object({
|
|
319
|
-
path: z.string(),
|
|
320
|
-
type: z.enum(["file", "directory", "symlink", "other"]),
|
|
321
|
-
mode: z.number().int().nonnegative(),
|
|
322
|
-
size: z.number().int().nonnegative(),
|
|
323
|
-
sha256: z
|
|
324
|
-
.string()
|
|
325
|
-
.regex(/^[a-f0-9]{64}$/u)
|
|
326
|
-
.nullable(),
|
|
327
|
-
symlink_target: z.string().nullable(),
|
|
328
|
-
})
|
|
329
|
-
.nullable(),
|
|
330
|
-
})),
|
|
119
|
+
files_before: z.array(fileStateSchema),
|
|
120
|
+
files_after: z.array(fileStateSchema),
|
|
121
|
+
filesystem_effects: z.array(fileEffectSchema),
|
|
331
122
|
truncated: z.boolean(),
|
|
332
123
|
limitations: z.array(z.string()),
|
|
333
124
|
residual_unknowns: z.array(z.object({
|
|
@@ -347,4 +138,19 @@ export const processCaptureSchema = z.object({
|
|
|
347
138
|
temporary_root: z.literal("removed"),
|
|
348
139
|
}),
|
|
349
140
|
});
|
|
141
|
+
export { validateProcessCapture } from "./processCaptureValidation.js";
|
|
142
|
+
import { validateProcessCapture } from "./processCaptureValidation.js";
|
|
143
|
+
/** Parse unknown input as v4 and reject invalid commitments or semantics. */
|
|
144
|
+
export const parseProcessCapture = (input) => {
|
|
145
|
+
if (typeof input === "object" &&
|
|
146
|
+
input !== null &&
|
|
147
|
+
"schema_version" in input &&
|
|
148
|
+
input.schema_version === 3)
|
|
149
|
+
throw new TypeError(LEGACY_PROCESS_CAPTURE_MESSAGE);
|
|
150
|
+
const capture = processCaptureSchema.parse(input);
|
|
151
|
+
const issues = validateProcessCapture(capture);
|
|
152
|
+
if (issues.length > 0)
|
|
153
|
+
throw new TypeError(`Invalid Process Capture v4: ${issues.map(({ path, message }) => `${path}: ${message}`).join("; ")}`);
|
|
154
|
+
return capture;
|
|
155
|
+
};
|
|
350
156
|
export { compareProcessCaptures, comparisonStatusSchema, processCaptureComparisonSchema, } from "./processComparison.js";
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { digestProcessCommitment } from "./processScenario.js";
|
|
2
|
+
const orderedTimestamps = (values) => values.every((value, index) => index === 0 || value.at_ms >= values[index - 1].at_ms);
|
|
3
|
+
const validateCommitments = (capture, require) => {
|
|
4
|
+
const { manifest } = capture;
|
|
5
|
+
require(manifest.scenario.executable_sha256 ===
|
|
6
|
+
manifest.executable_sha256, "manifest.executable_sha256", "executable commitment does not match the scenario projection");
|
|
7
|
+
for (const [field, value] of [
|
|
8
|
+
["full_scenario_sha256", manifest.scenario],
|
|
9
|
+
["comparison_contract_sha256", manifest.comparison_contract],
|
|
10
|
+
["normalization_sha256", capture.normalization],
|
|
11
|
+
["shim_plan_sha256", manifest.shim_plan],
|
|
12
|
+
["replay_plan_sha256", manifest.replay_plan],
|
|
13
|
+
])
|
|
14
|
+
require(manifest[field] ===
|
|
15
|
+
digestProcessCommitment(value), `manifest.${field}`, "commitment does not match its canonical value");
|
|
16
|
+
require(Date.parse(manifest.started_at) <=
|
|
17
|
+
Date.parse(manifest.completed_at), "manifest.completed_at", "completion precedes start");
|
|
18
|
+
};
|
|
19
|
+
const validateOrdering = (capture, require) => {
|
|
20
|
+
for (const [name, values] of [
|
|
21
|
+
["frames", capture.frames],
|
|
22
|
+
["rendered_frames", capture.rendered_frames],
|
|
23
|
+
["interaction_events", capture.interaction_events],
|
|
24
|
+
["shim_events", capture.shim_events],
|
|
25
|
+
["protocol_events", capture.protocol_events],
|
|
26
|
+
])
|
|
27
|
+
require(values.every(({ sequence }, index) => sequence === index), name, "sequence values must be contiguous from zero");
|
|
28
|
+
for (const [name, values] of [
|
|
29
|
+
["frames", capture.frames],
|
|
30
|
+
["rendered_frames", capture.rendered_frames],
|
|
31
|
+
["process_samples", capture.process_samples],
|
|
32
|
+
["filesystem_checkpoints", capture.filesystem_checkpoints],
|
|
33
|
+
["shim_events", capture.shim_events],
|
|
34
|
+
["protocol_events", capture.protocol_events],
|
|
35
|
+
])
|
|
36
|
+
require(orderedTimestamps(values), name, "timestamps must be ordered");
|
|
37
|
+
};
|
|
38
|
+
const validateLifecycle = (capture, require) => {
|
|
39
|
+
const checkpointNames = capture.filesystem_checkpoints.map(({ name }) => name);
|
|
40
|
+
require(new Set(checkpointNames).size ===
|
|
41
|
+
checkpointNames.length, "filesystem_checkpoints", "checkpoint names must be unique");
|
|
42
|
+
require(checkpointNames[0] ===
|
|
43
|
+
"before", "filesystem_checkpoints", "first checkpoint must be before");
|
|
44
|
+
require(checkpointNames.at(-1) ===
|
|
45
|
+
"after_settlement", "filesystem_checkpoints", "last checkpoint must be after_settlement");
|
|
46
|
+
require(!capture.filesystem_checkpoints.some(({ truncated }) => truncated) ||
|
|
47
|
+
capture.truncated, "truncated", "checkpoint truncation must propagate to the capture");
|
|
48
|
+
require(capture.exit.reason === "exited" ||
|
|
49
|
+
capture.exit.code ===
|
|
50
|
+
null, "exit", "deadline termination cannot declare a normal exit code");
|
|
51
|
+
require(capture.settlement.state !== "quiesced" ||
|
|
52
|
+
capture.settlement.cleanup_outcome ===
|
|
53
|
+
"not_required", "settlement.cleanup_outcome", "quiesced settlement cannot require cleanup");
|
|
54
|
+
require(capture.settlement.state === "quiesced" ||
|
|
55
|
+
capture.settlement.cleanup_outcome !==
|
|
56
|
+
"not_required", "settlement.cleanup_outcome", "non-quiesced settlement must report cleanup");
|
|
57
|
+
};
|
|
58
|
+
const shimPlans = (capture) => capture.manifest.shim_plan.flatMap((shim) => typeof shim === "object" &&
|
|
59
|
+
shim !== null &&
|
|
60
|
+
"name" in shim &&
|
|
61
|
+
"routes" in shim &&
|
|
62
|
+
typeof shim.name === "string" &&
|
|
63
|
+
Array.isArray(shim.routes)
|
|
64
|
+
? shim.routes.map((route, routeIndex) => ({
|
|
65
|
+
command: shim.name,
|
|
66
|
+
route,
|
|
67
|
+
routeIndex,
|
|
68
|
+
}))
|
|
69
|
+
: []);
|
|
70
|
+
const validateShimEvents = (capture, require) => {
|
|
71
|
+
const plans = shimPlans(capture);
|
|
72
|
+
for (const event of capture.shim_events) {
|
|
73
|
+
const declared = plans.some(({ command, routeIndex }) => command === event.command && routeIndex === event.route_index);
|
|
74
|
+
require(event.outcome === "unmatched"
|
|
75
|
+
? event.route_index === null
|
|
76
|
+
: declared, "shim_events", "shim event has no declared route");
|
|
77
|
+
}
|
|
78
|
+
for (const plan of plans) {
|
|
79
|
+
const maximum = typeof plan.route === "object" &&
|
|
80
|
+
plan.route !== null &&
|
|
81
|
+
"max_calls" in plan.route &&
|
|
82
|
+
typeof plan.route.max_calls === "number"
|
|
83
|
+
? plan.route.max_calls
|
|
84
|
+
: 0;
|
|
85
|
+
const matches = capture.shim_events.filter((event) => event.command === plan.command &&
|
|
86
|
+
event.route_index === plan.routeIndex &&
|
|
87
|
+
event.outcome === "matched").length;
|
|
88
|
+
require(matches <=
|
|
89
|
+
maximum, "shim_events", "matched shim events exceed the declared route max_calls");
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const validateReplayEvents = (capture, require) => {
|
|
93
|
+
const { replay_plan: replayPlan } = capture.manifest;
|
|
94
|
+
const routes = "http" in replayPlan && Array.isArray(replayPlan.http)
|
|
95
|
+
? replayPlan.http
|
|
96
|
+
: [];
|
|
97
|
+
for (const event of capture.protocol_events) {
|
|
98
|
+
if (event.protocol !== "http" ||
|
|
99
|
+
event.direction !== "request" ||
|
|
100
|
+
event.outcome !== "matched")
|
|
101
|
+
continue;
|
|
102
|
+
const declared = routes.some((route) => typeof route === "object" &&
|
|
103
|
+
route !== null &&
|
|
104
|
+
"method" in route &&
|
|
105
|
+
route.method === event.method &&
|
|
106
|
+
"path" in route &&
|
|
107
|
+
route.path === event.path);
|
|
108
|
+
require(declared, "protocol_events", "matched HTTP event has no declared replay route");
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
/** Recompute v4 commitments and cross-field invariants without side effects. */
|
|
112
|
+
export const validateProcessCapture = (capture) => {
|
|
113
|
+
const issues = [];
|
|
114
|
+
const require = (condition, path, message) => {
|
|
115
|
+
if (!condition)
|
|
116
|
+
issues.push({ path, message });
|
|
117
|
+
};
|
|
118
|
+
validateCommitments(capture, require);
|
|
119
|
+
validateOrdering(capture, require);
|
|
120
|
+
validateLifecycle(capture, require);
|
|
121
|
+
validateShimEvents(capture, require);
|
|
122
|
+
validateReplayEvents(capture, require);
|
|
123
|
+
return issues;
|
|
124
|
+
};
|