rea-agents 1.7.0 → 2.0.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 +38 -31
- package/dist/application/CapabilityInventory.js +18 -0
- package/dist/application/CrossVersionInvestigation.js +10 -6
- package/dist/application/Doctor.js +56 -6
- package/dist/application/ElectronBoundaryAnalysis.js +96 -0
- package/dist/application/ElectronBoundaryGraph.js +9 -0
- package/dist/application/ElectronBoundaryGraphContext.js +62 -0
- package/dist/application/ElectronBoundaryGraphIpc.js +242 -0
- package/dist/application/ElectronBoundaryGraphNative.js +86 -0
- package/dist/application/ElectronBoundaryGraphWindows.js +199 -0
- package/dist/application/EnhancedTools.js +38 -13
- package/dist/application/InvestigationProviders.js +30 -0
- package/dist/application/JavaScriptApplicationEvidence.js +26 -0
- package/dist/application/JavaScriptApplicationEvidenceGraph.js +89 -0
- package/dist/application/JavaScriptApplicationService.js +74 -0
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +29 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +90 -0
- package/dist/application/JavaScriptArtifactAnalysis.js +7 -1
- package/dist/application/JavaScriptArtifactGraphBuilder.js +59 -16
- package/dist/application/JavaScriptArtifactGraphEvidence.js +24 -14
- package/dist/application/JavaScriptArtifactReconstruction.js +3 -0
- package/dist/application/JavaScriptArtifactReconstructionInput.js +2 -83
- package/dist/application/JavaScriptReplayPlanning.js +201 -0
- package/dist/application/JavaScriptReplayService.js +266 -0
- package/dist/application/JavaScriptRuntimeReconciliationEvidence.js +25 -0
- package/dist/application/JavaScriptRuntimeReconciliationService.js +27 -0
- package/dist/application/ManagedApplicationGraphService.js +55 -0
- package/dist/application/ManagedMemberComparisonService.js +122 -0
- package/dist/application/ManagedNativeVerificationService.js +39 -0
- package/dist/application/ManagedReconstructionService.js +58 -0
- package/dist/application/ManagedRuntimeCorrelationService.js +103 -0
- package/dist/application/PermissionAuthority.js +1 -1
- package/dist/application/ProcessSampling.js +15 -11
- package/dist/application/ProjectPermissionStore.js +1 -0
- package/dist/application/runtime.js +2 -0
- package/dist/browser/CdpCaptureEvents.js +49 -0
- package/dist/browser/CdpElectronInspection.js +106 -162
- package/dist/browser/CdpElectronProvider.js +5 -1
- package/dist/browser/CdpElectronScriptEvents.js +69 -0
- package/dist/browser/CdpElectronScripts.js +94 -0
- package/dist/browser/CdpElectronWorkers.js +56 -0
- package/dist/browser/CdpPageCapture.js +8 -4
- package/dist/catalogIdentity.js +4 -1
- package/dist/cli.js +388 -0
- package/dist/cliApplicationCommands.js +62 -0
- package/dist/cliCommandNames.js +13 -0
- package/dist/cliElectronCommands.js +103 -1
- package/dist/cliJsonInput.js +54 -0
- package/dist/config.js +83 -0
- package/dist/contracts/applicationToolContracts.js +91 -0
- package/dist/contracts/artifactToolContracts.js +5 -9
- package/dist/contracts/browserToolContracts.js +14 -62
- package/dist/contracts/electronToolContracts.js +45 -19
- package/dist/contracts/errorSchemas.js +1 -0
- package/dist/contracts/investigationExamples.js +0 -6
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +46 -0
- package/dist/contracts/javascriptRuntimeReconciliationExample.js +208 -0
- package/dist/contracts/managedToolContracts.js +153 -0
- package/dist/contracts/managedWorkflowExamples.js +346 -0
- package/dist/contracts/managedWorkflowToolContracts.js +203 -0
- package/dist/contracts/nativeToolContracts.js +2 -6
- package/dist/contracts/promptContracts.js +18 -2
- package/dist/contracts/toolContractExamples.js +10 -25
- package/dist/contracts/toolContracts.js +47 -45
- package/dist/contracts/toolEffects.js +160 -0
- package/dist/contracts/toolOutputSchemas.js +46 -31
- package/dist/doctorRuntime.js +42 -1
- package/dist/domain/artifactComparison.js +3 -4
- package/dist/domain/browserObservation.js +3 -0
- package/dist/domain/bundleComparison.js +4 -4
- package/dist/domain/electronObservation.js +13 -0
- package/dist/domain/electronStaticAnalysis.js +9 -0
- package/dist/domain/electronStaticAnalysisBrowser.js +194 -0
- package/dist/domain/electronStaticAnalysisIpc.js +171 -0
- package/dist/domain/electronStaticAnalysisNative.js +164 -0
- package/dist/domain/electronStaticAnalysisTypes.js +1 -0
- package/dist/domain/electronStaticAnalysisValues.js +103 -0
- package/dist/domain/errors.js +67 -1
- package/dist/domain/functionComparisonSchemas.js +3 -8
- package/dist/domain/inputIssueProjection.js +52 -0
- package/dist/domain/javascriptApplicationAnalysis.js +155 -0
- package/dist/domain/javascriptApplicationChangeGraph.js +231 -0
- package/dist/domain/javascriptApplicationEvidenceSchemas.js +18 -0
- package/dist/domain/javascriptApplicationGraph.js +2 -1
- package/dist/domain/javascriptApplicationGraphSchemas.js +7 -0
- package/dist/domain/javascriptApplicationVersionComparison.js +116 -0
- package/dist/domain/javascriptApplicationVersionComparisonSchemas.js +139 -0
- package/dist/domain/javascriptApplicationVersionItems.js +221 -0
- package/dist/domain/javascriptApplicationVersionKeys.js +170 -0
- package/dist/domain/javascriptFeatureSeed.js +140 -0
- package/dist/domain/javascriptFeatureTrace.js +232 -0
- package/dist/domain/javascriptFeatureTraceSchemas.js +136 -0
- package/dist/domain/javascriptFeatureTraversal.js +88 -0
- package/dist/domain/javascriptNativeHandoff.js +85 -0
- package/dist/domain/javascriptReplay.js +361 -0
- package/dist/domain/javascriptRuntimeLoadState.js +106 -0
- package/dist/domain/javascriptRuntimeReconciliation.js +42 -0
- package/dist/domain/javascriptRuntimeReconciliationGraph.js +169 -0
- package/dist/domain/javascriptRuntimeReconciliationMatching.js +247 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +160 -0
- package/dist/domain/javascriptRuntimeReconciliationResult.js +119 -0
- package/dist/domain/javascriptRuntimeReconciliationRuntime.js +316 -0
- package/dist/domain/javascriptRuntimeReconciliationSchemas.js +245 -0
- package/dist/domain/javascriptRuntimeStaticCandidates.js +191 -0
- package/dist/domain/javascriptStaticAnalysis.js +31 -43
- package/dist/domain/javascriptStaticAnalysisFindings.js +39 -0
- package/dist/domain/javascriptStaticAnalysisHelpers.js +10 -0
- package/dist/domain/javascriptStaticAnalysisState.js +6 -0
- package/dist/domain/managedApplicationGraph.js +556 -0
- package/dist/domain/managedArtifact.js +444 -0
- package/dist/domain/managedMemberComparison.js +565 -0
- package/dist/domain/managedNativeVerification.js +408 -0
- package/dist/domain/managedReconstruction.js +183 -0
- package/dist/domain/managedRuntimeCorrelation.js +175 -0
- package/dist/domain/reconstructionVerificationSchemas.js +1 -3
- package/dist/dotnet/ManagedArtifactInspector.js +318 -0
- package/dist/dotnet/ManagedMemberInspector.js +887 -0
- package/dist/dotnet/ManagedMetadataHeaps.js +131 -0
- package/dist/dotnet/ManagedMetadataInventory.js +368 -0
- package/dist/dotnet/ManagedMetadataLayout.js +322 -0
- package/dist/dotnet/ManagedNativeBoundaryInspector.js +391 -0
- package/dist/dotnet/ManagedPeReader.js +157 -0
- package/dist/dotnet/ManagedReaderFailure.js +10 -0
- package/dist/dotnet/ManagedStaticProvider.js +203 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/ghidra/GhidraProvider.js +6 -0
- package/dist/main.js +14 -0
- package/dist/replay/JavaScriptReplayWorker.js +286 -0
- package/dist/replay/LinuxJavaScriptReplayRunner.js +434 -0
- package/dist/replay/LinuxRuntimeClosure.js +46 -0
- package/dist/replay/LinuxSeccompPolicy.js +40 -0
- package/dist/replay/ReplayWorkerProtocol.js +55 -0
- package/dist/replay/SystemJavaScriptReplayHost.js +182 -0
- package/dist/server/createServer.js +52 -0
- package/dist/server/registerApplicationTools.js +115 -0
- package/dist/server/registerArtifactComparisonTool.js +16 -9
- package/dist/server/registerBrowserTools.js +33 -8
- package/dist/server/registerBundleComparisonTool.js +17 -20
- package/dist/server/registerElectronTools.js +45 -3
- package/dist/server/registerEnhancedTools.js +58 -13
- package/dist/server/registerEvidenceTools.js +5 -1
- package/dist/server/registerFunctionComparisonTool.js +14 -7
- package/dist/server/registerInvestigationTools.js +21 -17
- package/dist/server/registerManagedTools.js +6 -0
- package/dist/server/registerManagedWorkflowTools.js +277 -0
- package/dist/server/registerOfficialTools.js +8 -8
- package/dist/server/registerProcessComparisonTool.js +25 -47
- package/dist/server/registerSessionStatusTool.js +7 -7
- package/dist/server/registerSessionTools.js +51 -86
- package/dist/server/sessionEvidence.js +14 -19
- package/dist/server/toolInputValidation.js +77 -0
- package/dist/server/toolRegistrationOptions.js +79 -2
- package/dist/server/toolResult.js +25 -17
- package/package.json +12 -4
- package/scripts/rea.mjs +1 -1
- package/skills/rea-analysis/SKILL.md +59 -3
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { open, readFile } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { digestBytes, digestJson, } from "../application/JavaScriptReplayPlanning.js";
|
|
6
|
+
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
7
|
+
import { buildLinuxX64ReplaySeccomp, linuxX64ReplaySeccompDigest, } from "./LinuxSeccompPolicy.js";
|
|
8
|
+
import { resolveLinuxRuntimeClosure } from "./LinuxRuntimeClosure.js";
|
|
9
|
+
import { parseReplayWorkerResponse, } from "./ReplayWorkerProtocol.js";
|
|
10
|
+
const isAborted = (signal) => signal?.aborted === true;
|
|
11
|
+
/** Run one prepared experiment under the exact Linux ADR-0002 boundary. */
|
|
12
|
+
export class LinuxJavaScriptReplayRunner {
|
|
13
|
+
async execute(prepared, policy, signal) {
|
|
14
|
+
if (isAborted(signal))
|
|
15
|
+
return terminationResult(prepared, "", "cancelled", {
|
|
16
|
+
state: "complete",
|
|
17
|
+
residual_resources: [],
|
|
18
|
+
});
|
|
19
|
+
const unit = `rea-replay-${randomBytes(8).toString("hex")}.service`;
|
|
20
|
+
const workerPath = prepared.publicPlan.runtime.worker.path;
|
|
21
|
+
const closure = await resolveLinuxRuntimeClosure(policy.nodePath);
|
|
22
|
+
assertRuntimeCommitment(prepared, closure);
|
|
23
|
+
if (digestBytes(await readFile(workerPath)) !==
|
|
24
|
+
prepared.publicPlan.runtime.worker.sha256)
|
|
25
|
+
throw new TypeError("Replay worker changed after approval");
|
|
26
|
+
if (linuxX64ReplaySeccompDigest() !==
|
|
27
|
+
prepared.publicPlan.sandbox.seccomp_sha256)
|
|
28
|
+
throw new TypeError("Replay seccomp policy changed after approval");
|
|
29
|
+
const request = workerRequest(prepared);
|
|
30
|
+
const encoded = Buffer.from(JSON.stringify(request));
|
|
31
|
+
if (encoded.byteLength > prepared.publicPlan.limits.protocol_bytes)
|
|
32
|
+
throw new RangeError("Replay worker protocol input exceeds the committed limit");
|
|
33
|
+
const filter = await temporaryFilterHandle();
|
|
34
|
+
const arguments_ = systemdArguments(unit, policy, prepared, workerPath, closure, filter.path);
|
|
35
|
+
let child;
|
|
36
|
+
let timeout;
|
|
37
|
+
let cancelled = false;
|
|
38
|
+
let timedOut = false;
|
|
39
|
+
let terminationRequest;
|
|
40
|
+
const requestTermination = () => {
|
|
41
|
+
terminationRequest ??= killUnit(policy.systemctlPath, unit);
|
|
42
|
+
child?.kill("SIGKILL");
|
|
43
|
+
};
|
|
44
|
+
const terminate = () => {
|
|
45
|
+
cancelled = true;
|
|
46
|
+
requestTermination();
|
|
47
|
+
};
|
|
48
|
+
try {
|
|
49
|
+
const completion = new Promise((resolve, reject) => {
|
|
50
|
+
child = spawn(policy.systemdRunPath, arguments_, {
|
|
51
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
52
|
+
env: {
|
|
53
|
+
PATH: "/usr/bin:/bin",
|
|
54
|
+
LANG: "C",
|
|
55
|
+
LC_ALL: "C",
|
|
56
|
+
TZ: "UTC",
|
|
57
|
+
...(process.env.XDG_RUNTIME_DIR === undefined
|
|
58
|
+
? {}
|
|
59
|
+
: { XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR }),
|
|
60
|
+
...(process.env.DBUS_SESSION_BUS_ADDRESS === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: {
|
|
63
|
+
DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS,
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const output = boundedCollector(prepared.publicPlan.limits.output_bytes);
|
|
68
|
+
const diagnostics = boundedCollector(prepared.publicPlan.limits.stderr_bytes);
|
|
69
|
+
child.stdout?.on("data", output.append);
|
|
70
|
+
child.stderr?.on("data", diagnostics.append);
|
|
71
|
+
child.once("error", reject);
|
|
72
|
+
child.once("close", (code, signalName) => resolve({
|
|
73
|
+
code,
|
|
74
|
+
signal: signalName,
|
|
75
|
+
stdout: output.value(),
|
|
76
|
+
stderr: diagnostics.value(),
|
|
77
|
+
outputExceeded: output.exceeded(),
|
|
78
|
+
}));
|
|
79
|
+
child.stdin?.end(encoded);
|
|
80
|
+
});
|
|
81
|
+
signal?.addEventListener("abort", terminate, { once: true });
|
|
82
|
+
if (isAborted(signal))
|
|
83
|
+
terminate();
|
|
84
|
+
timeout = setTimeout(() => {
|
|
85
|
+
timedOut = true;
|
|
86
|
+
requestTermination();
|
|
87
|
+
}, prepared.publicPlan.limits.wall_time_ms);
|
|
88
|
+
timeout.unref();
|
|
89
|
+
const collected = await completion;
|
|
90
|
+
if (terminationRequest !== undefined)
|
|
91
|
+
await terminationRequest;
|
|
92
|
+
const unitResult = await observeUnitResult(policy.systemctlPath, unit);
|
|
93
|
+
const cleanup = await observeCleanup(policy.systemctlPath, unit);
|
|
94
|
+
if (cancelled || timedOut)
|
|
95
|
+
return terminationResult(prepared, collected.stderr, cancelled ? "cancelled" : "timeout", cleanup);
|
|
96
|
+
if (collected.outputExceeded)
|
|
97
|
+
return terminationResult(prepared, collected.stderr, "protocol_error", cleanup, "Worker stdout exceeded the committed bound");
|
|
98
|
+
if (collected.code !== 0)
|
|
99
|
+
return terminationResult(prepared, collected.stderr, collected.code === 137 || unitResult === "oom-kill" ? "oom" : "crash", cleanup);
|
|
100
|
+
let response;
|
|
101
|
+
try {
|
|
102
|
+
response = parseReplayWorkerResponse(JSON.parse(collected.stdout), prepared.publicPlan.cases, prepared.publicPlan.right !== undefined);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return terminationResult(prepared, collected.stderr, "protocol_error", cleanup);
|
|
106
|
+
}
|
|
107
|
+
const outcomes = commitOutcomes(response.left);
|
|
108
|
+
const right = response.right === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: commitOutcomes(response.right);
|
|
111
|
+
return {
|
|
112
|
+
schema_version: 1,
|
|
113
|
+
plan_digest: prepared.publicPlan.plan_digest,
|
|
114
|
+
outcomes: [...outcomes, ...(right ?? [])],
|
|
115
|
+
...(right === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: { comparison: compareOutcomes(outcomes, right) }),
|
|
118
|
+
stderr: collected.stderr,
|
|
119
|
+
termination: "completed",
|
|
120
|
+
cleanup,
|
|
121
|
+
limitations: [
|
|
122
|
+
"Controlled replay observes the isolated extracted modules, not the original application runtime.",
|
|
123
|
+
"Network and host filesystem access were unavailable; unsupported dependencies may change behavior.",
|
|
124
|
+
],
|
|
125
|
+
reproducer: null,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
signal?.removeEventListener("abort", terminate);
|
|
130
|
+
if (timeout !== undefined)
|
|
131
|
+
clearTimeout(timeout);
|
|
132
|
+
if (child !== undefined && child.exitCode === null) {
|
|
133
|
+
requestTermination();
|
|
134
|
+
await terminationRequest;
|
|
135
|
+
}
|
|
136
|
+
await filter.close();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const workerRequest = (prepared) => ({
|
|
141
|
+
schemaVersion: 1,
|
|
142
|
+
left: workerSide(prepared.publicPlan.left, prepared.leftSources),
|
|
143
|
+
...(prepared.publicPlan.right === undefined ||
|
|
144
|
+
prepared.rightSources === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { right: workerSide(prepared.publicPlan.right, prepared.rightSources) }),
|
|
147
|
+
cases: prepared.publicPlan.cases.map((item) => ({
|
|
148
|
+
caseId: item.case_id,
|
|
149
|
+
arguments: item.arguments,
|
|
150
|
+
inputSha256: item.sha256,
|
|
151
|
+
})),
|
|
152
|
+
determinism: {
|
|
153
|
+
clockIso: prepared.publicPlan.determinism.clock_iso,
|
|
154
|
+
randomSeed: prepared.publicPlan.determinism.random_seed,
|
|
155
|
+
},
|
|
156
|
+
limits: {
|
|
157
|
+
resultDepth: prepared.publicPlan.limits.result_depth,
|
|
158
|
+
resultNodes: prepared.publicPlan.limits.result_nodes,
|
|
159
|
+
exceptionBytes: Math.min(64 * 1024, prepared.publicPlan.limits.output_bytes),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
const workerSide = (side, sources) => ({
|
|
163
|
+
modules: side.modules.map((module) => ({
|
|
164
|
+
alias: module.alias,
|
|
165
|
+
format: module.format,
|
|
166
|
+
dependencies: module.dependencies,
|
|
167
|
+
source: sources[module.alias] ?? "",
|
|
168
|
+
})),
|
|
169
|
+
entryAlias: side.entry_alias,
|
|
170
|
+
entryExport: side.entry_export,
|
|
171
|
+
});
|
|
172
|
+
const systemdArguments = (unit, policy, prepared, workerPath, closure, filterPath) => {
|
|
173
|
+
const limits = prepared.publicPlan.limits;
|
|
174
|
+
const descriptorPaths = [
|
|
175
|
+
filterPath,
|
|
176
|
+
...closure.map(({ sourcePath }) => sourcePath),
|
|
177
|
+
workerPath,
|
|
178
|
+
];
|
|
179
|
+
const workerFd = descriptorPaths.length + 2;
|
|
180
|
+
const wrapper = descriptorPaths
|
|
181
|
+
.map((_, index) => `exec ${String(index + 3)}<"$1"; shift`)
|
|
182
|
+
.concat('exec "$@"')
|
|
183
|
+
.join("\n");
|
|
184
|
+
return [
|
|
185
|
+
"--user",
|
|
186
|
+
"--pipe",
|
|
187
|
+
"--wait",
|
|
188
|
+
"--service-type=exec",
|
|
189
|
+
"--quiet",
|
|
190
|
+
`--unit=${unit}`,
|
|
191
|
+
`--property=MemoryMax=${String(limits.memory_bytes)}`,
|
|
192
|
+
"--property=MemorySwapMax=0",
|
|
193
|
+
`--property=TasksMax=${String(limits.tasks)}`,
|
|
194
|
+
`--property=CPUQuota=${String(limits.cpu_quota_percent)}%`,
|
|
195
|
+
"--property=KillMode=control-group",
|
|
196
|
+
"--property=SendSIGKILL=yes",
|
|
197
|
+
"--",
|
|
198
|
+
policy.shellPath,
|
|
199
|
+
"-c",
|
|
200
|
+
wrapper,
|
|
201
|
+
"rea-replay-wrapper",
|
|
202
|
+
...descriptorPaths,
|
|
203
|
+
policy.bubblewrapPath,
|
|
204
|
+
"--unshare-all",
|
|
205
|
+
"--unshare-user",
|
|
206
|
+
"--disable-userns",
|
|
207
|
+
"--new-session",
|
|
208
|
+
"--die-with-parent",
|
|
209
|
+
"--cap-drop",
|
|
210
|
+
"ALL",
|
|
211
|
+
"--clearenv",
|
|
212
|
+
"--setenv",
|
|
213
|
+
"HOME",
|
|
214
|
+
"/work",
|
|
215
|
+
"--setenv",
|
|
216
|
+
"TMPDIR",
|
|
217
|
+
"/tmp",
|
|
218
|
+
"--setenv",
|
|
219
|
+
"TZ",
|
|
220
|
+
"UTC",
|
|
221
|
+
"--setenv",
|
|
222
|
+
"LANG",
|
|
223
|
+
"C",
|
|
224
|
+
"--setenv",
|
|
225
|
+
"LC_ALL",
|
|
226
|
+
"C",
|
|
227
|
+
"--tmpfs",
|
|
228
|
+
"/",
|
|
229
|
+
"--dir",
|
|
230
|
+
"/runtime",
|
|
231
|
+
"--dir",
|
|
232
|
+
"/work",
|
|
233
|
+
"--size",
|
|
234
|
+
String(limits.tmpfs_bytes),
|
|
235
|
+
"--tmpfs",
|
|
236
|
+
"/tmp",
|
|
237
|
+
...bindArguments(closure),
|
|
238
|
+
"--ro-bind-fd",
|
|
239
|
+
String(workerFd),
|
|
240
|
+
"/runtime/worker.js",
|
|
241
|
+
"--proc",
|
|
242
|
+
"/proc",
|
|
243
|
+
"--dev",
|
|
244
|
+
"/dev",
|
|
245
|
+
"--remount-ro",
|
|
246
|
+
"/",
|
|
247
|
+
"--chdir",
|
|
248
|
+
"/work",
|
|
249
|
+
"--seccomp",
|
|
250
|
+
"3",
|
|
251
|
+
"--",
|
|
252
|
+
"/runtime/node",
|
|
253
|
+
`--max-old-space-size=${String(Math.max(8, Math.floor(limits.memory_bytes / 1024 / 1024 / 2)))}`,
|
|
254
|
+
"--experimental-vm-modules",
|
|
255
|
+
"--permission",
|
|
256
|
+
"--allow-fs-read=/runtime/worker.js",
|
|
257
|
+
"/runtime/worker.js",
|
|
258
|
+
];
|
|
259
|
+
};
|
|
260
|
+
const bindArguments = (closure) => {
|
|
261
|
+
const arguments_ = [];
|
|
262
|
+
const directories = new Set();
|
|
263
|
+
for (const [index, item] of closure.entries()) {
|
|
264
|
+
const destination = item.destinationPath;
|
|
265
|
+
let parent = dirname(destination);
|
|
266
|
+
const chain = [];
|
|
267
|
+
while (parent !== "/" && !directories.has(parent)) {
|
|
268
|
+
chain.push(parent);
|
|
269
|
+
parent = dirname(parent);
|
|
270
|
+
}
|
|
271
|
+
for (const directory of chain.reverse()) {
|
|
272
|
+
directories.add(directory);
|
|
273
|
+
arguments_.push("--dir", directory);
|
|
274
|
+
}
|
|
275
|
+
arguments_.push("--ro-bind-fd", String(index + 4), destination);
|
|
276
|
+
}
|
|
277
|
+
return arguments_;
|
|
278
|
+
};
|
|
279
|
+
const assertRuntimeCommitment = (prepared, closure) => {
|
|
280
|
+
const observed = closure.map((item) => ({
|
|
281
|
+
source_path: item.sourcePath,
|
|
282
|
+
destination_path: item.destinationPath,
|
|
283
|
+
sha256: item.sha256,
|
|
284
|
+
}));
|
|
285
|
+
if (digestJson(observed) !==
|
|
286
|
+
digestJson(prepared.publicPlan.runtime.read_only_files))
|
|
287
|
+
throw new TypeError("Replay runtime closure changed after approval");
|
|
288
|
+
};
|
|
289
|
+
const temporaryFilterHandle = async () => {
|
|
290
|
+
const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
|
|
291
|
+
const { tmpdir } = await import("node:os");
|
|
292
|
+
const { join } = await import("node:path");
|
|
293
|
+
const directory = await mkdtemp(join(tmpdir(), "rea-replay-filter-"));
|
|
294
|
+
const path = join(directory, "seccomp.bpf");
|
|
295
|
+
await writeFile(path, buildLinuxX64ReplaySeccomp(), { mode: 0o600 });
|
|
296
|
+
const handle = await open(path, "r");
|
|
297
|
+
return {
|
|
298
|
+
path,
|
|
299
|
+
handle,
|
|
300
|
+
close: async () => {
|
|
301
|
+
await handle.close();
|
|
302
|
+
await rm(directory, { recursive: true, force: true });
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
};
|
|
306
|
+
const boundedCollector = (maximum) => {
|
|
307
|
+
const chunks = [];
|
|
308
|
+
let bytes = 0;
|
|
309
|
+
let overflow = false;
|
|
310
|
+
return {
|
|
311
|
+
append: (chunk) => {
|
|
312
|
+
const retained = chunk.subarray(0, Math.max(0, maximum - bytes));
|
|
313
|
+
if (retained.byteLength > 0)
|
|
314
|
+
chunks.push(retained);
|
|
315
|
+
bytes += retained.byteLength;
|
|
316
|
+
if (retained.byteLength < chunk.byteLength)
|
|
317
|
+
overflow = true;
|
|
318
|
+
},
|
|
319
|
+
value: () => Buffer.concat(chunks).toString("utf8"),
|
|
320
|
+
exceeded: () => overflow,
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
const collect = async (executable, arguments_, maximum) => new Promise((resolve, reject) => {
|
|
324
|
+
const child = spawn(executable, [...arguments_], {
|
|
325
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
326
|
+
});
|
|
327
|
+
const stdout = boundedCollector(maximum);
|
|
328
|
+
const stderr = boundedCollector(maximum);
|
|
329
|
+
const timeout = setTimeout(() => child.kill("SIGKILL"), 5_000);
|
|
330
|
+
timeout.unref();
|
|
331
|
+
child.stdout?.on("data", stdout.append);
|
|
332
|
+
child.stderr?.on("data", stderr.append);
|
|
333
|
+
child.once("error", (error) => {
|
|
334
|
+
clearTimeout(timeout);
|
|
335
|
+
reject(error);
|
|
336
|
+
});
|
|
337
|
+
child.once("close", (code) => {
|
|
338
|
+
clearTimeout(timeout);
|
|
339
|
+
resolve({ code, stdout: stdout.value(), stderr: stderr.value() });
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
const killUnit = async (systemctl, unit) => {
|
|
343
|
+
try {
|
|
344
|
+
await collect(systemctl, ["--user", "kill", "--kill-whom=all", "--signal=SIGKILL", unit], 4096);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
/* cleanup continues */
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
const observeCleanup = async (systemctl, unit) => {
|
|
351
|
+
try {
|
|
352
|
+
const state = await collect(systemctl, ["--user", "show", "--property=ActiveState", "--value", unit], 4096);
|
|
353
|
+
if (state.code !== 0 ||
|
|
354
|
+
state.stdout.trim() === "inactive" ||
|
|
355
|
+
state.stdout.trim() === "failed" ||
|
|
356
|
+
state.stdout.trim().length === 0) {
|
|
357
|
+
await collect(systemctl, ["--user", "reset-failed", unit], 4096);
|
|
358
|
+
return { state: "complete", residual_resources: [] };
|
|
359
|
+
}
|
|
360
|
+
return { state: "incomplete", residual_resources: [unit] };
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return { state: "incomplete", residual_resources: [unit] };
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const observeUnitResult = async (systemctl, unit) => {
|
|
367
|
+
try {
|
|
368
|
+
const result = await collect(systemctl, ["--user", "show", "--property=Result", "--value", unit], 4096);
|
|
369
|
+
return result.stdout.trim();
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return "unknown";
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
const commitOutcomes = (outcomes) => outcomes.map((outcome) => {
|
|
376
|
+
const semantic = outcome.value === undefined ? (outcome.exception ?? null) : outcome.value;
|
|
377
|
+
return {
|
|
378
|
+
case_id: outcome.case_id,
|
|
379
|
+
outcome: outcome.outcome,
|
|
380
|
+
...(outcome.value === undefined
|
|
381
|
+
? {}
|
|
382
|
+
: { value: jsonValueSchema.parse(outcome.value) }),
|
|
383
|
+
...(outcome.exception === undefined
|
|
384
|
+
? {}
|
|
385
|
+
: { exception: outcome.exception }),
|
|
386
|
+
input_sha256: outcome.input_sha256,
|
|
387
|
+
output_sha256: digestJson(semantic),
|
|
388
|
+
truncated: false,
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
const compareOutcomes = (left, right) => left.map((item, index) => ({
|
|
392
|
+
case_id: item.case_id,
|
|
393
|
+
status: right[index] === undefined
|
|
394
|
+
? "unknown"
|
|
395
|
+
: item.outcome === right[index].outcome &&
|
|
396
|
+
item.output_sha256 === right[index].output_sha256
|
|
397
|
+
? "equal"
|
|
398
|
+
: "changed",
|
|
399
|
+
left_index: index,
|
|
400
|
+
right_index: index,
|
|
401
|
+
}));
|
|
402
|
+
const terminationResult = (prepared, stderr, termination, cleanup, limitation) => {
|
|
403
|
+
const outcomes = prepared.publicPlan.cases.map((item) => ({
|
|
404
|
+
case_id: item.case_id,
|
|
405
|
+
outcome: termination,
|
|
406
|
+
input_sha256: item.sha256,
|
|
407
|
+
output_sha256: null,
|
|
408
|
+
truncated: termination === "protocol_error",
|
|
409
|
+
}));
|
|
410
|
+
const differential = prepared.publicPlan.right !== undefined;
|
|
411
|
+
return {
|
|
412
|
+
schema_version: 1,
|
|
413
|
+
plan_digest: prepared.publicPlan.plan_digest,
|
|
414
|
+
outcomes: [...outcomes, ...(differential ? outcomes : [])],
|
|
415
|
+
...(differential
|
|
416
|
+
? {
|
|
417
|
+
comparison: outcomes.map((item, index) => ({
|
|
418
|
+
case_id: item.case_id,
|
|
419
|
+
status: "unknown",
|
|
420
|
+
left_index: index,
|
|
421
|
+
right_index: index,
|
|
422
|
+
})),
|
|
423
|
+
}
|
|
424
|
+
: {}),
|
|
425
|
+
stderr,
|
|
426
|
+
termination,
|
|
427
|
+
cleanup,
|
|
428
|
+
limitations: [
|
|
429
|
+
limitation ??
|
|
430
|
+
`Replay terminated with ${termination}; the requested functional result remains unknown.`,
|
|
431
|
+
],
|
|
432
|
+
reproducer: null,
|
|
433
|
+
};
|
|
434
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
4
|
+
const LDD_OUTPUT_LIMIT = 64 * 1024;
|
|
5
|
+
/** Resolve and content-address the exact ELF closure mounted into a replay. */
|
|
6
|
+
export const resolveLinuxRuntimeClosure = async (nodePath) => {
|
|
7
|
+
const canonicalNode = await realpath(nodePath);
|
|
8
|
+
const collected = await collectLdd(canonicalNode);
|
|
9
|
+
if (collected.code !== 0)
|
|
10
|
+
throw new TypeError("Node runtime dependency closure could not be resolved");
|
|
11
|
+
const paths = [...collected.stdout.matchAll(/(?:=>\s+)?(\/[^\s()]+)/gu)]
|
|
12
|
+
.map((match) => match[1])
|
|
13
|
+
.filter((value) => value !== undefined);
|
|
14
|
+
const libraries = await Promise.all([...new Set(paths)].map(async (path) => {
|
|
15
|
+
const sourcePath = await realpath(path);
|
|
16
|
+
return identity(sourcePath, path);
|
|
17
|
+
}));
|
|
18
|
+
return [await identity(canonicalNode, "/runtime/node"), ...libraries];
|
|
19
|
+
};
|
|
20
|
+
const identity = async (sourcePath, destinationPath) => ({
|
|
21
|
+
sourcePath,
|
|
22
|
+
destinationPath,
|
|
23
|
+
sha256: createHash("sha256")
|
|
24
|
+
.update(await readFile(sourcePath))
|
|
25
|
+
.digest("hex"),
|
|
26
|
+
});
|
|
27
|
+
const collectLdd = async (path) => new Promise((resolve, reject) => {
|
|
28
|
+
const child = spawn("/usr/bin/ldd", [path], {
|
|
29
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
30
|
+
});
|
|
31
|
+
const timeout = setTimeout(() => child.kill("SIGKILL"), 5_000);
|
|
32
|
+
timeout.unref();
|
|
33
|
+
let stdout = "";
|
|
34
|
+
child.stdout.on("data", (chunk) => {
|
|
35
|
+
const remaining = Math.max(0, LDD_OUTPUT_LIMIT - Buffer.byteLength(stdout));
|
|
36
|
+
stdout += chunk.subarray(0, remaining).toString("utf8");
|
|
37
|
+
});
|
|
38
|
+
child.once("error", (error) => {
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
reject(error);
|
|
41
|
+
});
|
|
42
|
+
child.once("close", (code) => {
|
|
43
|
+
clearTimeout(timeout);
|
|
44
|
+
resolve({ code, stdout });
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const BPF_LD_W_ABS = 0x20;
|
|
3
|
+
const BPF_JMP_JEQ_K = 0x15;
|
|
4
|
+
const BPF_RET_K = 0x06;
|
|
5
|
+
const SECCOMP_RET_KILL_PROCESS = 0x8000_0000;
|
|
6
|
+
const SECCOMP_RET_ERRNO = 0x0005_0000 | 1;
|
|
7
|
+
const SECCOMP_RET_ALLOW = 0x7fff_0000;
|
|
8
|
+
const AUDIT_ARCH_X86_64 = 0xc000_003e;
|
|
9
|
+
const IOCTL = 16;
|
|
10
|
+
const TIOCSTI = 0x5412;
|
|
11
|
+
const DENIED_X86_64_SYSCALLS = [
|
|
12
|
+
101, 103, 155, 161, 163, 165, 166, 167, 168, 169, 172, 173, 175, 176, 179,
|
|
13
|
+
212, 246, 248, 249, 250, 272, 298, 303, 304, 308, 310, 311, 312, 313, 320,
|
|
14
|
+
321, 323, 425, 426, 427, 428, 429, 430, 431, 432, 433, 442,
|
|
15
|
+
];
|
|
16
|
+
/** Build the architecture-checked deny filter committed by ADR-0002. */
|
|
17
|
+
export const buildLinuxX64ReplaySeccomp = () => {
|
|
18
|
+
const instructions = [
|
|
19
|
+
{ code: BPF_LD_W_ABS, jt: 0, jf: 0, k: 4 },
|
|
20
|
+
{ code: BPF_JMP_JEQ_K, jt: 1, jf: 0, k: AUDIT_ARCH_X86_64 },
|
|
21
|
+
{ code: BPF_RET_K, jt: 0, jf: 0, k: SECCOMP_RET_KILL_PROCESS },
|
|
22
|
+
{ code: BPF_LD_W_ABS, jt: 0, jf: 0, k: 0 },
|
|
23
|
+
];
|
|
24
|
+
for (const syscall of DENIED_X86_64_SYSCALLS) {
|
|
25
|
+
instructions.push({ code: BPF_JMP_JEQ_K, jt: 0, jf: 1, k: syscall });
|
|
26
|
+
instructions.push({ code: BPF_RET_K, jt: 0, jf: 0, k: SECCOMP_RET_ERRNO });
|
|
27
|
+
}
|
|
28
|
+
instructions.push({ code: BPF_JMP_JEQ_K, jt: 0, jf: 3, k: IOCTL }, { code: BPF_LD_W_ABS, jt: 0, jf: 0, k: 24 }, { code: BPF_JMP_JEQ_K, jt: 0, jf: 1, k: TIOCSTI }, { code: BPF_RET_K, jt: 0, jf: 0, k: SECCOMP_RET_ERRNO }, { code: BPF_RET_K, jt: 0, jf: 0, k: SECCOMP_RET_ALLOW });
|
|
29
|
+
const encoded = new Uint8Array(instructions.length * 8);
|
|
30
|
+
const view = new DataView(encoded.buffer);
|
|
31
|
+
instructions.forEach((instruction, index) => {
|
|
32
|
+
const offset = index * 8;
|
|
33
|
+
view.setUint16(offset, instruction.code, true);
|
|
34
|
+
view.setUint8(offset + 2, instruction.jt);
|
|
35
|
+
view.setUint8(offset + 3, instruction.jf);
|
|
36
|
+
view.setUint32(offset + 4, instruction.k, true);
|
|
37
|
+
});
|
|
38
|
+
return encoded;
|
|
39
|
+
};
|
|
40
|
+
export const linuxX64ReplaySeccompDigest = () => createHash("sha256").update(buildLinuxX64ReplaySeccomp()).digest("hex");
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const workerOutcomeSchema = z
|
|
3
|
+
.object({
|
|
4
|
+
case_id: z.string(),
|
|
5
|
+
outcome: z.enum(["return", "exception", "serialization_error", "denied"]),
|
|
6
|
+
value: z.json().optional(),
|
|
7
|
+
exception: z
|
|
8
|
+
.object({
|
|
9
|
+
name: z.string(),
|
|
10
|
+
message: z.string(),
|
|
11
|
+
stack: z.string().nullable(),
|
|
12
|
+
})
|
|
13
|
+
.strict()
|
|
14
|
+
.optional(),
|
|
15
|
+
input_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
16
|
+
output_sha256: z.null(),
|
|
17
|
+
truncated: z.literal(false),
|
|
18
|
+
})
|
|
19
|
+
.strict()
|
|
20
|
+
.superRefine((value, context) => {
|
|
21
|
+
if (value.outcome === "return" && value.value === undefined)
|
|
22
|
+
context.addIssue({ code: "custom", message: "Return value is missing" });
|
|
23
|
+
if (value.outcome !== "return" && value.exception === undefined)
|
|
24
|
+
context.addIssue({ code: "custom", message: "Exception is missing" });
|
|
25
|
+
if (value.outcome === "return" && value.exception !== undefined)
|
|
26
|
+
context.addIssue({ code: "custom", message: "Return has an exception" });
|
|
27
|
+
});
|
|
28
|
+
const workerResponseSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
schema_version: z.literal(1),
|
|
31
|
+
left: z.array(workerOutcomeSchema),
|
|
32
|
+
right: z.array(workerOutcomeSchema).optional(),
|
|
33
|
+
})
|
|
34
|
+
.strict();
|
|
35
|
+
/** Strictly authenticate case ordering and commitments in worker output. */
|
|
36
|
+
export const parseReplayWorkerResponse = (rawResponse, cases, differential) => {
|
|
37
|
+
const response = workerResponseSchema.parse(rawResponse);
|
|
38
|
+
if ((response.right !== undefined) !== differential)
|
|
39
|
+
throw new TypeError("Replay worker differential response is incomplete");
|
|
40
|
+
validateOutcomeSequence(response.left, cases);
|
|
41
|
+
if (response.right !== undefined)
|
|
42
|
+
validateOutcomeSequence(response.right, cases);
|
|
43
|
+
return response;
|
|
44
|
+
};
|
|
45
|
+
const validateOutcomeSequence = (outcomes, cases) => {
|
|
46
|
+
if (outcomes.length !== cases.length)
|
|
47
|
+
throw new TypeError("Replay worker response case count changed");
|
|
48
|
+
outcomes.forEach((outcome, index) => {
|
|
49
|
+
const expected = cases[index];
|
|
50
|
+
if (expected === undefined ||
|
|
51
|
+
outcome.case_id !== expected.case_id ||
|
|
52
|
+
outcome.input_sha256 !== expected.sha256)
|
|
53
|
+
throw new TypeError("Replay worker response case identity changed");
|
|
54
|
+
});
|
|
55
|
+
};
|