rea-agents 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/README.md +103 -38
  2. package/bridge/ghidra/ReaGhidraBridge.java +96 -11
  3. package/dist/application/AndroidApplicationService.js +17 -0
  4. package/dist/application/AppleApplicationService.js +17 -0
  5. package/dist/application/ArtifactExtraction.js +1 -1
  6. package/dist/application/ArtifactInventory/classify.js +42 -0
  7. package/dist/application/ArtifactInventory/hash.js +43 -0
  8. package/dist/application/ArtifactInventory/reader.js +41 -0
  9. package/dist/application/ArtifactInventory/scanCanonical.js +155 -0
  10. package/dist/application/ArtifactInventory/scanReader.js +124 -0
  11. package/dist/application/ArtifactInventory/types.js +10 -0
  12. package/dist/application/ArtifactInventory.js +9 -351
  13. package/dist/application/AuthorizedArtifactInventory.js +2 -2
  14. package/dist/application/BinarySession.js +47 -270
  15. package/dist/application/BinarySessionExecution.js +28 -0
  16. package/dist/application/BinarySessionOpen.js +39 -0
  17. package/dist/application/BinarySessionRecords.js +149 -0
  18. package/dist/application/BinarySessionStatus.js +75 -0
  19. package/dist/application/CapabilityInventory.js +24 -2
  20. package/dist/application/ClientRegistrationStatus.js +11 -4
  21. package/dist/application/CrossVersionInventory.js +8 -2
  22. package/dist/application/CrossVersionInvestigation.js +1 -23
  23. package/dist/application/CrossVersionInvestigationCollections.js +23 -0
  24. package/dist/application/DirectAnalysis.js +47 -44
  25. package/dist/application/Doctor.js +156 -238
  26. package/dist/application/DoctorDiagnostics.js +180 -0
  27. package/dist/application/ElectronBoundaryAnalysis.js +23 -7
  28. package/dist/application/ElectronBoundaryGraphWindows.js +32 -8
  29. package/dist/application/EnhancedToolPagination.js +22 -0
  30. package/dist/application/EnhancedToolTypes.js +4 -0
  31. package/dist/application/EnhancedTools.js +20 -37
  32. package/dist/application/InstrumentedJavaScriptReplayHost.js +36 -0
  33. package/dist/application/InventoryProjectionEvidence.js +41 -0
  34. package/dist/application/InvestigationProviders.js +12 -0
  35. package/dist/application/InvestigationWorkspaceStore.js +24 -10
  36. package/dist/application/JavaScriptArtifactAnalysis.js +45 -6
  37. package/dist/application/JavaScriptArtifactFiles.js +6 -2
  38. package/dist/application/JavaScriptArtifactGraphBuilder.js +23 -1
  39. package/dist/application/JavaScriptArtifactGraphBundlers.js +225 -0
  40. package/dist/application/JavaScriptArtifactGraphContext.js +37 -36
  41. package/dist/application/JavaScriptArtifactGraphDocuments.js +27 -4
  42. package/dist/application/JavaScriptArtifactGraphFindings.js +54 -8
  43. package/dist/application/JavaScriptArtifactGraphStructure.js +29 -101
  44. package/dist/application/JavaScriptArtifactPathResolution.js +269 -0
  45. package/dist/application/JavaScriptArtifactReconstruction.js +1 -1
  46. package/dist/application/JavaScriptJsonModules.js +39 -0
  47. package/dist/application/JavaScriptModuleRelationships.js +262 -0
  48. package/dist/application/JavaScriptReplayPermission.js +16 -0
  49. package/dist/application/JavaScriptReplayService.js +182 -156
  50. package/dist/application/LinuxHopper.js +10 -0
  51. package/dist/application/MacHopper.js +22 -11
  52. package/dist/application/NodeRuntimeCharacterizationService.js +196 -0
  53. package/dist/application/PermissionAuthority.js +106 -63
  54. package/dist/application/ProcessCaptureCapability.js +1 -1
  55. package/dist/application/ProcessCapturePermission.js +13 -0
  56. package/dist/application/ProcessCli.js +2 -12
  57. package/dist/application/ProcessHarness.js +1 -1
  58. package/dist/application/ProcessPairedExperiment.js +72 -0
  59. package/dist/application/ProjectPermissionStore.js +48 -9
  60. package/dist/application/RealHopperAssertions.js +76 -0
  61. package/dist/application/ReconstructionCoverageService.js +69 -0
  62. package/dist/application/ReconstructionCoverageWorkspaceStore.js +31 -0
  63. package/dist/application/RuntimeExecutableDiagnostics.js +172 -0
  64. package/dist/application/Setup.js +148 -260
  65. package/dist/application/SetupClientConfiguration.js +224 -0
  66. package/dist/application/SetupClients.js +24 -2
  67. package/dist/application/SetupPlan.js +124 -12
  68. package/dist/application/SetupSkill.js +54 -36
  69. package/dist/application/SupportedClients.js +1 -0
  70. package/dist/application/Uninstall.js +12 -3
  71. package/dist/application/Upgrade.js +16 -1
  72. package/dist/application/WindowsCapabilities.js +86 -0
  73. package/dist/artifacts/AsarArtifactReader.js +11 -0
  74. package/dist/browser/BrowserProcessStartup.js +88 -0
  75. package/dist/browser/CdpCaptureDocuments.js +12 -8
  76. package/dist/browser/CdpCaptureEventBodyShapes.js +75 -0
  77. package/dist/browser/CdpCaptureEventHandlers.js +336 -0
  78. package/dist/browser/CdpCaptureEventHelpers.js +71 -0
  79. package/dist/browser/CdpCaptureEventState.js +1 -0
  80. package/dist/browser/CdpCaptureEventTypes.js +1 -0
  81. package/dist/browser/CdpCaptureEvents.js +29 -496
  82. package/dist/browser/CdpObservationSession.js +51 -8
  83. package/dist/browser/CdpPageCapture.js +12 -133
  84. package/dist/browser/CdpPageCaptureScripts.js +97 -0
  85. package/dist/browser/CdpPageCaptureWorkers.js +46 -0
  86. package/dist/browser/CdpWebMcpDiscovery.js +38 -14
  87. package/dist/browser/PngVisualDiff.js +9 -2
  88. package/dist/catalogIdentity.js +4 -0
  89. package/dist/cli/artifactCommands.js +82 -0
  90. package/dist/cli/coreAnalysisCommands.js +232 -0
  91. package/dist/cli/managedCommands.js +397 -0
  92. package/dist/cli/managedProjectGraphCommand.js +24 -0
  93. package/dist/cli/options.js +9 -0
  94. package/dist/cli/setupCommands.js +113 -0
  95. package/dist/cli/types.js +1 -0
  96. package/dist/cli/utilityCommands.js +122 -0
  97. package/dist/cli.js +10 -768
  98. package/dist/cliApplicationCommands.js +118 -4
  99. package/dist/cliBrowserAdvancedCommands.js +73 -16
  100. package/dist/cliBrowserCommands.js +39 -115
  101. package/dist/cliCommandNames.js +4 -0
  102. package/dist/cliElectronCommands.js +33 -62
  103. package/dist/cliEvidenceCommands.js +13 -2
  104. package/dist/cliInvestigationCommands.js +53 -10
  105. package/dist/cliObservationOptions.js +137 -0
  106. package/dist/cliPolicyCommands.js +80 -46
  107. package/dist/cliSetup.js +306 -0
  108. package/dist/config/environment.js +107 -0
  109. package/dist/config/parseConfig.js +228 -0
  110. package/dist/config/parsers.js +68 -0
  111. package/dist/config/permissions.js +30 -0
  112. package/dist/config/types.js +1 -0
  113. package/dist/config.js +1 -375
  114. package/dist/contracts/applicationToolContracts.js +134 -0
  115. package/dist/contracts/electronToolContracts.js +51 -3
  116. package/dist/contracts/managedToolContracts.js +8 -3
  117. package/dist/contracts/managedWorkflowToolContracts.js +3 -3
  118. package/dist/contracts/providerSelection.js +2 -0
  119. package/dist/contracts/sessionStatusContract.js +26 -0
  120. package/dist/contracts/toolContractTypes.js +12 -1
  121. package/dist/contracts/toolContracts.js +5 -12
  122. package/dist/contracts/toolEffects.js +14 -0
  123. package/dist/contracts/toolOutputSchemaGroups.js +274 -0
  124. package/dist/contracts/toolOutputSchemaPrimitives.js +328 -0
  125. package/dist/contracts/toolOutputSchemas.js +2 -550
  126. package/dist/domain/analysisErrorPresentation.js +234 -0
  127. package/dist/domain/analysisErrorProjection.js +284 -0
  128. package/dist/domain/androidApplication.js +206 -0
  129. package/dist/domain/appleApplication.js +221 -0
  130. package/dist/domain/artifactComparison.js +34 -15
  131. package/dist/domain/binaryTarget.js +87 -37
  132. package/dist/domain/boundedCartesianProjection.js +15 -0
  133. package/dist/domain/browserObservation.js +1 -285
  134. package/dist/domain/browserObservationSchemas.js +286 -0
  135. package/dist/domain/electronStaticAnalysisBrowser.js +14 -2
  136. package/dist/domain/errors.js +4 -486
  137. package/dist/domain/functionComparison.js +4 -165
  138. package/dist/domain/functionComparisonDimensions.js +164 -0
  139. package/dist/domain/javascriptApplicationVersionItems.js +18 -6
  140. package/dist/domain/javascriptApplicationVersionKeys.js +8 -8
  141. package/dist/domain/javascriptExportInstrumentation.js +97 -0
  142. package/dist/domain/javascriptFeatureTraversal.js +19 -7
  143. package/dist/domain/javascriptRuntimeStaticCandidates.js +3 -1
  144. package/dist/domain/javascriptSemanticAnalysis.js +386 -0
  145. package/dist/domain/javascriptSemanticIr.js +30 -0
  146. package/dist/domain/javascriptSemanticProjection.js +366 -0
  147. package/dist/domain/javascriptSemanticState.js +40 -0
  148. package/dist/domain/javascriptSemanticTraversal.js +22 -0
  149. package/dist/domain/javascriptSemanticValues.js +352 -0
  150. package/dist/domain/javascriptStaticAnalysis.js +5 -221
  151. package/dist/domain/javascriptStaticAnalysisBundler.js +209 -0
  152. package/dist/domain/javascriptStaticAnalysisCalls.js +180 -0
  153. package/dist/domain/javascriptStaticAnalysisHelpers.js +71 -2
  154. package/dist/domain/knownPageCoverage.js +21 -0
  155. package/dist/domain/managedApplicationGraph.js +14 -364
  156. package/dist/domain/managedApplicationGraphCoverage.js +170 -0
  157. package/dist/domain/managedApplicationGraphNodeCoverage.js +6 -0
  158. package/dist/domain/managedApplicationGraphNodes.js +336 -0
  159. package/dist/domain/managedArtifact.js +16 -8
  160. package/dist/domain/managedMemberComparison.js +37 -391
  161. package/dist/domain/managedMemberComparisonCoverage.js +72 -0
  162. package/dist/domain/managedMemberComparisonMatch.js +356 -0
  163. package/dist/domain/managedNativeVerification.js +27 -389
  164. package/dist/domain/managedNativeVerificationMatch.js +272 -0
  165. package/dist/domain/managedNativeVerificationSchemas.js +141 -0
  166. package/dist/domain/managedReconstruction.js +82 -69
  167. package/dist/domain/managedRuntimeCorrelation.js +12 -3
  168. package/dist/domain/nodeRuntimeCharacterization.js +66 -0
  169. package/dist/domain/permissionPolicy.js +2 -0
  170. package/dist/domain/processComparison.js +19 -0
  171. package/dist/domain/processPairedExperiment.js +128 -0
  172. package/dist/domain/reconstructionCoverage.js +387 -0
  173. package/dist/domain/reconstructionCoverageEvaluation.js +212 -0
  174. package/dist/domain/reconstructionVerificationSchemas.js +8 -0
  175. package/dist/domain/replayMachine.js +316 -0
  176. package/dist/domain/replayMachineRuntime.js +215 -0
  177. package/dist/domain/runtimeCharacterization.js +85 -0
  178. package/dist/domain/runtimeIdentification.js +210 -0
  179. package/dist/domain/webBundleAnalyzer.js +3 -409
  180. package/dist/domain/webBundleAnalyzerAst.js +77 -0
  181. package/dist/domain/webBundleAnalyzerInspection.js +307 -0
  182. package/dist/domain/webBundleAnalyzerResult.js +120 -0
  183. package/dist/dotnet/ManagedArtifactInspector.js +50 -29
  184. package/dist/dotnet/ManagedMemberInspector.js +51 -785
  185. package/dist/dotnet/ManagedMemberInspectorCore.js +297 -0
  186. package/dist/dotnet/ManagedMemberInstructionDecoder.js +427 -0
  187. package/dist/dotnet/ManagedMemberRows.js +120 -0
  188. package/dist/dotnet/ManagedMetadataInventory.js +42 -306
  189. package/dist/dotnet/ManagedMetadataInventoryRows.js +292 -0
  190. package/dist/dotnet/ManagedMetadataLayout.js +48 -111
  191. package/dist/dotnet/ManagedMetadataRowSizes.js +54 -0
  192. package/dist/dotnet/ManagedMethodBodyReader.js +143 -0
  193. package/dist/dotnet/ManagedNativeBoundaryHelpers.js +311 -0
  194. package/dist/dotnet/ManagedNativeBoundaryInspector.js +102 -350
  195. package/dist/dotnet/ManagedPeReader.js +59 -28
  196. package/dist/evaluation/CodexAgentEval.js +132 -0
  197. package/dist/generatedPackageMetadata.js +2 -2
  198. package/dist/ghidra/GhidraClient.js +76 -91
  199. package/dist/ghidra/GhidraClientStartup.js +29 -0
  200. package/dist/ghidra/GhidraClientWire.js +34 -0
  201. package/dist/ghidra/GhidraDefaults.js +1 -1
  202. package/dist/ghidra/GhidraDiagnostics.js +6 -1
  203. package/dist/ghidra/GhidraInstallation.js +20 -15
  204. package/dist/ghidra/GhidraLauncher.js +93 -29
  205. package/dist/ghidra/GhidraProvider.js +61 -161
  206. package/dist/ghidra/GhidraProviderCapabilities.js +173 -0
  207. package/dist/ghidra/GhidraSessionValues.js +2 -0
  208. package/dist/ghidra/GhidraSocketConnection.js +44 -7
  209. package/dist/ghidra/GhidraTargetSnapshot.js +30 -0
  210. package/dist/ghidra/GhidraTransport.js +71 -0
  211. package/dist/identity.js +4 -2
  212. package/dist/main/messages.js +5 -0
  213. package/dist/main/reload.js +49 -0
  214. package/dist/main/shutdown.js +25 -0
  215. package/dist/main/startup.js +14 -0
  216. package/dist/main/state.js +26 -0
  217. package/dist/main/transport.js +61 -0
  218. package/dist/main/types.js +1 -0
  219. package/dist/main.js +35 -146
  220. package/dist/process/PrivateRuntimeRoot.js +3 -2
  221. package/dist/process/ProcessOwnership.js +35 -0
  222. package/dist/process/ProviderProcess.js +2 -0
  223. package/dist/replay/JavaScriptReplayWorker.js +64 -50
  224. package/dist/replay/LinuxJavaScriptReplayRunner.js +173 -390
  225. package/dist/replay/LinuxJavaScriptReplaySandbox.js +140 -0
  226. package/dist/replay/ReplayOutcome.js +95 -0
  227. package/dist/replay/ReplayProcessLifecycle.js +71 -0
  228. package/dist/server/ProcessCaptureElicitation.js +157 -0
  229. package/dist/server/createServer.js +124 -56
  230. package/dist/server/javascriptApplicationResult.js +85 -0
  231. package/dist/server/registerApplicationTools/characterization.js +57 -0
  232. package/dist/server/registerApplicationTools/compareVersions.js +34 -0
  233. package/dist/server/registerApplicationTools/controlledReplay.js +38 -0
  234. package/dist/server/registerApplicationTools/coverage.js +84 -0
  235. package/dist/server/registerApplicationTools/helpers.js +42 -0
  236. package/dist/server/registerApplicationTools/traceFeature.js +26 -0
  237. package/dist/server/registerApplicationTools/types.js +1 -0
  238. package/dist/server/registerApplicationTools.js +10 -112
  239. package/dist/server/registerBrowserTools.js +59 -83
  240. package/dist/server/registerElectronTools.js +48 -49
  241. package/dist/server/registerEnhancedTools.js +34 -42
  242. package/dist/server/registerEvidenceResources.js +56 -0
  243. package/dist/server/registerInvestigationTools/helpers.js +74 -0
  244. package/dist/server/registerInvestigationTools/types.js +1 -0
  245. package/dist/server/registerInvestigationTools.js +83 -116
  246. package/dist/server/registerJavaScriptApplicationGraphResource.js +69 -0
  247. package/dist/server/registerManagedTools.js +18 -1
  248. package/dist/server/registerManagedWorkflowTools/compareManagedMembers.js +65 -0
  249. package/dist/server/registerManagedWorkflowTools/contract.js +8 -0
  250. package/dist/server/registerManagedWorkflowTools/evidence.js +43 -0
  251. package/dist/server/registerManagedWorkflowTools/importManagedReconstruction.js +39 -0
  252. package/dist/server/registerManagedWorkflowTools/planManagedRuntimeCorrelation.js +39 -0
  253. package/dist/server/registerManagedWorkflowTools/projectManagedApplicationGraph.js +52 -0
  254. package/dist/server/registerManagedWorkflowTools/types.js +1 -0
  255. package/dist/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.js +75 -0
  256. package/dist/server/registerManagedWorkflowTools.js +10 -274
  257. package/dist/server/registerOfficialTools.js +73 -59
  258. package/dist/server/registerSessionRecordTools.js +137 -0
  259. package/dist/server/registerSessionStatusTool.js +97 -26
  260. package/dist/server/registerSessionTools.js +60 -164
  261. package/dist/server/sessionAvailabilityPolicy.js +10 -0
  262. package/dist/server/toolRegistrationOptions.js +101 -3
  263. package/dist/server/toolResult.js +15 -4
  264. package/package.json +11 -11
  265. package/skills/reverse-engineer-anything/SKILL.md +76 -0
  266. package/skills/reverse-engineer-anything/references/controlled-replay.md +12 -0
  267. package/skills/reverse-engineer-anything/references/evidence-workflows.md +33 -0
  268. package/skills/reverse-engineer-anything/references/javascript-applications.md +25 -0
  269. package/skills/reverse-engineer-anything/references/native-and-artifacts.md +36 -0
  270. package/skills/reverse-engineer-anything/references/runtime-observation.md +24 -0
  271. package/scripts/prepare-node-pty.mjs +0 -35
  272. package/scripts/rebuild-native.mjs +0 -22
  273. package/skills/rea-analysis/SKILL.md +0 -222
