rea-agents 1.6.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.
Files changed (238) hide show
  1. package/README.md +157 -66
  2. package/bridge/ghidra/ReaGhidraBridge.java +2075 -0
  3. package/bridge/hopper_bridge.py +13 -3
  4. package/dist/application/AbortablePromise.js +25 -0
  5. package/dist/application/AnalysisProvider.js +3 -0
  6. package/dist/application/AnalysisProviderEvaluation.js +60 -0
  7. package/dist/application/AnalysisProviderRegistry.js +228 -0
  8. package/dist/application/AnalysisSnapshotCache.js +41 -21
  9. package/dist/application/AnalysisSnapshotFiles.js +5 -6
  10. package/dist/application/BinarySession.js +218 -101
  11. package/dist/application/CapabilityInventory.js +18 -0
  12. package/dist/application/CompositeProvider.js +20 -8
  13. package/dist/application/CrossVersionInvestigation.js +10 -6
  14. package/dist/application/DirectAnalysis.js +49 -35
  15. package/dist/application/Doctor.js +110 -8
  16. package/dist/application/ElectronBoundaryAnalysis.js +96 -0
  17. package/dist/application/ElectronBoundaryGraph.js +9 -0
  18. package/dist/application/ElectronBoundaryGraphContext.js +62 -0
  19. package/dist/application/ElectronBoundaryGraphIpc.js +242 -0
  20. package/dist/application/ElectronBoundaryGraphNative.js +86 -0
  21. package/dist/application/ElectronBoundaryGraphWindows.js +199 -0
  22. package/dist/application/EnhancedTools.js +39 -14
  23. package/dist/application/InvestigationProviders.js +41 -0
  24. package/dist/application/JavaScriptApplicationEvidence.js +26 -0
  25. package/dist/application/JavaScriptApplicationEvidenceGraph.js +89 -0
  26. package/dist/application/JavaScriptApplicationService.js +74 -0
  27. package/dist/application/JavaScriptApplicationWorkflowEvidence.js +29 -0
  28. package/dist/application/JavaScriptApplicationWorkflowService.js +90 -0
  29. package/dist/application/JavaScriptArtifactAnalysis.js +305 -0
  30. package/dist/application/JavaScriptArtifactAnalysisTypes.js +1 -0
  31. package/dist/application/JavaScriptArtifactFiles.js +231 -0
  32. package/dist/application/JavaScriptArtifactGraphAccumulator.js +54 -0
  33. package/dist/application/JavaScriptArtifactGraphBuilder.js +144 -0
  34. package/dist/application/JavaScriptArtifactGraphContext.js +205 -0
  35. package/dist/application/JavaScriptArtifactGraphDocuments.js +108 -0
  36. package/dist/application/JavaScriptArtifactGraphEvidence.js +99 -0
  37. package/dist/application/JavaScriptArtifactGraphFindings.js +271 -0
  38. package/dist/application/JavaScriptArtifactGraphStructure.js +409 -0
  39. package/dist/application/JavaScriptArtifactReconstruction.js +77 -0
  40. package/dist/application/JavaScriptArtifactReconstructionInput.js +18 -0
  41. package/dist/application/JavaScriptReplayPlanning.js +201 -0
  42. package/dist/application/JavaScriptReplayService.js +266 -0
  43. package/dist/application/JavaScriptRuntimeReconciliationEvidence.js +25 -0
  44. package/dist/application/JavaScriptRuntimeReconciliationService.js +27 -0
  45. package/dist/application/MacHopper.js +20 -4
  46. package/dist/application/ManagedApplicationGraphService.js +55 -0
  47. package/dist/application/ManagedMemberComparisonService.js +122 -0
  48. package/dist/application/ManagedNativeVerificationService.js +39 -0
  49. package/dist/application/ManagedReconstructionService.js +58 -0
  50. package/dist/application/ManagedRuntimeCorrelationService.js +103 -0
  51. package/dist/application/PermissionAuthority.js +1 -1
  52. package/dist/application/ProcessCaptureLifecycle.js +1 -1
  53. package/dist/application/ProcessHarness.js +1 -1
  54. package/dist/application/ProcessSampling.js +16 -12
  55. package/dist/application/ProjectPermissionStore.js +1 -0
  56. package/dist/application/SessionProviderRouter.js +219 -0
  57. package/dist/application/Setup.js +84 -60
  58. package/dist/application/SetupClients.js +1 -1
  59. package/dist/application/SetupPlan.js +9 -2
  60. package/dist/application/SupportedClients.js +40 -16
  61. package/dist/application/Upgrade.js +1 -1
  62. package/dist/application/runtime.js +11 -6
  63. package/dist/artifacts/AsarArtifactReader.js +23 -3
  64. package/dist/browser/CdpBrowserProvider.js +4 -2
  65. package/dist/browser/CdpCaptureEvents.js +49 -0
  66. package/dist/browser/CdpElectronInspection.js +106 -162
  67. package/dist/browser/CdpElectronProvider.js +9 -3
  68. package/dist/browser/CdpElectronScriptEvents.js +69 -0
  69. package/dist/browser/CdpElectronScripts.js +94 -0
  70. package/dist/browser/CdpElectronWorkers.js +56 -0
  71. package/dist/browser/CdpEndpoint.js +14 -1
  72. package/dist/browser/CdpPageCapture.js +8 -4
  73. package/dist/catalogIdentity.js +6 -42
  74. package/dist/cli.js +446 -34
  75. package/dist/cliApplicationCommands.js +62 -0
  76. package/dist/cliBrowserAdvancedCommands.js +5 -4
  77. package/dist/cliBrowserCommands.js +5 -4
  78. package/dist/cliCommandNames.js +56 -0
  79. package/dist/cliElectronCommands.js +106 -3
  80. package/dist/cliEvidenceCommands.js +4 -3
  81. package/dist/cliInvestigationCommands.js +2 -1
  82. package/dist/cliJsonInput.js +54 -0
  83. package/dist/cliPolicyCommands.js +2 -1
  84. package/dist/cliProcessCommands.js +3 -2
  85. package/dist/config.js +101 -2
  86. package/dist/contracts/applicationToolContracts.js +91 -0
  87. package/dist/contracts/artifactToolContracts.js +5 -9
  88. package/dist/contracts/browserToolContracts.js +14 -62
  89. package/dist/contracts/electronToolContracts.js +45 -19
  90. package/dist/contracts/enhancedInputs.js +2 -2
  91. package/dist/contracts/errorSchemas.js +1 -0
  92. package/dist/contracts/investigationExamples.js +0 -6
  93. package/dist/contracts/javascriptApplicationWorkflowExamples.js +46 -0
  94. package/dist/contracts/javascriptRuntimeReconciliationExample.js +208 -0
  95. package/dist/contracts/managedToolContracts.js +153 -0
  96. package/dist/contracts/managedWorkflowExamples.js +346 -0
  97. package/dist/contracts/managedWorkflowToolContracts.js +203 -0
  98. package/dist/contracts/nativeToolContracts.js +2 -6
  99. package/dist/contracts/promptContracts.js +18 -2
  100. package/dist/contracts/providerSelection.js +28 -0
  101. package/dist/contracts/sessionLifecycleInputs.js +2 -0
  102. package/dist/contracts/toolContractExamples.js +10 -25
  103. package/dist/contracts/toolContracts.js +69 -65
  104. package/dist/contracts/toolEffects.js +160 -0
  105. package/dist/contracts/toolOutputSchemas.js +152 -42
  106. package/dist/doctorRuntime.js +47 -0
  107. package/dist/domain/analysisProfile.js +78 -0
  108. package/dist/domain/analysisSnapshot.js +92 -46
  109. package/dist/domain/artifactComparison.js +3 -4
  110. package/dist/domain/artifactGraph.js +1 -0
  111. package/dist/domain/binaryTarget.js +4 -29
  112. package/dist/domain/browserObservation.js +3 -0
  113. package/dist/domain/bundleComparison.js +4 -4
  114. package/dist/domain/electronObservation.js +13 -0
  115. package/dist/domain/electronStaticAnalysis.js +9 -0
  116. package/dist/domain/electronStaticAnalysisBrowser.js +194 -0
  117. package/dist/domain/electronStaticAnalysisIpc.js +171 -0
  118. package/dist/domain/electronStaticAnalysisNative.js +164 -0
  119. package/dist/domain/electronStaticAnalysisTypes.js +1 -0
  120. package/dist/domain/electronStaticAnalysisValues.js +103 -0
  121. package/dist/domain/errors.js +148 -7
  122. package/dist/domain/evidence.js +44 -5
  123. package/dist/domain/evidenceBundle.js +9 -7
  124. package/dist/domain/functionComparison.js +9 -3
  125. package/dist/domain/functionComparisonNormalization.js +37 -0
  126. package/dist/domain/functionComparisonSchemas.js +3 -8
  127. package/dist/domain/functionDossierEvidence.js +4 -1
  128. package/dist/domain/hopperValues.js +66 -9
  129. package/dist/domain/inputIssueProjection.js +52 -0
  130. package/dist/domain/investigationWorkspace.js +2 -1
  131. package/dist/domain/javascriptApplicationAnalysis.js +155 -0
  132. package/dist/domain/javascriptApplicationChangeGraph.js +231 -0
  133. package/dist/domain/javascriptApplicationEvidenceSchemas.js +379 -0
  134. package/dist/domain/javascriptApplicationGraph.js +282 -0
  135. package/dist/domain/javascriptApplicationGraphSchemas.js +139 -0
  136. package/dist/domain/javascriptApplicationVersionComparison.js +116 -0
  137. package/dist/domain/javascriptApplicationVersionComparisonSchemas.js +139 -0
  138. package/dist/domain/javascriptApplicationVersionItems.js +221 -0
  139. package/dist/domain/javascriptApplicationVersionKeys.js +170 -0
  140. package/dist/domain/javascriptAstFingerprint.js +143 -0
  141. package/dist/domain/javascriptFeatureSeed.js +140 -0
  142. package/dist/domain/javascriptFeatureTrace.js +232 -0
  143. package/dist/domain/javascriptFeatureTraceSchemas.js +136 -0
  144. package/dist/domain/javascriptFeatureTraversal.js +88 -0
  145. package/dist/domain/javascriptNativeHandoff.js +85 -0
  146. package/dist/domain/javascriptReplay.js +361 -0
  147. package/dist/domain/javascriptRuntimeLoadState.js +106 -0
  148. package/dist/domain/javascriptRuntimeReconciliation.js +42 -0
  149. package/dist/domain/javascriptRuntimeReconciliationGraph.js +169 -0
  150. package/dist/domain/javascriptRuntimeReconciliationMatching.js +247 -0
  151. package/dist/domain/javascriptRuntimeReconciliationParsing.js +160 -0
  152. package/dist/domain/javascriptRuntimeReconciliationResult.js +119 -0
  153. package/dist/domain/javascriptRuntimeReconciliationRuntime.js +316 -0
  154. package/dist/domain/javascriptRuntimeReconciliationSchemas.js +245 -0
  155. package/dist/domain/javascriptRuntimeStaticCandidates.js +191 -0
  156. package/dist/domain/javascriptStaticAnalysis.js +346 -0
  157. package/dist/domain/javascriptStaticAnalysisFindings.js +39 -0
  158. package/dist/domain/javascriptStaticAnalysisHelpers.js +274 -0
  159. package/dist/domain/javascriptStaticAnalysisState.js +23 -0
  160. package/dist/domain/javascriptStaticAnalysisTypes.js +1 -0
  161. package/dist/domain/managedApplicationGraph.js +556 -0
  162. package/dist/domain/managedArtifact.js +444 -0
  163. package/dist/domain/managedMemberComparison.js +565 -0
  164. package/dist/domain/managedNativeVerification.js +408 -0
  165. package/dist/domain/managedReconstruction.js +183 -0
  166. package/dist/domain/managedRuntimeCorrelation.js +175 -0
  167. package/dist/domain/reconstructionVerificationSchemas.js +1 -3
  168. package/dist/dotnet/ManagedArtifactInspector.js +318 -0
  169. package/dist/dotnet/ManagedMemberInspector.js +887 -0
  170. package/dist/dotnet/ManagedMetadataHeaps.js +131 -0
  171. package/dist/dotnet/ManagedMetadataInventory.js +368 -0
  172. package/dist/dotnet/ManagedMetadataLayout.js +322 -0
  173. package/dist/dotnet/ManagedNativeBoundaryInspector.js +391 -0
  174. package/dist/dotnet/ManagedPeReader.js +157 -0
  175. package/dist/dotnet/ManagedReaderFailure.js +10 -0
  176. package/dist/dotnet/ManagedStaticProvider.js +203 -0
  177. package/dist/generatedPackageMetadata.js +2 -2
  178. package/dist/ghidra/GhidraAnalysisProfile.js +37 -0
  179. package/dist/ghidra/GhidraClient.js +400 -0
  180. package/dist/ghidra/GhidraClientTypes.js +1 -0
  181. package/dist/ghidra/GhidraDefaults.js +20 -0
  182. package/dist/ghidra/GhidraDiagnostics.js +43 -0
  183. package/dist/ghidra/GhidraDoctor.js +59 -0
  184. package/dist/ghidra/GhidraFunctionValues.js +264 -0
  185. package/dist/ghidra/GhidraInstallation.js +239 -0
  186. package/dist/ghidra/GhidraInventoryValues.js +217 -0
  187. package/dist/ghidra/GhidraLauncher.js +139 -0
  188. package/dist/ghidra/GhidraProvider.js +366 -0
  189. package/dist/ghidra/GhidraRequestQueue.js +98 -0
  190. package/dist/ghidra/GhidraResponseBuffer.js +31 -0
  191. package/dist/ghidra/GhidraResponseRouter.js +24 -0
  192. package/dist/ghidra/GhidraSessionError.js +39 -0
  193. package/dist/ghidra/GhidraSessionValues.js +62 -0
  194. package/dist/ghidra/GhidraSocketConnection.js +46 -0
  195. package/dist/ghidra/protocol.js +69 -0
  196. package/dist/hopper/BridgeLauncher.js +19 -54
  197. package/dist/hopper/HopperAnalysisProfile.js +106 -0
  198. package/dist/hopper/HopperClient.js +154 -162
  199. package/dist/hopper/HopperProvider.js +99 -7
  200. package/dist/hopper/HopperSessionValues.js +27 -0
  201. package/dist/hopper/HopperSocketConnection.js +31 -0
  202. package/dist/main.js +14 -0
  203. package/dist/native/NativeMacOSProvider.js +3 -1
  204. package/dist/process/PendingOperations.js +59 -0
  205. package/dist/process/PrivateRuntimeRoot.js +33 -0
  206. package/dist/process/ProviderDeadline.js +95 -0
  207. package/dist/process/ProviderProcess.js +275 -0
  208. package/dist/replay/JavaScriptReplayWorker.js +286 -0
  209. package/dist/replay/LinuxJavaScriptReplayRunner.js +434 -0
  210. package/dist/replay/LinuxRuntimeClosure.js +46 -0
  211. package/dist/replay/LinuxSeccompPolicy.js +40 -0
  212. package/dist/replay/ReplayWorkerProtocol.js +55 -0
  213. package/dist/replay/SystemJavaScriptReplayHost.js +182 -0
  214. package/dist/server/createServer.js +53 -0
  215. package/dist/server/promptCompletion.js +9 -3
  216. package/dist/server/registerApplicationTools.js +115 -0
  217. package/dist/server/registerArtifactComparisonTool.js +16 -9
  218. package/dist/server/registerBrowserTools.js +33 -8
  219. package/dist/server/registerBundleComparisonTool.js +17 -20
  220. package/dist/server/registerElectronTools.js +45 -3
  221. package/dist/server/registerEnhancedTools.js +67 -19
  222. package/dist/server/registerEvidenceTools.js +5 -1
  223. package/dist/server/registerFunctionComparisonTool.js +14 -7
  224. package/dist/server/registerInvestigationTools.js +21 -17
  225. package/dist/server/registerManagedTools.js +6 -0
  226. package/dist/server/registerManagedWorkflowTools.js +277 -0
  227. package/dist/server/registerOfficialTools.js +11 -8
  228. package/dist/server/registerProcessComparisonTool.js +25 -47
  229. package/dist/server/registerSessionStatusTool.js +7 -7
  230. package/dist/server/registerSessionTools.js +57 -87
  231. package/dist/server/sessionEvidence.js +14 -19
  232. package/dist/server/toolInputValidation.js +77 -0
  233. package/dist/server/toolRegistrationOptions.js +79 -2
  234. package/dist/server/toolResult.js +25 -17
  235. package/package.json +17 -7
  236. package/scripts/rea.mjs +36 -2
  237. package/skills/rea-analysis/SKILL.md +59 -3
  238. /package/dist/{application → process}/ProcessOwnership.js +0 -0
