rea-agents 2.2.0 → 2.3.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 +56 -9
- package/dist/application/ApplicationWorkflowEvidenceResolver.js +162 -0
- package/dist/application/BinarySession.js +9 -2
- package/dist/application/BinarySessionStatus.js +39 -1
- package/dist/application/CapabilityInventory.js +15 -0
- package/dist/application/CommandShimReplay.js +6 -3
- package/dist/application/CompositeProvider.js +5 -2
- package/dist/application/DirectAnalysis.js +1 -20
- package/dist/application/DirectAnalysisStatus.js +83 -0
- package/dist/application/Doctor.js +34 -25
- package/dist/application/DoctorDiagnostics.js +1 -4
- package/dist/application/DoctorProjection.js +43 -0
- package/dist/application/DoctorScope.js +102 -0
- package/dist/application/EvidenceReferenceResolver.js +26 -0
- package/dist/application/JavaScriptApplicationEvidence.js +1 -1
- package/dist/application/JavaScriptApplicationEvidenceGraph.js +10 -1
- package/dist/application/JavaScriptApplicationService.js +3 -2
- package/dist/application/JavaScriptApplicationWorkflowEvidence.js +26 -0
- package/dist/application/JavaScriptApplicationWorkflowService.js +49 -1
- package/dist/application/JavaScriptArtifactReconstruction.js +7 -0
- package/dist/application/JavaScriptModuleRelationships.js +25 -1
- package/dist/application/JavaScriptReturnShapeProjection.js +111 -0
- package/dist/application/JavaScriptSemanticGraphBuilder.js +294 -0
- package/dist/application/JavaScriptSemanticGraphConstruction.js +144 -0
- package/dist/application/JavaScriptSemanticGraphEvidence.js +86 -0
- package/dist/application/JavaScriptSemanticGraphFlowProjection.js +72 -0
- package/dist/application/JavaScriptSemanticGraphProjection.js +33 -0
- package/dist/application/JavaScriptSemanticTraceService.js +45 -0
- package/dist/application/LoopbackReplay.js +277 -133
- package/dist/application/LoopbackReplayRecorder.js +101 -0
- package/dist/application/PermissionAuthority.js +7 -3
- package/dist/application/ProcessCaptureJournal.js +95 -0
- package/dist/application/ProcessCaptureLifecycle.js +24 -13
- package/dist/application/ProcessCheckpoints.js +30 -4
- package/dist/application/ProcessCli.js +26 -4
- package/dist/application/ProcessHarness.js +20 -50
- package/dist/application/ProcessNormalization.js +3 -2
- package/dist/application/ProcessReactiveCoordinator.js +218 -0
- package/dist/application/ProcessReactiveEffects.js +133 -0
- package/dist/application/ProcessReactiveObservations.js +20 -0
- package/dist/application/ProcessSampling.js +2 -0
- package/dist/application/SessionProviderRouter.js +7 -6
- package/dist/application/Setup.js +11 -10
- package/dist/application/SetupPlan.js +1 -1
- package/dist/application/SetupRegistrationEnvironment.js +12 -0
- package/dist/application/TerminalRenderer.js +3 -1
- package/dist/browser/CdpEndpoint.js +37 -4
- package/dist/catalogIdentity.js +16 -7
- package/dist/cli/coreAnalysisCommands.js +34 -2
- package/dist/cli/javascriptApplicationAnalysis.js +19 -0
- package/dist/cli/setupCommands.js +81 -32
- package/dist/cli/utilityCommands.js +10 -2
- package/dist/cliApplicationCommands.js +79 -16
- package/dist/cliCommandNames.js +3 -0
- package/dist/cliElectronCommands.js +23 -33
- package/dist/cliObservationOptions.js +1 -1
- package/dist/cliOutput.js +56 -0
- package/dist/cliProcessCommands.js +32 -1
- package/dist/cliSetup.js +30 -21
- package/dist/contracts/applicationToolContracts.js +54 -7
- package/dist/contracts/applicationWorkflowInputContracts.js +119 -0
- package/dist/contracts/electronToolContracts.js +53 -32
- package/dist/contracts/javascriptApplicationWorkflowExamples.js +32 -9
- package/dist/contracts/processCaptureExample.js +1 -0
- package/dist/contracts/replayMachineExample.js +37 -0
- package/dist/contracts/toolContractExamples.js +2 -0
- package/dist/contracts/toolContracts.js +5 -1
- package/dist/contracts/toolEffects.js +3 -0
- package/dist/contracts/toolOutputSchemaGroups.js +2 -0
- package/dist/contracts/toolOutputSchemaPrimitives.js +40 -0
- package/dist/doctorRuntime.js +41 -0
- package/dist/domain/analysisErrorPresentation.js +5 -3
- package/dist/domain/analysisErrorProjection.js +7 -1
- package/dist/domain/changedBehavior.js +5 -4
- package/dist/domain/completionLedgerGeneration.js +190 -0
- package/dist/domain/errors.js +4 -2
- package/dist/domain/evidenceCompletionLedger.js +113 -0
- package/dist/domain/hopperStartupFailure.js +9 -0
- package/dist/domain/javascriptApplicationAnalysis.js +44 -2
- package/dist/domain/javascriptExportShapeComparison.js +145 -0
- package/dist/domain/javascriptExportShapeComparisonIdentity.js +11 -0
- package/dist/domain/javascriptExportShapeComparisonSchemas.js +277 -0
- package/dist/domain/javascriptExportShapeSelection.js +132 -0
- package/dist/domain/javascriptExportShapeVariants.js +272 -0
- package/dist/domain/javascriptRuntimeReconciliationParsing.js +2 -2
- package/dist/domain/javascriptSemanticAnalysis.js +17 -36
- package/dist/domain/javascriptSemanticCallResolution.js +129 -0
- package/dist/domain/javascriptSemanticCalls.js +269 -0
- package/dist/domain/javascriptSemanticGraph.js +356 -0
- package/dist/domain/javascriptSemanticGraphSchemas.js +329 -0
- package/dist/domain/javascriptSemanticGraphSerialization.js +23 -0
- package/dist/domain/javascriptSemanticIr.js +14 -1
- package/dist/domain/javascriptSemanticPrimitives.js +36 -0
- package/dist/domain/javascriptSemanticProjection.js +36 -4
- package/dist/domain/javascriptSemanticProvenance.js +29 -0
- package/dist/domain/javascriptSemanticQuery.js +294 -0
- package/dist/domain/javascriptSemanticQueryAssessment.js +74 -0
- package/dist/domain/javascriptSemanticQueryIdentity.js +86 -0
- package/dist/domain/javascriptSemanticQueryRelations.js +15 -0
- package/dist/domain/javascriptSemanticQuerySchemas.js +188 -0
- package/dist/domain/javascriptSemanticReturns.js +187 -0
- package/dist/domain/javascriptSemanticState.js +32 -0
- package/dist/domain/javascriptSemanticTraceSchemas.js +14 -0
- package/dist/domain/javascriptSemanticValues.js +54 -70
- package/dist/domain/processCapture.js +36 -0
- package/dist/domain/processCaptureValidation.js +101 -12
- package/dist/domain/processComparison.js +78 -25
- package/dist/domain/processObservation.js +153 -0
- package/dist/domain/processReactiveCheckpointDataflow.js +32 -0
- package/dist/domain/processReactiveMatching.js +210 -0
- package/dist/domain/processReactiveRuntime.js +109 -0
- package/dist/domain/processReactiveScenario.js +361 -0
- package/dist/domain/processReactiveScenarioPreflight.js +120 -0
- package/dist/domain/processReactiveTransition.js +159 -0
- package/dist/domain/processScenario.js +12 -0
- package/dist/domain/processTraceComparison.js +43 -0
- package/dist/domain/processTraceDimensionProjection.js +50 -0
- package/dist/domain/processTraceEvaluation.js +312 -0
- package/dist/domain/processTraceSpecification.js +373 -0
- package/dist/domain/reconstructionVerification.js +5 -4
- package/dist/domain/replayMachine.js +18 -2
- package/dist/domain/replayMachineRun.js +216 -0
- package/dist/domain/replayMachineRuntime.js +88 -9
- package/dist/domain/staticRuntimeCorrelation.js +4 -2
- package/dist/evaluation/CodexAgentEval.js +24 -0
- package/dist/generatedPackageMetadata.js +1 -1
- package/dist/ghidra/GhidraClient.js +13 -2
- package/dist/ghidra/GhidraDiagnostics.js +1 -0
- package/dist/ghidra/GhidraLauncher.js +7 -3
- package/dist/ghidra/GhidraProvider.js +8 -1
- package/dist/hopper/BridgeLauncher.js +56 -14
- package/dist/hopper/HopperClient.js +23 -10
- package/dist/hopper/HopperDiagnostics.js +14 -0
- package/dist/hopper/HopperProcessDiagnostic.js +14 -0
- package/dist/hopper/HopperProvider.js +8 -1
- package/dist/hopper/LinuxPrivateDisplayDiagnostic.js +72 -0
- package/dist/hopper/LinuxPrivateDisplayProbe.js +217 -0
- package/dist/main/transport.js +2 -0
- package/dist/mcpDoctor.js +310 -0
- package/dist/process/ProcessOwnership.js +200 -53
- package/dist/process/ProviderRunLineage.js +21 -0
- package/dist/server/createServer.js +4 -1
- package/dist/server/javascriptApplicationResult.js +22 -1
- package/dist/server/registerApplicationTools/characterization.js +3 -3
- package/dist/server/registerApplicationTools/compareExportShapes.js +37 -0
- package/dist/server/registerApplicationTools/compareVersions.js +12 -6
- package/dist/server/registerApplicationTools/controlledReplay.js +2 -2
- package/dist/server/registerApplicationTools/coverage.js +3 -3
- package/dist/server/registerApplicationTools/helpers.js +37 -25
- package/dist/server/registerApplicationTools/traceFeature.js +9 -5
- package/dist/server/registerApplicationTools/traceSemantics.js +30 -0
- package/dist/server/registerApplicationTools.js +4 -0
- package/dist/server/registerEvidenceResources.js +5 -1
- package/dist/server/registerProcessComparisonTool.js +17 -7
- package/dist/server/registerReplayMachineTool.js +20 -0
- package/dist/server/registerSessionTools.js +12 -7
- package/dist/server/sessionAvailabilityPolicy.js +2 -1
- package/dist/serverIdentity.js +19 -0
- package/package.json +62 -30
- package/scripts/hopper-demo-x11.py +459 -61
- package/scripts/prepack.mjs +55 -0
- package/scripts/prepare.mjs +28 -0
- package/scripts/rea.mjs +26 -5
- package/skills/reverse-engineer-anything/SKILL.md +2 -2
- package/skills/reverse-engineer-anything/references/javascript-applications.md +10 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/client";
|
|
2
|
+
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
3
|
+
import { Formatter } from "incur";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { CATALOG_IDENTITY, MCP_RESOURCE_CATALOG, MCP_RESOURCE_TEMPLATE_CATALOG, } from "./catalogIdentity.js";
|
|
6
|
+
import { PROMPT_CONTRACTS } from "./contracts/promptContracts.js";
|
|
7
|
+
import { PRODUCT_IDENTITY } from "./identity.js";
|
|
8
|
+
const DEFAULT_DEADLINE_MS = 20_000;
|
|
9
|
+
const STDERR_LIMIT_BYTES = 64 * 1_024;
|
|
10
|
+
const OUTPUT_FORMATS = ["toon", "json", "yaml", "md", "jsonl"];
|
|
11
|
+
/** Diagnose the actual production stdio adapter with one bounded SDK session. */
|
|
12
|
+
export const runProductionMcpDoctor = async (options) => {
|
|
13
|
+
const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS);
|
|
14
|
+
const controller = new AbortController();
|
|
15
|
+
const timer = setTimeout(() => controller.abort(new Error("Production MCP doctor deadline expired")), Math.max(1, deadline - Date.now()));
|
|
16
|
+
const stderr = boundedStderr();
|
|
17
|
+
const transport = new TrackedStdioClientTransport({
|
|
18
|
+
command: options.command,
|
|
19
|
+
args: [...options.args],
|
|
20
|
+
cwd: options.cwd,
|
|
21
|
+
env: targetFreeEnvironment(options.environment),
|
|
22
|
+
stderr: "pipe",
|
|
23
|
+
});
|
|
24
|
+
transport.stderr?.on("data", stderr.append);
|
|
25
|
+
const client = new Client({ name: "rea-mcp-doctor", version: "1" });
|
|
26
|
+
try {
|
|
27
|
+
await client.connect(transport, requestOptions(deadline, controller.signal));
|
|
28
|
+
return {
|
|
29
|
+
...(await inspectProductionMcpSession(client, deadline, controller.signal)),
|
|
30
|
+
diagnostics: stderr.value(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (cause) {
|
|
34
|
+
return {
|
|
35
|
+
healthy: false,
|
|
36
|
+
adapter: "production-stdio",
|
|
37
|
+
server: null,
|
|
38
|
+
inventory: null,
|
|
39
|
+
identity: null,
|
|
40
|
+
request_flow: { tool: "binary_session", ok: false },
|
|
41
|
+
checks: [
|
|
42
|
+
{
|
|
43
|
+
name: "transport",
|
|
44
|
+
ok: false,
|
|
45
|
+
detail: errorMessage(cause),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
diagnostics: stderr.value(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
await closeMcpChild(client, transport);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const inspectProductionMcpSession = async (client, deadline, signal) => {
|
|
57
|
+
const request = requestOptions(deadline, signal);
|
|
58
|
+
const [tools, prompts, resources, templates, identity, requestFlow] = await Promise.all([
|
|
59
|
+
client.listTools(undefined, request),
|
|
60
|
+
client.listPrompts(undefined, request),
|
|
61
|
+
client.listResources(undefined, request),
|
|
62
|
+
client.listResourceTemplates(undefined, request),
|
|
63
|
+
client.readResource({ uri: "rea://server/identity" }, request),
|
|
64
|
+
client.callTool({ name: "binary_session", arguments: {} }, request),
|
|
65
|
+
]);
|
|
66
|
+
const inventory = {
|
|
67
|
+
tools: compareInventory(CATALOG_IDENTITY.tools.map(({ name }) => name), tools.tools.map(({ name }) => name)),
|
|
68
|
+
prompts: compareInventory(PROMPT_CONTRACTS.map(({ name }) => name), prompts.prompts.map(({ name }) => name)),
|
|
69
|
+
resources: compareInventory(MCP_RESOURCE_CATALOG.map(({ name }) => name), resources.resources.map(({ name }) => name)),
|
|
70
|
+
resource_templates: compareInventory(MCP_RESOURCE_TEMPLATE_CATALOG.map(({ name }) => name), templates.resourceTemplates.map(({ name }) => name)),
|
|
71
|
+
};
|
|
72
|
+
const observedIdentity = parseIdentityResource(identity.contents);
|
|
73
|
+
const serverVersion = client.getServerVersion();
|
|
74
|
+
const protocolVersion = client.getNegotiatedProtocolVersion();
|
|
75
|
+
const checks = [
|
|
76
|
+
{
|
|
77
|
+
name: "initialize",
|
|
78
|
+
ok: serverVersion?.name === PRODUCT_IDENTITY.mcpServerKey &&
|
|
79
|
+
serverVersion.version === PRODUCT_IDENTITY.packageVersion &&
|
|
80
|
+
protocolVersion !== undefined,
|
|
81
|
+
detail: `${serverVersion?.name ?? "unknown"}@${serverVersion?.version ?? "unknown"}; protocol=${protocolVersion ?? "unknown"}`,
|
|
82
|
+
},
|
|
83
|
+
...Object.entries(inventory).map(([name, comparison]) => inventoryCheck(name, comparison)),
|
|
84
|
+
{
|
|
85
|
+
name: "server-identity",
|
|
86
|
+
ok: observedIdentity.packageVersion === PRODUCT_IDENTITY.packageVersion &&
|
|
87
|
+
observedIdentity.catalogDigest ===
|
|
88
|
+
CATALOG_IDENTITY.digests.combined_sha256,
|
|
89
|
+
detail: `package=${observedIdentity.packageVersion ?? "missing"}; catalog=${observedIdentity.catalogDigest ?? "missing"}`,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "request-flow",
|
|
93
|
+
ok: requestFlow.isError !== true,
|
|
94
|
+
detail: "binary_session target-free request",
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
return {
|
|
98
|
+
healthy: checks.every(({ ok }) => ok),
|
|
99
|
+
adapter: "production-stdio",
|
|
100
|
+
server: {
|
|
101
|
+
name: serverVersion?.name ?? null,
|
|
102
|
+
version: serverVersion?.version ?? null,
|
|
103
|
+
protocol_version: protocolVersion ?? null,
|
|
104
|
+
},
|
|
105
|
+
inventory,
|
|
106
|
+
identity: {
|
|
107
|
+
expected_package_version: PRODUCT_IDENTITY.packageVersion,
|
|
108
|
+
observed_package_version: observedIdentity.packageVersion,
|
|
109
|
+
expected_catalog_digest: CATALOG_IDENTITY.digests.combined_sha256,
|
|
110
|
+
observed_catalog_digest: observedIdentity.catalogDigest,
|
|
111
|
+
},
|
|
112
|
+
request_flow: {
|
|
113
|
+
tool: "binary_session",
|
|
114
|
+
ok: requestFlow.isError !== true,
|
|
115
|
+
},
|
|
116
|
+
checks,
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Dispatcher entry for `rea mcp doctor`, kept separate from Incur MCP setup.
|
|
121
|
+
* @public
|
|
122
|
+
*/
|
|
123
|
+
export const runProductionMcpDoctorCli = async (arguments_, input) => {
|
|
124
|
+
const parsed = parseOutputArguments(arguments_);
|
|
125
|
+
if (!parsed.ok)
|
|
126
|
+
return {
|
|
127
|
+
output: `${Formatter.format({ code: "VALIDATION_ERROR", message: parsed.message }, parsed.format)}\n`,
|
|
128
|
+
exitCode: 1,
|
|
129
|
+
};
|
|
130
|
+
const result = await runProductionMcpDoctor({
|
|
131
|
+
command: process.execPath,
|
|
132
|
+
args: [resolve(input.dispatcherPath), "mcp"],
|
|
133
|
+
cwd: input.packageRoot,
|
|
134
|
+
environment: process.env,
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
output: `${Formatter.format(result, parsed.format)}\n`,
|
|
138
|
+
exitCode: result.healthy ? 0 : 1,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
/** Compare exact names while retaining actionable mismatch locations. */
|
|
142
|
+
export const compareInventory = (expectedNames, observedNames) => {
|
|
143
|
+
const expected = new Set(expectedNames);
|
|
144
|
+
const observed = new Set(observedNames);
|
|
145
|
+
return {
|
|
146
|
+
expected: expectedNames.length,
|
|
147
|
+
observed: observedNames.length,
|
|
148
|
+
missing: [...expected].filter((name) => !observed.has(name)).sort(),
|
|
149
|
+
unexpected: [...observed].filter((name) => !expected.has(name)).sort(),
|
|
150
|
+
duplicates: [...observed]
|
|
151
|
+
.filter((name) => observedNames.filter((candidate) => candidate === name).length > 1)
|
|
152
|
+
.sort(),
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
const inventoryCheck = (name, comparison) => ({
|
|
156
|
+
name: `inventory:${name}`,
|
|
157
|
+
ok: comparison.expected === comparison.observed &&
|
|
158
|
+
comparison.missing.length === 0 &&
|
|
159
|
+
comparison.unexpected.length === 0 &&
|
|
160
|
+
comparison.duplicates.length === 0,
|
|
161
|
+
detail: `expected=${String(comparison.expected)} observed=${String(comparison.observed)} missing=${comparison.missing.join(",") || "none"} unexpected=${comparison.unexpected.join(",") || "none"} duplicates=${comparison.duplicates.join(",") || "none"}`,
|
|
162
|
+
});
|
|
163
|
+
const parseIdentityResource = (contents) => {
|
|
164
|
+
const first = contents.find((candidate) => isRecord(candidate) && typeof candidate.text === "string");
|
|
165
|
+
if (!isRecord(first) || typeof first.text !== "string")
|
|
166
|
+
return { packageVersion: null, catalogDigest: null };
|
|
167
|
+
try {
|
|
168
|
+
const identity = JSON.parse(first.text);
|
|
169
|
+
if (!isRecord(identity))
|
|
170
|
+
return { packageVersion: null, catalogDigest: null };
|
|
171
|
+
const packageIdentity = identity.package;
|
|
172
|
+
const catalog = identity.catalog;
|
|
173
|
+
const digests = isRecord(catalog) ? catalog.digests : undefined;
|
|
174
|
+
return {
|
|
175
|
+
packageVersion: isRecord(packageIdentity) && typeof packageIdentity.version === "string"
|
|
176
|
+
? packageIdentity.version
|
|
177
|
+
: null,
|
|
178
|
+
catalogDigest: isRecord(digests) && typeof digests.combined_sha256 === "string"
|
|
179
|
+
? digests.combined_sha256
|
|
180
|
+
: null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return { packageVersion: null, catalogDigest: null };
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
const parseOutputArguments = (arguments_) => {
|
|
188
|
+
let format = "toon";
|
|
189
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
190
|
+
const argument = arguments_[index];
|
|
191
|
+
if (argument === "--json") {
|
|
192
|
+
format = "json";
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const inline = argument?.startsWith("--format=")
|
|
196
|
+
? argument.slice("--format=".length)
|
|
197
|
+
: undefined;
|
|
198
|
+
if (argument === "--format" || inline !== undefined) {
|
|
199
|
+
const candidate = inline ?? arguments_[index + 1];
|
|
200
|
+
if (inline === undefined)
|
|
201
|
+
index += 1;
|
|
202
|
+
if (!isOutputFormat(candidate))
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
format,
|
|
206
|
+
message: `Invalid output format: ${candidate ?? "missing"}`,
|
|
207
|
+
};
|
|
208
|
+
format = candidate;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (argument === "--full-output")
|
|
212
|
+
continue;
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
format,
|
|
216
|
+
message: `Unknown mcp doctor option: ${argument ?? "missing"}`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { ok: true, format };
|
|
220
|
+
};
|
|
221
|
+
const isOutputFormat = (value) => OUTPUT_FORMATS.some((candidate) => candidate === value);
|
|
222
|
+
const requestOptions = (deadline, signal) => ({
|
|
223
|
+
signal,
|
|
224
|
+
timeout: Math.max(1, deadline - Date.now()),
|
|
225
|
+
});
|
|
226
|
+
const targetFreeEnvironment = (environment) => {
|
|
227
|
+
const result = Object.fromEntries(Object.entries(environment).filter((entry) => entry[1] !== undefined));
|
|
228
|
+
for (const name of [
|
|
229
|
+
"HOPPER_TARGET_PATH",
|
|
230
|
+
"HOPPER_TARGET_KIND",
|
|
231
|
+
"HOPPER_LOADER_ARGS_JSON",
|
|
232
|
+
])
|
|
233
|
+
delete result[name];
|
|
234
|
+
return result;
|
|
235
|
+
};
|
|
236
|
+
const boundedStderr = () => {
|
|
237
|
+
let text = "";
|
|
238
|
+
let bytes = 0;
|
|
239
|
+
let truncated = false;
|
|
240
|
+
return {
|
|
241
|
+
append(chunk) {
|
|
242
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
243
|
+
? chunk
|
|
244
|
+
: Buffer.from(String(chunk), "utf8");
|
|
245
|
+
bytes += buffer.byteLength;
|
|
246
|
+
const retained = Buffer.byteLength(text, "utf8");
|
|
247
|
+
if (retained < STDERR_LIMIT_BYTES)
|
|
248
|
+
text += buffer
|
|
249
|
+
.subarray(0, STDERR_LIMIT_BYTES - retained)
|
|
250
|
+
.toString("utf8");
|
|
251
|
+
if (bytes > STDERR_LIMIT_BYTES)
|
|
252
|
+
truncated = true;
|
|
253
|
+
},
|
|
254
|
+
value: () => ({
|
|
255
|
+
stderr: text,
|
|
256
|
+
stderr_bytes: bytes,
|
|
257
|
+
stderr_truncated: truncated,
|
|
258
|
+
}),
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
262
|
+
const errorMessage = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
263
|
+
class TrackedStdioClientTransport extends StdioClientTransport {
|
|
264
|
+
spawnedPid = null;
|
|
265
|
+
async start() {
|
|
266
|
+
await super.start();
|
|
267
|
+
this.spawnedPid = this.pid;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const closeMcpChild = async (client, transport) => {
|
|
271
|
+
const pid = transport.spawnedPid ?? transport.pid;
|
|
272
|
+
await Promise.allSettled([client.close()]);
|
|
273
|
+
await Promise.allSettled([transport.close()]);
|
|
274
|
+
if (pid === null || !(await processExists(pid)))
|
|
275
|
+
return;
|
|
276
|
+
await terminateProcess(pid, "SIGTERM");
|
|
277
|
+
if (await waitForProcessExit(pid, 1_000))
|
|
278
|
+
return;
|
|
279
|
+
await terminateProcess(pid, "SIGKILL");
|
|
280
|
+
await waitForProcessExit(pid, 1_000);
|
|
281
|
+
};
|
|
282
|
+
const terminateProcess = async (pid, signal) => {
|
|
283
|
+
try {
|
|
284
|
+
process.kill(pid, signal);
|
|
285
|
+
}
|
|
286
|
+
catch (cause) {
|
|
287
|
+
if (!(cause instanceof Error && "code" in cause && cause.code === "ESRCH"))
|
|
288
|
+
throw cause;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
const waitForProcessExit = async (pid, timeoutMs) => {
|
|
292
|
+
const deadline = Date.now() + timeoutMs;
|
|
293
|
+
do {
|
|
294
|
+
if (!(await processExists(pid)))
|
|
295
|
+
return true;
|
|
296
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
297
|
+
} while (Date.now() < deadline);
|
|
298
|
+
return !(await processExists(pid));
|
|
299
|
+
};
|
|
300
|
+
const processExists = async (pid) => {
|
|
301
|
+
try {
|
|
302
|
+
process.kill(pid, 0);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
catch (cause) {
|
|
306
|
+
if (cause instanceof Error && "code" in cause && cause.code === "ESRCH")
|
|
307
|
+
return false;
|
|
308
|
+
throw cause;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
@@ -44,7 +44,7 @@ export const parseProcessEnvironment = (value) => Object.fromEntries(value
|
|
|
44
44
|
return [entry.slice(0, separator), entry.slice(separator + 1)];
|
|
45
45
|
}));
|
|
46
46
|
const systemHost = {
|
|
47
|
-
async
|
|
47
|
+
async listProcesses() {
|
|
48
48
|
if (process.platform === "win32")
|
|
49
49
|
return [];
|
|
50
50
|
const { stdout } = await execFileAsync("ps", [
|
|
@@ -61,8 +61,7 @@ const systemHost = {
|
|
|
61
61
|
processGroupId: Number(match[3]),
|
|
62
62
|
state: match[4] ?? "",
|
|
63
63
|
command: match[5] ?? "",
|
|
64
|
-
}))
|
|
65
|
-
.filter((member) => member.processGroupId === processGroupId);
|
|
64
|
+
}));
|
|
66
65
|
},
|
|
67
66
|
async environment(pid) {
|
|
68
67
|
if (process.platform === "linux") {
|
|
@@ -95,73 +94,140 @@ const systemWindowsProcessTreeHost = {
|
|
|
95
94
|
};
|
|
96
95
|
/** Read the capture run token currently exposed by one live process. */
|
|
97
96
|
export const readProcessRunId = async (pid, host = systemHost) => (await host.environment(pid)).REA_PROCESS_RUN_ID;
|
|
98
|
-
/**
|
|
99
|
-
* Verify run-token ownership before signaling a POSIX process group.
|
|
100
|
-
*
|
|
101
|
-
* Process IDs and group IDs can be reused. REA therefore re-reads every live
|
|
102
|
-
* member's environment immediately before signaling and fails closed if any
|
|
103
|
-
* member cannot be inspected or lacks the per-capture run token.
|
|
104
|
-
*/
|
|
97
|
+
/** Token-validate every rooted POSIX process group before signaling any. */
|
|
105
98
|
export const cleanupOwnedProcessGroup = async (ownership, host = systemHost) => {
|
|
106
|
-
let
|
|
99
|
+
let processes;
|
|
107
100
|
try {
|
|
108
|
-
|
|
101
|
+
processes = liveProcesses(await host.listProcesses());
|
|
109
102
|
}
|
|
110
103
|
catch {
|
|
111
|
-
return { cleaned: false, reason: "process
|
|
104
|
+
return { cleaned: false, reason: "process table could not be inspected" };
|
|
112
105
|
}
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
106
|
+
const plan = await createOwnedCleanupPlan(ownership, processes, host);
|
|
107
|
+
if ("cleaned" in plan)
|
|
108
|
+
return plan;
|
|
109
|
+
let signaled = false;
|
|
110
|
+
for (const processGroupId of plan.signalOrder) {
|
|
111
|
+
const revalidation = await revalidateOwnedProcessGroup(processGroupId, ownership, host);
|
|
112
|
+
if ("cleaned" in revalidation)
|
|
113
|
+
return revalidation;
|
|
114
|
+
if (revalidation.empty)
|
|
115
|
+
continue;
|
|
116
|
+
try {
|
|
117
|
+
host.signalGroup(processGroupId, "SIGKILL");
|
|
118
|
+
signaled = true;
|
|
119
|
+
}
|
|
120
|
+
catch (cause) {
|
|
121
|
+
const code = cause instanceof Error && "code" in cause ? cause.code : undefined;
|
|
122
|
+
if (code !== "ESRCH")
|
|
123
|
+
return { cleaned: false, reason: "owned process group signal failed" };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { cleaned: true, signaled };
|
|
127
|
+
};
|
|
128
|
+
const createOwnedCleanupPlan = async (ownership, processes, host) => {
|
|
129
|
+
const launcher = processes.find(({ pid }) => pid === ownership.leaderPid);
|
|
130
|
+
const rootMembers = processes.filter(({ processGroupId }) => processGroupId === ownership.processGroupId);
|
|
131
|
+
if (launcher === undefined && rootMembers.length === 0)
|
|
115
132
|
return { cleaned: true, signaled: false };
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
let descendants = [];
|
|
134
|
+
if (launcher !== undefined) {
|
|
135
|
+
const identityFailure = launcherIdentityFailure(launcher, ownership);
|
|
136
|
+
if (identityFailure !== null)
|
|
137
|
+
return { cleaned: false, reason: identityFailure };
|
|
138
|
+
descendants = descendantsOf(launcher.pid, processes);
|
|
139
|
+
}
|
|
140
|
+
const descendantPids = new Set(descendants.map(({ pid }) => pid));
|
|
141
|
+
const processGroupIds = new Set([ownership.processGroupId]);
|
|
142
|
+
for (const descendant of descendants)
|
|
143
|
+
processGroupIds.add(descendant.processGroupId);
|
|
144
|
+
for (const processGroupId of processGroupIds) {
|
|
145
|
+
if (processGroupId === ownership.processGroupId)
|
|
146
|
+
continue;
|
|
147
|
+
const groupLeader = processes.find(({ pid, processGroupId: observedGroupId }) => pid === processGroupId && observedGroupId === processGroupId);
|
|
148
|
+
if (groupLeader === undefined || !descendantPids.has(groupLeader.pid))
|
|
126
149
|
return {
|
|
127
150
|
cleaned: false,
|
|
128
|
-
reason: "
|
|
151
|
+
reason: "descendant process-group leader identity could not be verified",
|
|
129
152
|
};
|
|
130
153
|
}
|
|
154
|
+
const liveMembers = processes.filter(({ processGroupId }) => processGroupIds.has(processGroupId));
|
|
155
|
+
const failures = await processOwnershipFailures(liveMembers, ownership.runId, host);
|
|
156
|
+
if (failures.length > 0)
|
|
157
|
+
return cleanupValidationFailure(failures);
|
|
158
|
+
return {
|
|
159
|
+
signalOrder: [ownership.processGroupId].concat([...processGroupIds]
|
|
160
|
+
.filter((processGroupId) => processGroupId !== ownership.processGroupId)
|
|
161
|
+
.sort((left, right) => left - right)),
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
const launcherIdentityFailure = (launcher, ownership) => {
|
|
165
|
+
if (launcher.processGroupId !== ownership.processGroupId)
|
|
166
|
+
return "owned launcher process-group identity did not match";
|
|
167
|
+
if (ownership.expectedParentPid !== undefined &&
|
|
168
|
+
launcher.parentPid !== ownership.expectedParentPid)
|
|
169
|
+
return "owned launcher parent identity did not match";
|
|
170
|
+
if (ownership.expectedCommand !== undefined &&
|
|
171
|
+
!commandMatches(launcher.command, ownership.expectedCommand))
|
|
172
|
+
return "owned launcher command identity did not match";
|
|
173
|
+
return null;
|
|
174
|
+
};
|
|
175
|
+
const revalidateOwnedProcessGroup = async (processGroupId, ownership, host) => {
|
|
176
|
+
let members;
|
|
177
|
+
try {
|
|
178
|
+
members = liveProcesses(await host.listProcesses()).filter(({ processGroupId: observedGroupId }) => observedGroupId === processGroupId);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return {
|
|
182
|
+
cleaned: false,
|
|
183
|
+
reason: "process ownership could not be revalidated",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const failures = await processOwnershipFailures(members, ownership.runId, host);
|
|
187
|
+
if (failures.length > 0)
|
|
188
|
+
return cleanupValidationFailure(failures);
|
|
189
|
+
if (processGroupId === ownership.processGroupId) {
|
|
190
|
+
const launcher = members.find(({ pid }) => pid === ownership.leaderPid);
|
|
191
|
+
if (launcher !== undefined) {
|
|
192
|
+
const identityFailure = launcherIdentityFailure(launcher, ownership);
|
|
193
|
+
if (identityFailure !== null)
|
|
194
|
+
return { cleaned: false, reason: identityFailure };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (processGroupId !== ownership.processGroupId &&
|
|
198
|
+
members.length > 0 &&
|
|
199
|
+
!members.some(({ pid }) => pid === processGroupId))
|
|
200
|
+
return {
|
|
201
|
+
cleaned: false,
|
|
202
|
+
reason: "descendant process-group leader identity could not be revalidated",
|
|
203
|
+
};
|
|
204
|
+
return { empty: members.length === 0 };
|
|
205
|
+
};
|
|
206
|
+
const cleanupValidationFailure = (failures) => ({
|
|
207
|
+
cleaned: false,
|
|
208
|
+
reason: failures.some(({ reason }) => reason === "run-token-mismatch")
|
|
209
|
+
? "process tree contains an unowned or PID-reused process"
|
|
210
|
+
: "process ownership could not be revalidated",
|
|
211
|
+
failures,
|
|
212
|
+
});
|
|
213
|
+
const processOwnershipFailures = async (members, runId, host) => {
|
|
131
214
|
const failures = [];
|
|
132
|
-
for (const member of
|
|
215
|
+
for (const member of members) {
|
|
133
216
|
try {
|
|
134
|
-
|
|
135
|
-
if (environment.REA_PROCESS_RUN_ID !== ownership.runId)
|
|
217
|
+
if ((await host.environment(member.pid)).REA_PROCESS_RUN_ID !== runId)
|
|
136
218
|
failures.push({ pid: member.pid, reason: "run-token-mismatch" });
|
|
137
219
|
}
|
|
138
220
|
catch {
|
|
139
221
|
failures.push({ pid: member.pid, reason: "environment-unreadable" });
|
|
140
222
|
}
|
|
141
223
|
}
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
cleaned: false,
|
|
145
|
-
reason: failures.some(({ reason }) => reason === "run-token-mismatch")
|
|
146
|
-
? "process group contains an unowned or PID-reused process"
|
|
147
|
-
: "process ownership could not be revalidated",
|
|
148
|
-
failures,
|
|
149
|
-
};
|
|
150
|
-
try {
|
|
151
|
-
host.signalGroup(ownership.processGroupId, "SIGKILL");
|
|
152
|
-
}
|
|
153
|
-
catch (cause) {
|
|
154
|
-
const code = cause instanceof Error && "code" in cause ? cause.code : undefined;
|
|
155
|
-
if (code !== "ESRCH")
|
|
156
|
-
return { cleaned: false, reason: "owned process group signal failed" };
|
|
157
|
-
}
|
|
158
|
-
return { cleaned: true, signaled: true };
|
|
224
|
+
return failures;
|
|
159
225
|
};
|
|
160
226
|
/** Observe one group without signaling it, failing closed on identity doubt. */
|
|
161
227
|
export const observeOwnedProcessGroup = async (ownership, host = systemHost) => {
|
|
162
228
|
let members;
|
|
163
229
|
try {
|
|
164
|
-
members = await host.
|
|
230
|
+
members = (await host.listProcesses()).filter(({ processGroupId }) => processGroupId === ownership.processGroupId);
|
|
165
231
|
}
|
|
166
232
|
catch {
|
|
167
233
|
return {
|
|
@@ -169,7 +235,7 @@ export const observeOwnedProcessGroup = async (ownership, host = systemHost) =>
|
|
|
169
235
|
reason: "process group could not be inspected",
|
|
170
236
|
};
|
|
171
237
|
}
|
|
172
|
-
const liveMembers =
|
|
238
|
+
const liveMembers = liveProcesses(members);
|
|
173
239
|
if (liveMembers.length === 0)
|
|
174
240
|
return { state: "empty" };
|
|
175
241
|
for (const member of liveMembers) {
|
|
@@ -190,10 +256,91 @@ export const observeOwnedProcessGroup = async (ownership, host = systemHost) =>
|
|
|
190
256
|
}
|
|
191
257
|
return { state: "alive" };
|
|
192
258
|
};
|
|
193
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Record the live launcher and descendant lineage after run-token validation.
|
|
261
|
+
*
|
|
262
|
+
* The observation is intentionally point-in-time. A verified empty descendant
|
|
263
|
+
* list means no descendants were live during this observation, not that the
|
|
264
|
+
* run never created a short-lived child.
|
|
265
|
+
*/
|
|
266
|
+
export const observeOwnedProcessLineage = async (ownership, host = systemHost) => {
|
|
267
|
+
let processes;
|
|
268
|
+
try {
|
|
269
|
+
processes = liveProcesses(await host.listProcesses());
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return unavailableLineage(ownership, "process table could not be inspected");
|
|
273
|
+
}
|
|
274
|
+
const launcher = processes.find(({ pid }) => pid === ownership.leaderPid);
|
|
275
|
+
if (launcher === undefined)
|
|
276
|
+
return unavailableLineage(ownership, "owned launcher is not live");
|
|
277
|
+
if (launcher.processGroupId !== ownership.processGroupId)
|
|
278
|
+
return unavailableLineage(ownership, "owned launcher process-group identity did not match");
|
|
279
|
+
if (ownership.expectedParentPid !== undefined &&
|
|
280
|
+
launcher.parentPid !== ownership.expectedParentPid)
|
|
281
|
+
return unavailableLineage(ownership, "owned launcher parent identity did not match");
|
|
282
|
+
if (ownership.expectedCommand !== undefined &&
|
|
283
|
+
!commandMatches(launcher.command, ownership.expectedCommand))
|
|
284
|
+
return unavailableLineage(ownership, "owned launcher command identity did not match");
|
|
285
|
+
const descendants = descendantsOf(launcher.pid, processes);
|
|
286
|
+
for (const member of [launcher, ...descendants]) {
|
|
287
|
+
try {
|
|
288
|
+
if ((await host.environment(member.pid)).REA_PROCESS_RUN_ID !==
|
|
289
|
+
ownership.runId)
|
|
290
|
+
return unavailableLineage(ownership, "process lineage contains an unowned or PID-reused process");
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return unavailableLineage(ownership, "process ownership could not be revalidated");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
status: "verified",
|
|
298
|
+
observedAt: new Date().toISOString(),
|
|
299
|
+
lineage: {
|
|
300
|
+
schemaVersion: 1,
|
|
301
|
+
runId: ownership.runId,
|
|
302
|
+
launcherPid: launcher.pid,
|
|
303
|
+
launcherParentPid: launcher.parentPid,
|
|
304
|
+
processGroupId: launcher.processGroupId,
|
|
305
|
+
descendants: descendants
|
|
306
|
+
.sort((left, right) => left.pid - right.pid)
|
|
307
|
+
.map(({ pid, parentPid, processGroupId }) => ({
|
|
308
|
+
pid,
|
|
309
|
+
parentPid,
|
|
310
|
+
processGroupId,
|
|
311
|
+
})),
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
};
|
|
315
|
+
const descendantsOf = (launcherPid, processes) => {
|
|
316
|
+
const childrenByParent = new Map();
|
|
317
|
+
for (const process of processes) {
|
|
318
|
+
const siblings = childrenByParent.get(process.parentPid) ?? [];
|
|
319
|
+
siblings.push(process);
|
|
320
|
+
childrenByParent.set(process.parentPid, siblings);
|
|
321
|
+
}
|
|
322
|
+
const descendants = [];
|
|
323
|
+
const pending = [...(childrenByParent.get(launcherPid) ?? [])];
|
|
324
|
+
const visited = new Set([launcherPid]);
|
|
325
|
+
for (const process of pending) {
|
|
326
|
+
if (visited.has(process.pid))
|
|
327
|
+
continue;
|
|
328
|
+
visited.add(process.pid);
|
|
329
|
+
descendants.push(process);
|
|
330
|
+
pending.push(...(childrenByParent.get(process.pid) ?? []));
|
|
331
|
+
}
|
|
332
|
+
return descendants;
|
|
333
|
+
};
|
|
334
|
+
const unavailableLineage = (ownership, reason) => ({
|
|
335
|
+
status: "unavailable",
|
|
336
|
+
observedAt: new Date().toISOString(),
|
|
337
|
+
runId: ownership.runId,
|
|
338
|
+
launcherPid: ownership.leaderPid,
|
|
339
|
+
processGroupId: ownership.processGroupId,
|
|
340
|
+
reason,
|
|
341
|
+
});
|
|
342
|
+
const liveProcesses = (members) => members.filter(({ state }) => !state.startsWith("Z"));
|
|
194
343
|
const commandMatches = (actual, expected) => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return (normalizedActual === normalizedExpected ||
|
|
198
|
-
normalizedActual.startsWith(`${normalizedExpected} `));
|
|
344
|
+
return (actual.trim() === expected.trim() ||
|
|
345
|
+
actual.trim().startsWith(`${expected.trim()} `));
|
|
199
346
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { observeOwnedProcessLineage } from "./ProcessOwnership.js";
|
|
2
|
+
/** Retain the latest truthful process-lineage observation for one client. */
|
|
3
|
+
export class ProviderRunLineage {
|
|
4
|
+
#observation = null;
|
|
5
|
+
/** Clear stale lineage before a new launch attempt. */
|
|
6
|
+
reset() {
|
|
7
|
+
this.#observation = null;
|
|
8
|
+
}
|
|
9
|
+
/** Observe a token-owned launcher without signaling it. */
|
|
10
|
+
async observe(launch) {
|
|
11
|
+
if (launch?.ownership === undefined)
|
|
12
|
+
return;
|
|
13
|
+
this.#observation = await observeOwnedProcessLineage(launch.ownership);
|
|
14
|
+
}
|
|
15
|
+
/** Return a detached copy suitable for caller-visible status. */
|
|
16
|
+
snapshot() {
|
|
17
|
+
return this.#observation === null
|
|
18
|
+
? null
|
|
19
|
+
: structuredClone(this.#observation);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -15,7 +15,7 @@ import { createServerIdentity } from "../serverIdentity.js";
|
|
|
15
15
|
import { registerBrowserTools } from "./registerBrowserTools.js";
|
|
16
16
|
import { registerElectronTools } from "./registerElectronTools.js";
|
|
17
17
|
import { registerApplicationTools } from "./registerApplicationTools.js";
|
|
18
|
-
const TARGET_FREE_INSTRUCTIONS = "REA analyzes shipped artifacts
|
|
18
|
+
const TARGET_FREE_INSTRUCTIONS = "REA analyzes shipped artifacts. Route: ASAR/JavaScript -> analyze_javascript_application (configured root required); archive/package -> inventory_artifact; managed PE/CLI -> inspect_managed_artifact; browser/Electron runtime -> list_browser_targets/list_electron_targets; native binary/database -> open_binary, then binary_overview. Check binary_session for policy. Start with summaries and cite Evidence IDs. Never repeat identical analysis or read full Evidence without a specific need.";
|
|
19
19
|
const ACTIVE_TARGET_INSTRUCTIONS = "REA analyzes the active reverse-engineering target. Start native analysis with binary_overview, then narrow with analyze_function, literal search, callers, callees, and xrefs. Prefer summary views, never repeat an identical call, and read full Evidence only when the task requires it.";
|
|
20
20
|
import { LinuxJavaScriptReplayRunner } from "../replay/LinuxJavaScriptReplayRunner.js";
|
|
21
21
|
import { SystemJavaScriptReplayHost } from "../replay/SystemJavaScriptReplayHost.js";
|
|
@@ -205,6 +205,9 @@ const registerObservationTools = ({ server, session, options, logger, permission
|
|
|
205
205
|
logger,
|
|
206
206
|
recordEvidence,
|
|
207
207
|
recordEvidenceWithUnknown,
|
|
208
|
+
evidenceLookup: session === undefined
|
|
209
|
+
? undefined
|
|
210
|
+
: (evidenceId) => session.evidenceById(evidenceId),
|
|
208
211
|
evidenceFilePolicy: options.evidenceFilePolicy ?? {
|
|
209
212
|
roots: [],
|
|
210
213
|
maxBytes: 1,
|