@@ -10,6 +10,16 @@ const RELEASES_URL = "https://www.hopperapp.com/include/files-api.php?request=re
10
10
  const DOWNLOAD_PREFIX = "https://www.hopperapp.com:443/downloader/public/";
11
11
  const MAX_PACKAGE_BYTES = 100_000_000;
12
12
  const BUNDLE_IDENTIFIER = "com.cryptic-apps.hopper-web-4";
13
+ /** Stable network and command effects disclosed before macOS Hopper setup. */
14
+ export const macHopperInstallDisclosure = {
15
+ networkOrigins: [RELEASES_URL, DOWNLOAD_PREFIX],
16
+ commands: [
17
+ "hdiutil attach -readonly -nobrowse -mountpoint <private-mount> <verified-hopper.dmg>",
18
+ "codesign --verify --deep --strict <mounted-hopper.app>",
19
+ "ditto <mounted-hopper.app> <user-Applications-stage>",
20
+ "open <installed-hopper.app>",
21
+ ],
22
+ };
13
23
  /** Download, verify, and install Hopper into the current user's Applications directory. */
14
24
  export const installMacHopper = async (options = {}, host = systemMacHopperInstallHost()) => {
15
25
  let temporary;
@@ -66,17 +76,7 @@ export const installMacHopper = async (options = {}, host = systemMacHopperInsta
66
76
  }
67
77
  };