@@ -0,0 +1,31 @@
1
+ import { createConnection } from "node:net";
2
+ import { HopperCancelledError, HopperStartError } from "../domain/errors.js";
3
+ import { err, ok } from "../domain/result.js";
4
+ /** Open one abort-aware Hopper Unix-socket connection attempt. */
5
+ export const connectHopperSocketOnce = (socketPath, signal) => new Promise((resolve) => {
6
+ const socket = createConnection(socketPath);
7
+ const detach = () => {
8
+ socket.off("connect", onConnect);
9
+ socket.off("error", onError);
10
+ signal.removeEventListener("abort", onAbort);
11
+ };
12
+ const onConnect = () => {
13
+ detach();
14
+ resolve(ok(socket));
15
+ };
16
+ const onError = (cause) => {
17
+ detach();
18
+ socket.destroy();
19
+ resolve(err(new HopperStartError({ cause })));
20
+ };
21
+ const onAbort = () => {
22
+ detach();
23
+ socket.destroy();
24
+ resolve(err(new HopperCancelledError()));
25
+ };
26
+ socket.once("connect", onConnect);
27
+ socket.once("error", onError);
28
+ signal.addEventListener("abort", onAbort, { once: true });
29
+ if (signal.aborted)
30
+ onAbort();
31
+ });
package/dist/main.js CHANGED
@@ -89,6 +89,14 @@ export const run = async (dependencies = runtimeDependencies()) => {
89
89
  roots: [...config.value.analysisSnapshotFilePolicy.roots],
90
90
  };
91
91
  const runtimeInvestigationRoots = [...config.value.investigationInputRoots];
92
+ const runtimeJavascriptReplayPolicy = {
93
+ ...config.value.javascriptReplayPolicy,
94
+ roots: [...config.value.javascriptReplayPolicy.roots],
95
+ };
96
+ const runtimeManagedRuntimePolicy = {
97
+ ...config.value.managedRuntimePolicy,
98
+ roots: [...config.value.managedRuntimePolicy.roots],
99
+ };
92
100
  const liveServers = new Set();
93
101
  let handle;
94
102
  try {
@@ -103,6 +111,8 @@ export const run = async (dependencies = runtimeDependencies()) => {
103
111
  browserObservation,
104
112
  electronObservation,
105
113
  artifactIntegrityContinueEnabled: () => currentConfig.artifactIntegrityContinueEnabled,
114
+ javascriptReplayPolicy: runtimeJavascriptReplayPolicy,
115
+ managedRuntimePolicy: runtimeManagedRuntimePolicy,
106
116
  availabilityPolicy: () => ({
107
117
  processCaptureEnabled: currentConfig.processExecutionPolicy.enabled,
108
118
  evidenceFileRoots: currentConfig.evidenceFilePolicy.roots.length,
@@ -112,6 +122,8 @@ export const run = async (dependencies = runtimeDependencies()) => {
112
122
  electronObservationEnabled: currentConfig.electronObservationEnabled &&
113
123
  currentConfig.electronCdpEndpoints.length > 0 &&
114
124
  currentConfig.electronFileRoots.length > 0,
125
+ javascriptReplayEnabled: currentConfig.javascriptReplayPolicy.enabled,
126
+ managedRuntimeEnabled: currentConfig.managedRuntimePolicy.enabled,
115
127
  }),
116
128
  });
117
129
  liveServers.add(server);
@@ -162,6 +174,8 @@ export const run = async (dependencies = runtimeDependencies()) => {
162
174
  Object.assign(runtimeEvidencePolicy, refreshed.value.evidenceFilePolicy);
163
175
  Object.assign(runtimeSnapshotPolicy, refreshed.value.analysisSnapshotFilePolicy);
164
176
  runtimeInvestigationRoots.splice(0, runtimeInvestigationRoots.length, ...refreshed.value.investigationInputRoots);
177
+ Object.assign(runtimeJavascriptReplayPolicy, refreshed.value.javascriptReplayPolicy);
178
+ Object.assign(runtimeManagedRuntimePolicy, refreshed.value.managedRuntimePolicy);
165
179
  for (const server of liveServers)
166
180
  server.sendToolListChanged();
167
181
  });