68
78
  const systemMacHopperInstallHost = () => ({
69
- async download(url) {
70
- const response = await fetch(url, {
71
- headers: { accept: "application/json", "user-agent": "rea-installer" },
72
- signal: AbortSignal.timeout(120_000),
73
- });
74
- const length = Number(response.headers.get("content-length") ?? "0");
75
- if (length > MAX_PACKAGE_BYTES)
76
- return { ok: false, bytes: new Uint8Array() };
77
- const bytes = new Uint8Array(await response.arrayBuffer());
78
- return { ok: response.ok && bytes.byteLength <= MAX_PACKAGE_BYTES, bytes };
79
- },
79
+ download: downloadPackage,
80
80
  createTemporaryDirectory: () => mkdtemp(join(tmpdir(), "rea-hopper-mac-")),
81
81
  createMountDirectory: (path) => mkdir(path),
82
82
  writePackage: (path, bytes) => writeFile(path, bytes, { mode: 0o600 }),
@@ -170,6 +170,17 @@ const systemMacHopperInstallHost = () => ({
170
170
  cleanup: (path) => rm(path, { recursive: true, force: true }),
171
171
  destination: () => join(homedir(), "Applications/Hopper Disassembler.app"),
172
172
  });
173
+ const downloadPackage = async (url) => {
174
+ const response = await fetch(url, {
175
+ headers: { accept: "application/json", "user-agent": "rea-installer" },
176
+ signal: AbortSignal.timeout(120_000),
177
+ });
178
+ const length = Number(response.headers.get("content-length") ?? "0");
179
+ if (length > MAX_PACKAGE_BYTES)
180
+ return { ok: false, bytes: new Uint8Array() };
181
+ const bytes = new Uint8Array(await response.arrayBuffer());
182
+ return { ok: response.ok && bytes.byteLength <= MAX_PACKAGE_BYTES, bytes };
183
+ };
173
184
  const parseJson = (bytes) => JSON.parse(new TextDecoder().decode(bytes));
174
185
  const integrityMatches = (bytes, release) => bytes.byteLength === Number(release.file_length) &&
175
186
  bytes.byteLength <= MAX_PACKAGE_BYTES &&
@@ -0,0 +1,196 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { nodeCharacterizationExecutionInputSchema, nodeCharacterizationExecutionOutputSchema, nodeCharacterizationPreparationInputSchema, nodeCharacterizationPreparationOutputSchema, } from "../domain/nodeRuntimeCharacterization.js";
4
+ import { AnalysisInputError, AnalysisProtocolError, ReplayPlanStaleError, } from "../domain/errors.js";
5
+ import { createEvidence } from "../domain/evidence.js";
6
+ import { controlledReplayOutputSchema } from "../domain/javascriptReplay.js";
7
+ import { createRuntimeCharacterizationPlan, } from "../domain/runtimeCharacterization.js";
8
+ import { err, ok } from "../domain/result.js";
9
+ import { jsonValueSchema } from "../domain/jsonValue.js";
10
+ import { InstrumentedJavaScriptReplayHost } from "./InstrumentedJavaScriptReplayHost.js";
11
+ import { runControlledReplayValidated, } from "./JavaScriptReplayService.js";
12
+ const PREPARE_OPERATION = "prepare_node_characterization";
13
+ const EXECUTE_OPERATION = "execute_node_characterization";
14
+ const PROVIDER = {
15
+ id: "rea-node-characterization",
16
+ name: "REA Node runtime characterization",
17
+ version: "1",
18
+ };
19
+ /** Prepare exact Node/JavaScript characterization without executing target code. */
20
+ export const prepareNodeCharacterization = async (dependencies, rawInput, options = {}) => {
21
+ const parsed = nodeCharacterizationPreparationInputSchema.safeParse(rawInput);
22
+ if (!parsed.success)
23
+ return err(new AnalysisInputError(PREPARE_OPERATION, { cause: parsed.error }));
24
+ const prepared = await prepareValidated(dependencies, parsed.data, options);
25
+ return prepared.ok
26
+ ? ok(jsonValueSchema.parse(nodeCharacterizationPreparationOutputSchema.parse(prepared.value)))
27
+ : prepared;
28
+ };
29
+ /** Execute only a freshly recomputed and separately approved characterization plan. */
30
+ export const executeNodeCharacterization = async (dependencies, rawInput, options = {}) => {
31
+ const parsed = nodeCharacterizationExecutionInputSchema.safeParse(rawInput);
32
+ if (!parsed.success)
33
+ return err(new AnalysisInputError(EXECUTE_OPERATION, { cause: parsed.error }));
34
+ const prepared = await prepareValidated(dependencies, parsed.data.preparation, options);
35
+ if (!prepared.ok)
36
+ return prepared;
37
+ if (prepared.value.plan.plan_sha256 !== parsed.data.approved_plan_sha256)
38
+ return err(new ReplayPlanStaleError(parsed.data.approved_plan_sha256, prepared.value.plan.plan_sha256));
39
+ const plannedReplay = controlledReplayOutputSchema.parse(prepared.value.replay);
40
+ if (plannedReplay.phase !== "plan" || plannedReplay.plan === null)
41
+ return err(new AnalysisProtocolError("Node characterization preparation did not produce a replay plan"));
42
+ const host = new InstrumentedJavaScriptReplayHost(dependencies.host, parsed.data.preparation.instrumentation);
43
+ const executed = await runControlledReplayValidated({ ...dependencies, host }, {
44
+ ...parsed.data.preparation.replay,
45
+ mode: "execute",
46
+ approved: true,
47
+ plan_digest: plannedReplay.plan.plan_digest,
48
+ }, options);
49
+ if (!executed.ok)
50
+ return executed;
51
+ if (host.instrumentation === null)
52
+ return err(new AnalysisProtocolError("Node characterization execution omitted instrumentation evidence"));
53
+ return ok(jsonValueSchema.parse(nodeCharacterizationExecutionOutputSchema.parse({
54
+ schema_version: 1,
55
+ phase: "execution",
56
+ plan: prepared.value.plan,
57
+ transformation: host.instrumentation.manifest,
58
+ transformation_evidence: prepared.value.transformation_evidence,
59
+ evidence: createCharacterizationEvidence(prepared.value, controlledReplayOutputSchema.parse(executed.value)),
60
+ replay: controlledReplayOutputSchema.parse(executed.value),
61
+ })));
62
+ };
63
+ const prepareValidated = async (dependencies, input, options) => {
64
+ const host = new InstrumentedJavaScriptReplayHost(dependencies.host, input.instrumentation);
65
+ const replay = await runControlledReplayValidated({ ...dependencies, host }, input.replay, options);
66
+ if (!replay.ok)
67
+ return replay;
68
+ const output = controlledReplayOutputSchema.parse(replay.value);
69
+ if (output.phase !== "plan" ||
70
+ output.plan === null ||
71
+ host.instrumentation === null)
72
+ return err(new AnalysisProtocolError("Node characterization preparation omitted committed plan evidence"));
73
+ const module = output.plan.left.modules.find(({ alias }) => alias === input.selected_alias);
74
+ if (module === undefined)
75
+ return err(new AnalysisProtocolError("Node characterization replay plan omitted selected module"));
76
+ const plan = createCharacterizationPlan(input, host.instrumentation, output.plan);
77
+ return ok({
78
+ schema_version: 1,
79
+ phase: "preparation",
80
+ plan,
81
+ transformation: host.instrumentation.manifest,
82
+ transformation_evidence: createTransformationEvidence(input, host.instrumentation),
83
+ replay: output,
84
+ });
85
+ };
86
+ const createTransformationEvidence = (input, instrumentation) => createEvidence({
87
+ path: input.instrumentation.artifact_path,
88
+ sha256: input.instrumentation.artifact_sha256,
89
+ format: "javascript-bundle",
90
+ }, PROVIDER, {
91
+ predicateType: "rea.javascript-export-transformation/v1",
92
+ operation: PREPARE_OPERATION,
93
+ parameters: {
94
+ selected_alias: input.selected_alias,
95
+ selected_sha256: input.instrumentation.selection.selected_sha256,
96
+ },
97
+ result: jsonValueSchema.parse(instrumentation.manifest),
98
+ confidence: "derived",
99
+ authority: "shipped-artifact",
100
+ locations: [
101
+ {
102
+ kind: "file-offset-range",
103
+ start: input.instrumentation.selection.byte_start,
104
+ end: input.instrumentation.selection.byte_end,
105
+ },
106
+ ],
107
+ limitations: [
108
+ "The transformation is deterministic derived evidence; execution authority is established only by a separately approved characterization run.",
109
+ ],
110
+ });
111
+ const createCharacterizationEvidence = (prepared, replay) => {
112
+ if (replay.phase !== "execute" || replay.evidence === null)
113
+ throw new AnalysisProtocolError("Node characterization execution omitted replay Evidence");
114
+ return createEvidence({
115
+ path: prepared.plan.artifact.path,
116
+ sha256: prepared.plan.artifact.sha256,
117
+ format: "javascript-bundle",
118
+ }, PROVIDER, {
119
+ predicateType: "rea.runtime-characterization/v1",
120
+ operation: EXECUTE_OPERATION,
121
+ parameters: {
122
+ plan_sha256: prepared.plan.plan_sha256,
123
+ replay_evidence_id: replay.evidence.evidence_id,
124
+ },
125
+ result: jsonValueSchema.parse({
126
+ plan: prepared.plan,
127
+ transformation_evidence_id: prepared.transformation_evidence.evidence_id,
128
+ replay_evidence_id: replay.evidence.evidence_id,
129
+ execution: replay.evidence.normalized_result,
130
+ }),
131
+ confidence: "observed",
132
+ authority: "controlled-replay",
133
+ environment: replay.evidence.environment,
134
+ limitations: [
135
+ "This Evidence proves only the finite approved characterization plan and transformed controlled-replay authority, not unmodified shipped-runtime equivalence.",
136
+ ...replay.evidence.limitations,
137
+ ],
138
+ evidenceLinks: [
139
+ prepared.transformation_evidence.evidence_id,
140
+ replay.evidence.evidence_id,
141
+ ],
142
+ });
143
+ };
144
+ const createCharacterizationPlan = (input, instrumentation, replayPlan) => createRuntimeCharacterizationPlan({
145
+ schema_version: 1,
146
+ preparation_sha256: digestPreparation(instrumentation.manifest.instrumented_sha256, replayPlan.plan_digest),
147
+ artifact: {
148
+ path: input.instrumentation.artifact_path,
149
+ sha256: input.instrumentation.artifact_sha256,
150
+ byte_length: instrumentation.manifest.original_byte_length,
151
+ },
152
+ runtime: {
153
+ family: "ecmascript",
154
+ provider_id: "node-javascript",
155
+ executable_path: replayPlan.runtime.executable.path,
156
+ executable_sha256: replayPlan.runtime.executable.sha256,
157
+ version: replayPlan.runtime.executable.version,
158
+ profile_sha256: replayPlan.policy_sha256,
159
+ },
160
+ callable: {
161
+ callable_id: `callable/${input.instrumentation.selection.selected_sha256}`,
162
+ module_id: stableModuleId(input.selected_alias),
163
+ export_name: input.instrumentation.selection.export_name,
164
+ semantic_evidence_id: null,
165
+ selector_sha256: input.instrumentation.selection.selected_sha256,
166
+ },
167
+ working_directory: "/work",
168
+ isolated_home: "/work",
169
+ expected_effect: input.expected_effect,
170
+ allowed_boundaries: [],
171
+ limits: {
172
+ max_calls: replayPlan.cases.length,
173
+ max_processes: 0,
174
+ max_files: 0,
175
+ max_bytes: replayPlan.limits.output_bytes,
176
+ max_handles: replayPlan.limits.tasks,
177
+ timeout_ms: replayPlan.limits.wall_time_ms,
178
+ idle_timeout_ms: replayPlan.limits.wall_time_ms,
179
+ },
180
+ determinism: {
181
+ clock: "fixed",
182
+ randomness: "seeded",
183
+ identifiers: "deterministic",
184
+ seed: replayPlan.determinism.random_seed,
185
+ },
186
+ authority: {
187
+ preparation_approved: true,
188
+ execution_approved: false,
189
+ network: "none",
190
+ provider_owned_process_only: true,
191
+ },
192
+ });
193
+ const digestPreparation = (instrumentedSha256, replayPlanDigest) => createHash("sha256")
194
+ .update(`rea.node-characterization-preparation/v1\0${instrumentedSha256}\0${replayPlanDigest}`)
195
+ .digest("hex");
196
+ const stableModuleId = (alias) => `module/${createHash("sha256").update(alias).digest("hex")}`;
@@ -73,31 +73,52 @@ const errorReason = (error) => isNotFound(error) ? "not_found" : "io";
73
73
  /** Stateful owner for permission evaluation, consumption, reload, and revoke. */
74
74
  export class PermissionAuthority {
75
75
  policy;
76
- constructor(policy) {
76
+ configuredAuthority;
77
+ constructor(policy, configuredAuthority) {
77
78
  this.policy = policy;
79
+ this.configuredAuthority = configuredAuthority;
80
+ }
81
+ /** Create a connection-owned overlay for once and session grants. */
82
+ createConnectionAuthority() {
83
+ return new PermissionAuthority(createPermissionPolicy([]), this.configuredAuthority ?? this);
78
84
  }
79
85
  /** Canonicalize and authorize one exact operation before its side effect. */
80
86
  async authorize(request, access, options = {}) {
81
- const evaluated = await this.explain(request, access, options);
87
+ const evaluated = await this.evaluateRequest(request, access, options);
82
88
  if (!evaluated.ok)
83
89
  return evaluated;
84
- const decision = evaluated.value;
85
- this.policy = consumePermission(this.policy, decision);
86
- return ok(decision);
90
+ const { decision, owner, request: canonicalRequest } = evaluated.value;
91
+ owner.policy = consumePermission(owner.policy, decision);
92
+ return ok({ ...decision, request: canonicalRequest });
87
93
  }
88
94
  /** Dry-run the exact shared evaluation without consuming a once grant. */
89
95
  async explain(request, access, options = {}) {
96
+ const evaluated = await this.evaluateRequest(request, access, options);
97
+ return evaluated.ok
98
+ ? ok({
99
+ ...evaluated.value.decision,
100
+ request: evaluated.value.request,
101
+ })
102
+ : evaluated;
103
+ }
104
+ async evaluateRequest(request, access, options) {
90
105
  const canonical = await canonicalizePermissionRequest(request, access);
91
106
  if (!canonical.ok)
92
107
  return canonical;
93
- const decision = evaluatePermission(this.policy, canonical.value);
94
- if (!decision.allowed)
95
- return err(new PermissionRequiredError(canonical.value, decision.missing, ceilingFor(this.policy, canonical.value.capability), options.elicitationSupported ?? false, options.restartRequired ?? false));
96
- return ok({ ...decision, request: canonical.value });
108
+ const evaluated = this.evaluateCanonical(canonical.value, options);
109
+ return evaluated.ok
110
+ ? ok({ ...evaluated.value, request: canonical.value })
111
+ : evaluated;
97
112
  }
98
113
  /** Add authority beneath the administrator ceiling. */
99
114
  grant(grant) {
100
- const result = grantPermission(this.policy, grant);
115
+ const policy = this.configuredAuthority === undefined
116
+ ? this.policy
117
+ : {
118
+ ...this.policy,
119
+ ceilings: this.configuredAuthority.effectivePolicy().ceilings,
120
+ };
121
+ const result = grantPermission(policy, grant);
101
122
  if (result.ok)
102
123
  this.policy = result.value;
103
124
  return result;
@@ -110,44 +131,61 @@ export class PermissionAuthority {
110
131
  clearSessionGrants() {
111
132
  this.policy = clearSessionPermissions(this.policy);
112
133
  }
113
- /** Atomically replace optional persisted project grants. */
114
- async replaceProjectGrants(grants) {
115
- let candidate = {
116
- ...this.policy,
117
- grants: this.policy.grants.filter(({ lifetime }) => lifetime !== "project"),
134
+ effectivePolicy() {
135
+ if (this.configuredAuthority === undefined)
136
+ return this.policy;
137
+ const configured = this.configuredAuthority.effectivePolicy();
138
+ return {
139
+ ceilings: configured.ceilings,
140
+ grants: [...configured.grants, ...this.policy.grants],
141
+ revoked_grant_ids: new Set([
142
+ ...configured.revoked_grant_ids,
143
+ ...this.policy.revoked_grant_ids,
144
+ ]),
145
+ consumed_grant_ids: new Set([
146
+ ...configured.consumed_grant_ids,
147
+ ...this.policy.consumed_grant_ids,
148
+ ]),
118
149
  };
119
- for (const grant of grants) {
120
- const canonical = await canonicalizeGrant(grant);
121
- if (!canonical.ok)
122
- return canonical;
123
- const added = grantPermission(candidate, canonical.value);
124
- if (!added.ok)
125
- return err(new PermissionPathError(grant.grant_id, "io", {
126
- cause: added.error,
127
- }));
128
- candidate = added.value;
150
+ }
151
+ evaluateCanonical(request, options) {
152
+ const configured = this.configuredAuthority;
153
+ if (configured !== undefined) {
154
+ const configuredDecision = evaluatePermission(configured.effectivePolicy(), request);
155
+ if (configuredDecision.allowed)
156
+ return ok({ decision: configuredDecision, owner: configured });
129
157
  }
130
- this.policy = candidate;
131
- return ok(null);
158
+ const localPolicy = configured === undefined
159
+ ? this.policy
160
+ : { ...this.policy, ceilings: configured.effectivePolicy().ceilings };
161
+ const localDecision = evaluatePermission(localPolicy, request);
162
+ if (localDecision.allowed)
163
+ return ok({ decision: localDecision, owner: this });
164
+ const effective = this.effectivePolicy();
165
+ const denied = evaluatePermission(effective, request);
166
+ if (denied.allowed)
167
+ return ok({ decision: denied, owner: this });
168
+ return err(new PermissionRequiredError(request, denied.missing, ceilingFor(effective, request.capability), options.elicitationSupported ?? false, options.restartRequired ?? false));
169
+ }
170
+ /** Atomically replace optional persisted project grants. */
171
+ async replaceProjectGrants(grants) {
172
+ return this.replaceGrants(grants, "project");
132
173
  }
133
174
  /** Replace grants derived from the currently loaded administrator ceiling. */
134
175
  async replaceAdministratorGrants(grants) {
135
- let candidate = {
176
+ return this.replaceGrants(grants, "administrator");
177
+ }
178
+ async replaceGrants(grants, lifetime) {
179
+ const canonical = await canonicalizeGrants(grants);
180
+ if (!canonical.ok)
181
+ return canonical;
182
+ const replaced = addGrantsToPolicy({
136
183
  ...this.policy,
137
- grants: this.policy.grants.filter(({ lifetime }) => lifetime !== "administrator"),
138
- };
139
- for (const grant of grants) {
140
- const canonical = await canonicalizeGrant(grant);
141
- if (!canonical.ok)
142
- return canonical;
143
- const added = grantPermission(candidate, canonical.value);
144
- if (!added.ok)
145
- return err(new PermissionPathError(grant.grant_id, "io", {
146
- cause: added.error,
147
- }));
148
- candidate = added.value;
149
- }
150
- this.policy = candidate;
184
+ grants: this.policy.grants.filter((grant) => grant.lifetime !== lifetime),
185
+ }, canonical.value);
186
+ if (!replaced.ok)
187
+ return replaced;
188
+ this.policy = replaced.value;
151
189
  return ok(null);
152
190
  }
153
191
  /** Atomically replace reloadable ceilings and persisted grants. */
@@ -155,29 +193,20 @@ export class PermissionAuthority {
155
193
  const ceilings = await canonicalizePermissionCeilings(configuration.ceilings);
156
194
  if (!ceilings.ok)
157
195
  return ceilings;
158
- const grants = [];
159
- for (const grant of [
196
+ const grants = await canonicalizeGrants([
160
197
  ...configuration.administratorGrants,
161
198
  ...configuration.projectGrants,
162
- ]) {
163
- const canonical = await canonicalizeGrant(grant);
164
- if (!canonical.ok)
165
- return canonical;
166
- grants.push(canonical.value);
167
- }
168
- let candidate = {
199
+ ]);
200
+ if (!grants.ok)
201
+ return grants;
202
+ const candidate = {
169
203
  ...reloadPermissionCeilings(this.policy, ceilings.value),
170
204
  grants: this.policy.grants.filter(({ lifetime }) => lifetime !== "administrator" && lifetime !== "project"),
171
205
  };
172
- for (const grant of grants) {
173
- const added = grantPermission(candidate, grant);
174
- if (!added.ok)
175
- return err(new PermissionPathError(grant.grant_id, "io", {
176
- cause: added.error,
177
- }));
178
- candidate = added.value;
179
- }
180
- this.policy = candidate;
206
+ const replaced = addGrantsToPolicy(candidate, grants.value);
207
+ if (!replaced.ok)
208
+ return replaced;
209
+ this.policy = replaced.value;
181
210
  return ok(null);
182
211
  }
183
212
  /** Replace ceilings without restarting the MCP process. */
@@ -194,17 +223,31 @@ export const createPermissionAuthority = async (ceilings, grants = []) => {
194
223
  const canonical = await canonicalizePermissionCeilings(ceilings);
195
224
  if (!canonical.ok)
196
225
  return canonical;
197
- let policy = createPermissionPolicy(canonical.value);
226
+ const canonicalGrants = await canonicalizeGrants(grants);
227
+ if (!canonicalGrants.ok)
228
+ return canonicalGrants;
229
+ const policy = addGrantsToPolicy(createPermissionPolicy(canonical.value), canonicalGrants.value);
230
+ return policy.ok ? ok(new PermissionAuthority(policy.value)) : policy;
231
+ };
232
+ const canonicalizeGrants = async (grants) => {
233
+ const canonical = [];
198
234
  for (const grant of grants) {
199
235
  const canonicalGrant = await canonicalizeGrant(grant);
200
236
  if (!canonicalGrant.ok)
201
237
  return canonicalGrant;
202
- const added = grantPermission(policy, canonicalGrant.value);
238
+ canonical.push(canonicalGrant.value);
239
+ }
240
+ return ok(canonical);
241
+ };
242
+ const addGrantsToPolicy = (initial, grants) => {
243
+ let policy = initial;
244
+ for (const grant of grants) {
245
+ const added = grantPermission(policy, grant);
203
246
  if (!added.ok)
204
247
  return err(new PermissionPathError(grant.grant_id, "io", { cause: added.error }));
205
248
  policy = added.value;
206
249
  }
207
- return ok(new PermissionAuthority(policy));
250
+ return ok(policy);
208
251
  };
209
252
  const canonicalizeGrant = async (grant) => {
210
253
  const scope = await canonicalizeScope(grant, "read");
@@ -1,8 +1,8 @@
1
1
  import { tmpdir } from "node:os";
2
- import { spawn } from "node-pty";
3
2
  /** Probe the actual native PTY seam instead of inferring support from the OS name. */
4
3
  export const probeProcessCaptureCapability = async () => {
5
4
  try {
5
+ const { spawn } = await import("@lydell/node-pty");
6
6
  const terminal = spawn(process.platform === "win32" ? "cmd.exe" : "/bin/sh", process.platform === "win32" ? ["/c", "exit", "0"] : ["-c", "exit 0"], {
7
7
  cwd: tmpdir(),
8
8
  env: { HOME: tmpdir(), TERM: "xterm-256color" },
@@ -0,0 +1,13 @@
1
+ /** Build the exact permission request shared by process-capture adapters. */
2
+ export const processCapturePermissionRequest = (scenario) => ({
3
+ capability: "process_capture",
4
+ roots: [scenario.working_directory, ...scenario.filesystem_roots],
5
+ executables: [scenario.executable],
6
+ environment_names: [
7
+ ...Object.keys(scenario.environment),
8
+ ...scenario.inherit_environment,
9
+ ],
10
+ network: scenario.network_access === "host" ? "external" : "none",
11
+ mount: false,
12
+ operation_identity: `capture_process_scenario:${scenario.executable}`,
13
+ });
@@ -5,6 +5,7 @@ import { createEvidence, parseEvidence } from "../domain/evidence.js";
5
5
  import { jsonValueSchema } from "../domain/jsonValue.js";
6
6
  import { compareProcessCaptures, LEGACY_PROCESS_CAPTURE_MESSAGE, parseProcessScenario, parseProcessCapture, } from "../domain/processCapture.js";
7
7
  import { captureProcessScenario } from "./ProcessHarness.js";
8
+ import { processCapturePermissionRequest } from "./ProcessCapturePermission.js";
8
9
  import { PROCESS_PROVIDER, createProcessCaptureEvidence, } from "./ProcessEvidence.js";
9
10
  import { loadConfiguredPermissionAuthority } from "./PermissionConfiguration.js";
10
11
  const MAX_INPUT_BYTES = 64 * 1024 * 1024;
@@ -25,18 +26,7 @@ export const captureProcessScenarioFile = async (path) => {
25
26
  const authority = await loadConfiguredPermissionAuthority(config.value);
26
27
  if (!authority.ok)
27
28
  return cliAnalysisError(authority.error);
28
- const authorized = await authority.value.authorize({
29
- capability: "process_capture",
30
- roots: [scenario.working_directory, ...scenario.filesystem_roots],
31
- executables: [scenario.executable],
32
- environment_names: [
33
- ...Object.keys(scenario.environment),
34
- ...scenario.inherit_environment,
35
- ],
36
- network: scenario.network_access === "host" ? "external" : "none",
37
- mount: false,
38
- operation_identity: `capture_process_scenario:${scenario.executable}`,
39
- }, "read");
29
+ const authorized = await authority.value.authorize(processCapturePermissionRequest(scenario), "read");
40
30
  if (!authorized.ok)
41
31
  return cliAnalysisError(authorized.error instanceof PermissionRequiredError
42
32
  ? authorized.error
@@ -1,4 +1,3 @@
1
- import { spawn } from "node-pty";
2
1
  import { validateProcessCapture } from "../domain/processCapture.js";
3
2
  import { err, ok } from "../domain/result.js";
4
3
  import { ProcessCaptureError, processCaptureCancelled, } from "./ProcessCaptureError.js";
@@ -111,6 +110,7 @@ const startCaptureRuntime = async (options) => {
111
110
  let shimReplay;
112
111
  let terminal;
113
112
  try {
113
+ const { spawn } = await import("@lydell/node-pty");
114
114
  replay = await startLoopbackReplay(scenario);
115
115
  const startedAt = new Date();
116
116
  const started = Date.now();
@@ -0,0 +1,72 @@
1
+ import { processCaptureCancelled, } from "./ProcessCaptureError.js";
2
+ import { captureProcessScenario } from "./ProcessHarness.js";
3
+ import { compareProcessCaptures } from "../domain/processComparison.js";
4
+ import { PROCESS_COMPARISON_DIMENSIONS } from "../domain/processComparison.js";
5
+ import { analyzeProcessRepeatability, bindProcessScenario, } from "../domain/processPairedExperiment.js";
6
+ import { err, ok } from "../domain/result.js";
7
+ const productionCapturePort = {
8
+ capture: captureProcessScenario,
9
+ };
10
+ const captureRepeats = async (input) => {
11
+ const captures = [];
12
+ for (let index = 0; index < input.repeatCount; index += 1) {
13
+ if (input.signal?.aborted === true)
14
+ return err(processCaptureCancelled());
15
+ const capture = await input.port.capture(input.scenario, input.policy, input.signal);
16
+ if (!capture.ok)
17
+ return err(capture.error);
18
+ captures.push(capture.value);
19
+ }
20
+ return ok(captures);
21
+ };
22
+ /** Execute repeatability-first authority/candidate captures under one contract. */
23
+ export const runPairedProcessExperiment = async (experiment, policy, options = {}) => {
24
+ const port = options.capturePort ?? productionCapturePort;
25
+ const authorityScenario = bindProcessScenario(experiment.shared_scenario, experiment.authority);
26
+ const candidateScenario = bindProcessScenario(experiment.shared_scenario, experiment.candidate);
27
+ const authorityRuns = await captureRepeats({
28
+ scenario: authorityScenario,
29
+ repeatCount: experiment.repeat_count,
30
+ policy,
31
+ port,
32
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
33
+ });
34
+ if (!authorityRuns.ok)
35
+ return authorityRuns;
36
+ const candidateRuns = await captureRepeats({
37
+ scenario: candidateScenario,
38
+ repeatCount: experiment.repeat_count,
39
+ policy,
40
+ port,
41
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
42
+ });
43
+ if (!candidateRuns.ok)
44
+ return candidateRuns;
45
+ const authorityRepeatability = analyzeProcessRepeatability(authorityRuns.value, experiment.required_dimensions);
46
+ const candidateRepeatability = analyzeProcessRepeatability(candidateRuns.value, experiment.required_dimensions);
47
+ const stable = authorityRepeatability.stable && candidateRepeatability.stable;
48
+ const allDimensionsRequired = PROCESS_COMPARISON_DIMENSIONS.every((dimension) => experiment.required_dimensions.includes(dimension));
49
+ const authorityLatest = authorityRuns.value.at(-1);
50
+ const candidateLatest = candidateRuns.value.at(-1);
51
+ const crossSide = stable &&
52
+ allDimensionsRequired &&
53
+ authorityLatest !== undefined &&
54
+ candidateLatest !== undefined
55
+ ? compareProcessCaptures(authorityLatest, candidateLatest, {
56
+ maxCaptureAgeMs: experiment.freshness_policy.max_capture_age_ms,
57
+ ...(options.now === undefined ? {} : { now: options.now }),
58
+ })
59
+ : null;
60
+ return ok({
61
+ authority_runs: authorityRuns.value,
62
+ candidate_runs: candidateRuns.value,
63
+ authority_repeatability: authorityRepeatability,
64
+ candidate_repeatability: candidateRepeatability,
65
+ cross_side: crossSide,
66
+ cross_side_blocked_reason: crossSide !== null
67
+ ? null
68
+ : !allDimensionsRequired
69
+ ? "Cross-side comparison is blocked until every returned dimension is required and repeatable."
70
+ : "Cross-side comparison is blocked because a required same-side dimension is unstable.",
71
+ });
72
+ };