@@ -14,11 +14,13 @@ import { parseDemangledSymbols } from "./parsers/demangle.js";
14
14
  import { parseLipoArchitectures } from "./parsers/lipo.js";
15
15
  import { parsePlistJson } from "./parsers/plist.js";
16
16
  import { architectureLocations, inspectNativeMacho, } from "./NativeMachoInspection.js";
17
- const IDENTITY = Object.freeze({
17
+ /** Public identity committed by macOS-native inspection observations. */
18
+ export const NATIVE_MACOS_PROVIDER_IDENTITY = Object.freeze({
18
19
  id: "native-macos",
19
20
  name: "macOS native inspection utilities",
20
21
  version: null,
21
22
  });
23
+ const IDENTITY = NATIVE_MACOS_PROVIDER_IDENTITY;
22
24
  const TIMEOUT_MS = 30_000;
23
25
  const OUTPUT_LIMIT = 4 * 1024 * 1024;
24
26
  /** Read-only semantic provider composed from Xcode command-line utilities. */
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Correlates event-driven provider replies with bounded waits.
3
+ *
4
+ * The registry owns every request timer and abort listener. Settling, timing
5
+ * out, cancelling, or failing an operation removes both before resolution, so
6
+ * late protocol replies can be ignored without retaining request resources.
7
+ */
8
+ export class PendingOperations {
9
+ #pending = new Map();
10
+ /** Number of currently correlated operations. */
11
+ get size() {
12
+ return this.#pending.size;
13
+ }
14
+ /** Return whether an operation key is awaiting a protocol reply. */
15
+ has(key) {
16
+ return this.#pending.has(key);
17
+ }
18
+ /** Register one unique operation key and start its bounded wait. */
19
+ wait(key, options) {
20
+ if (this.#pending.has(key))
21
+ throw new Error("Pending operation key is already registered");
22
+ if (options.signal?.aborted === true)
23
+ return Promise.resolve(options.cancelledValue());
24
+ return new Promise((resolve) => {
25
+ const onAbort = options.signal === undefined
26
+ ? undefined
27
+ : () => this.#finish(key, options.cancelledValue());
28
+ const timer = setTimeout(() => this.#finish(key, options.timeoutValue()), options.timeoutMs);
29
+ this.#pending.set(key, {
30
+ resolve,
31
+ timer,
32
+ signal: options.signal,
33
+ onAbort,
34
+ });
35
+ if (onAbort !== undefined)
36
+ options.signal?.addEventListener("abort", onAbort, { once: true });
37
+ });
38
+ }
39
+ /** Settle one correlated reply; returns false for late or unknown keys. */
40
+ settle(key, value) {
41
+ return this.#finish(key, value);
42
+ }
43
+ /** Fail every active operation while releasing all timers and listeners. */
44
+ failAll(valueFor) {
45
+ for (const key of this.#pending.keys())
46
+ this.#finish(key, valueFor(key));
47
+ }
48
+ #finish(key, value) {
49
+ const pending = this.#pending.get(key);
50
+ if (pending === undefined)
51
+ return false;
52
+ this.#pending.delete(key);
53
+ clearTimeout(pending.timer);
54
+ if (pending.signal !== undefined && pending.onAbort !== undefined)
55
+ pending.signal.removeEventListener("abort", pending.onAbort);
56
+ pending.resolve(value);
57
+ return true;
58
+ }
59
+ }
@@ -0,0 +1,33 @@
1
+ import { chmod, mkdtemp, rm } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ /**
5
+ * Owns one mode-0700 temporary runtime root and removes it idempotently.
6
+ *
7
+ * Protocol adapters decide what belongs inside the directory; this primitive
8
+ * only establishes a private filesystem boundary and deterministic cleanup.
9
+ */
10
+ export class PrivateRuntimeRoot {
11
+ path;
12
+ #closePromise;
13
+ constructor(path) {
14
+ this.path = path;
15
+ }
16
+ /** Allocate a new private runtime root without performing protocol work. */
17
+ static async create(options = {}) {
18
+ const path = await mkdtemp(join(options.parent ?? tmpdir(), options.prefix ?? "rea-provider-"));
19
+ try {
20
+ await chmod(path, 0o700);
21
+ return new PrivateRuntimeRoot(path);
22
+ }
23
+ catch (cause) {
24
+ await rm(path, { recursive: true, force: true });
25
+ throw cause;
26
+ }
27
+ }
28
+ /** Remove the runtime root; concurrent and repeated callers share cleanup. */
29
+ close() {
30
+ this.#closePromise ??= rm(this.path, { recursive: true, force: true });
31
+ return this.#closePromise;
32
+ }
33
+ }
@@ -0,0 +1,95 @@
1
+ /** Wait for an interval while removing the supplied abort listener on settle. */
2
+ export const waitForAbortableDelay = (milliseconds, signal) => {
3
+ if (signal?.aborted === true)
4
+ return Promise.resolve("aborted");
5
+ return new Promise((resolve) => {
6
+ const settle = (result) => {
7
+ clearTimeout(timer);
8
+ signal?.removeEventListener("abort", onAbort);
9
+ resolve(result);
10
+ };
11
+ const onAbort = () => settle("aborted");
12
+ const timer = setTimeout(() => settle("elapsed"), Math.max(0, milliseconds));
13
+ signal?.addEventListener("abort", onAbort, { once: true });
14
+ });
15
+ };
16
+ /**
17
+ * One absolute deadline shared by every phase of a provider startup handshake.
18
+ *
19
+ * The deadline owns its timer and the listener attached to an optional caller
20
+ * signal. Adapters must dispose it after startup succeeds or fails.
21
+ */
22
+ export class ProviderStartupDeadline {
23
+ timeoutMs;
24
+ #controller = new AbortController();
25
+ #deadlineAt;
26
+ #externalSignal;
27
+ #onExternalAbort;
28
+ #timer;
29
+ #timedOut = false;
30
+ #cancelled = false;
31
+ constructor(timeoutMs, signal) {
32
+ this.timeoutMs = timeoutMs;
33
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
34
+ throw new RangeError("Provider startup timeout must be positive");
35
+ this.#deadlineAt = Date.now() + timeoutMs;
36
+ this.#externalSignal = signal;
37
+ this.#onExternalAbort = () => {
38
+ if (this.#controller.signal.aborted)
39
+ return;
40
+ if (Date.now() >= this.#deadlineAt) {
41
+ this.#timedOut = true;
42
+ this.#abort(new DOMException("Provider startup deadline elapsed", "TimeoutError"));
43
+ return;
44
+ }
45
+ this.#cancelled = true;
46
+ this.#abort(signal?.reason);
47
+ };
48
+ if (signal?.aborted === true) {
49
+ this.#onExternalAbort();
50
+ return;
51
+ }
52
+ signal?.addEventListener("abort", this.#onExternalAbort, { once: true });
53
+ this.#timer = setTimeout(() => {
54
+ if (this.#controller.signal.aborted)
55
+ return;
56
+ this.#timedOut = true;
57
+ this.#abort(new DOMException("Provider startup deadline elapsed", "TimeoutError"));
58
+ }, timeoutMs);
59
+ }
60
+ /** Composite signal aborted by either the caller or the absolute deadline. */
61
+ get signal() {
62
+ return this.#controller.signal;
63
+ }
64
+ /** True only when the external caller signal initiated cancellation. */
65
+ get cancelled() {
66
+ return this.#cancelled;
67
+ }
68
+ /** True when the absolute startup deadline has elapsed. */
69
+ get timedOut() {
70
+ return (this.#timedOut || (!this.#cancelled && Date.now() >= this.#deadlineAt));
71
+ }
72
+ /** Milliseconds still available to the next startup phase. */
73
+ remainingMs() {
74
+ return Math.max(0, this.#deadlineAt - Date.now());
75
+ }
76
+ /** Wait for at most one polling interval under the shared deadline signal. */
77
+ wait(milliseconds) {
78
+ return waitForAbortableDelay(Math.min(milliseconds, this.remainingMs()), this.signal);
79
+ }
80
+ /** Release the deadline timer and external abort listener. */
81
+ dispose() {
82
+ this.#clearTimer();
83
+ this.#externalSignal?.removeEventListener("abort", this.#onExternalAbort);
84
+ }
85
+ #abort(reason) {
86
+ this.#clearTimer();
87
+ this.#externalSignal?.removeEventListener("abort", this.#onExternalAbort);
88
+ this.#controller.abort(reason);
89
+ }
90
+ #clearTimer() {
91
+ if (this.#timer !== undefined)
92
+ clearTimeout(this.#timer);
93
+ this.#timer = undefined;
94
+ }
95
+ }
@@ -0,0 +1,275 @@
1
+ import { spawn } from "node:child_process";
2
+ const DEFAULT_OUTPUT_BYTES = 64 * 1024;
3
+ const DEFAULT_TERMINATION_GRACE_MS = 250;
4
+ const DEFAULT_KILL_GRACE_MS = 1_000;
5
+ /**
6
+ * Spawn a provider in a dedicated POSIX process group with an ownership token.
7
+ *
8
+ * The caller remains responsible for persisting any ownership manifest and for
9
+ * selecting provider-specific command arguments or environment values.
10
+ */
11
+ export const spawnOwnedProviderProcess = async (options) => {
12
+ const child = spawn(options.command, [...options.arguments], {
13
+ shell: false,
14
+ stdio: ["ignore", "pipe", "pipe"],
15
+ detached: process.platform !== "win32",
16
+ env: {
17
+ ...process.env,
18
+ ...options.env,
19
+ REA_PROCESS_RUN_ID: options.runId,
20
+ },
21
+ ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
22
+ });
23
+ await waitForSpawn(child);
24
+ const pid = child.pid;
25
+ if (pid === undefined)
26
+ throw new Error("Provider launcher has no process ID");
27
+ const ownership = {
28
+ runId: options.runId,
29
+ leaderPid: pid,
30
+ processGroupId: pid,
31
+ expectedParentPid: process.pid,
32
+ ...(options.expectedCommand === null
33
+ ? {}
34
+ : { expectedCommand: options.expectedCommand ?? options.command }),
35
+ };
36
+ return { process: child, ownership };
37
+ };
38
+ /**
39
+ * Supervises only generic process resources; wire protocol remains adapter-owned.
40
+ *
41
+ * Output retention is bounded per stream while byte totals remain exact. Stop
42
+ * first honors token-verified group cleanup when supplied, otherwise it uses a
43
+ * bounded TERM-to-KILL escalation for a directly owned child.
44
+ */
45
+ export class ProviderProcessSupervisor {
46
+ launch;
47
+ #options;
48
+ #stdout;
49
+ #stderr;
50
+ #exit = deferred();
51
+ #onStdout = (chunk) => {
52
+ this.#capture("stdout", this.#stdout, chunk);
53
+ };
54
+ #onStderr = (chunk) => {
55
+ this.#capture("stderr", this.#stderr, chunk);
56
+ };
57
+ #onExit = (code, signal) => {
58
+ this.#recordExit(code, signal);
59
+ };
60
+ #onClose = (code, signal) => {
61
+ this.#recordExit(code, signal);
62
+ };
63
+ #onError = (cause) => {
64
+ this.#options.onDiagnostic?.({ type: "error", message: cause.message });
65
+ };
66
+ #exitObservation;
67
+ #stopPromise;
68
+ #disposed = false;
69
+ constructor(launch, options = {}) {
70
+ this.launch = launch;
71
+ this.#options = options;
72
+ const limit = options.maxOutputBytesPerStream ?? DEFAULT_OUTPUT_BYTES;
73
+ if (!Number.isSafeInteger(limit) || limit <= 0)
74
+ throw new RangeError("Provider output limit must be a positive integer");
75
+ this.#stdout = new BoundedByteCapture(limit);
76
+ this.#stderr = new BoundedByteCapture(limit);
77
+ this.#attach(launch.process.stdout, "stdout");
78
+ this.#attach(launch.process.stderr, "stderr");
79
+ launch.process.once("exit", this.#onExit);
80
+ launch.process.once("close", this.#onClose);
81
+ launch.process.on("error", this.#onError);
82
+ if (launch.process.exitCode !== null || launch.process.signalCode !== null)
83
+ this.#recordExit(launch.process.exitCode, launch.process.signalCode);
84
+ }
85
+ /** Latest bounded output and exit observation. */
86
+ snapshot() {
87
+ return {
88
+ stdout: this.#stdout.snapshot(),
89
+ stderr: this.#stderr.snapshot(),
90
+ exitCode: this.#exitObservation?.code,
91
+ signal: this.#exitObservation?.signal,
92
+ };
93
+ }
94
+ /** Wait for process exit up to a caller-owned bounded interval. */
95
+ async waitForExit(timeoutMs) {
96
+ if (this.#exitObservation !== undefined)
97
+ return true;
98
+ let timer;
99
+ try {
100
+ return await Promise.race([
101
+ this.#exit.promise.then(() => true),
102
+ new Promise((resolve) => {
103
+ timer = setTimeout(() => resolve(false), Math.max(0, timeoutMs));
104
+ }),
105
+ ]);
106
+ }
107
+ finally {
108
+ if (timer !== undefined)
109
+ clearTimeout(timer);
110
+ }
111
+ }
112
+ /** Stop an owned process once; concurrent callers share the same escalation. */
113
+ stop(options = {}) {
114
+ this.#stopPromise ??= this.#stop(options);
115
+ return this.#stopPromise;
116
+ }
117
+ /** Detach capture and process listeners after an adapter releases ownership. */
118
+ dispose() {
119
+ if (this.#disposed)
120
+ return;
121
+ this.#disposed = true;
122
+ this.launch.process.stdout?.off("data", this.#onStdout);
123
+ this.launch.process.stderr?.off("data", this.#onStderr);
124
+ this.launch.process.off("exit", this.#onExit);
125
+ this.launch.process.off("close", this.#onClose);
126
+ this.launch.process.off("error", this.#onError);
127
+ }
128
+ async #stop(options) {
129
+ try {
130
+ if (!this.launch.ownsProcessLifetime)
131
+ return { status: "not-owned" };
132
+ if (this.launch.cleanup !== undefined) {
133
+ const cleanup = await this.#ownedCleanup(options.cleanupResult);
134
+ if (!cleanup.cleaned)
135
+ return { status: "incomplete", reason: cleanup.reason };
136
+ const graceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS;
137
+ if (await this.waitForExit(graceMs))
138
+ return { status: "verified-cleanup" };
139
+ const retried = await this.launch.cleanup();
140
+ if (!retried.cleaned)
141
+ return { status: "incomplete", reason: retried.reason };
142
+ if (await this.waitForExit(graceMs))
143
+ return { status: "verified-cleanup" };
144
+ return {
145
+ status: "incomplete",
146
+ reason: "verified process-group cleanup did not stop the launcher",
147
+ };
148
+ }
149
+ if (this.#exitObservation !== undefined)
150
+ return { status: "already-exited" };
151
+ this.#signal("SIGTERM");
152
+ if (await this.waitForExit(options.terminationGraceMs ?? DEFAULT_TERMINATION_GRACE_MS))
153
+ return { status: "terminated" };
154
+ this.#signal("SIGKILL");
155
+ if (await this.waitForExit(options.killGraceMs ?? DEFAULT_KILL_GRACE_MS))
156
+ return { status: "killed" };
157
+ return {
158
+ status: "incomplete",
159
+ reason: "owned provider process did not exit after SIGKILL",
160
+ };
161
+ }
162
+ catch (cause) {
163
+ return {
164
+ status: "incomplete",
165
+ reason: cause instanceof Error
166
+ ? cause.message
167
+ : "owned provider process cleanup failed",
168
+ };
169
+ }
170
+ finally {
171
+ this.dispose();
172
+ }
173
+ }
174
+ #ownedCleanup(supplied) {
175
+ if (supplied !== undefined)
176
+ return Promise.resolve(supplied);
177
+ const cleanup = this.launch.cleanup;
178
+ return cleanup === undefined
179
+ ? Promise.resolve({
180
+ cleaned: false,
181
+ reason: "owned provider process has no cleanup capability",
182
+ })
183
+ : cleanup();
184
+ }
185
+ #signal(signal) {
186
+ if (this.#exitObservation !== undefined)
187
+ return;
188
+ this.launch.process.kill(signal);
189
+ }
190
+ #attach(stream, name) {
191
+ if (name === "stdout")
192
+ stream?.on("data", this.#onStdout);
193
+ else
194
+ stream?.on("data", this.#onStderr);
195
+ }
196
+ #capture(stream, capture, chunk) {
197
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
198
+ capture.append(bytes);
199
+ this.#options.onDiagnostic?.({
200
+ type: "output",
201
+ stream,
202
+ bytes: bytes.byteLength,
203
+ totalBytes: capture.bytes,
204
+ truncated: capture.truncated,
205
+ });
206
+ }
207
+ #recordExit(code, signal) {
208
+ if (this.#exitObservation !== undefined)
209
+ return;
210
+ this.#exitObservation = { code, signal };
211
+ this.#exit.resolve(this.#exitObservation);
212
+ this.#options.onDiagnostic?.({
213
+ type: "exit",
214
+ code,
215
+ signal,
216
+ snapshot: this.snapshot(),
217
+ });
218
+ }
219
+ }
220
+ class BoundedByteCapture {
221
+ limit;
222
+ #chunks = [];
223
+ #bytes = 0;
224
+ #retainedBytes = 0;
225
+ constructor(limit) {
226
+ this.limit = limit;
227
+ }
228
+ get bytes() {
229
+ return this.#bytes;
230
+ }
231
+ get truncated() {
232
+ return this.#bytes > this.#retainedBytes;
233
+ }
234
+ append(chunk) {
235
+ this.#bytes += chunk.byteLength;
236
+ const remaining = this.limit - this.#retainedBytes;
237
+ if (remaining <= 0)
238
+ return;
239
+ const retained = Buffer.from(chunk.subarray(0, remaining));
240
+ this.#chunks.push(retained);
241
+ this.#retainedBytes += retained.byteLength;
242
+ }
243
+ snapshot() {
244
+ return {
245
+ text: Buffer.concat(this.#chunks, this.#retainedBytes).toString("utf8"),
246
+ bytes: this.#bytes,
247
+ retainedBytes: this.#retainedBytes,
248
+ truncated: this.truncated,
249
+ };
250
+ }
251
+ }
252
+ const deferred = () => {
253
+ let resolver;
254
+ const promise = new Promise((resolve) => {
255
+ resolver = resolve;
256
+ });
257
+ return {
258
+ promise,
259
+ resolve: (value) => {
260
+ resolver?.(value);
261
+ },
262
+ };
263
+ };
264
+ const waitForSpawn = (child) => new Promise((resolve, reject) => {
265
+ const onSpawn = () => {
266
+ child.off("error", onError);
267
+ resolve();
268
+ };
269
+ const onError = (cause) => {
270
+ child.off("spawn", onSpawn);
271
+ reject(cause);
272
+ };
273
+ child.once("spawn", onSpawn);
274
+ child.once("error", onError);
275
+ });