rea-agents 0.3.0 → 0.5.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 (125) hide show
  1. package/README.md +139 -21
  2. package/bridge/hopper_bridge.py +156 -14
  3. package/dist/application/AnalysisProvider.js +12 -1
  4. package/dist/application/ArtifactExtraction.js +166 -0
  5. package/dist/application/ArtifactGraphConstruction.js +257 -0
  6. package/dist/application/ArtifactInventory.js +253 -0
  7. package/dist/application/BinarySession.js +174 -14
  8. package/dist/application/CompositeProvider.js +73 -0
  9. package/dist/application/DirectAnalysis.js +44 -3
  10. package/dist/application/Doctor.js +35 -6
  11. package/dist/application/EnhancedTools.js +10 -7
  12. package/dist/application/EvidenceBundleCommands.js +51 -0
  13. package/dist/application/EvidenceBundleFiles.js +127 -0
  14. package/dist/application/EvidenceLedger.js +225 -18
  15. package/dist/application/FilesystemSnapshot.js +124 -0
  16. package/dist/application/LinuxHopper.js +186 -0
  17. package/dist/application/LoopbackReplay.js +195 -37
  18. package/dist/application/ProcessHarness.js +200 -191
  19. package/dist/application/ProcessNormalization.js +44 -0
  20. package/dist/application/ProcessOwnership.js +105 -0
  21. package/dist/application/ProcessSampling.js +284 -0
  22. package/dist/application/RealHopperAssertions.js +116 -0
  23. package/dist/application/ReferenceSourceImport.js +182 -0
  24. package/dist/application/ReferenceSourceImportEntries.js +122 -0
  25. package/dist/application/ReferenceSourceImportPolicy.js +73 -0
  26. package/dist/application/ReferenceSourceImportTypes.js +18 -0
  27. package/dist/application/ReferenceSourceVcsAdapter.js +34 -0
  28. package/dist/application/Setup.js +192 -41
  29. package/dist/application/Uninstall.js +130 -0
  30. package/dist/application/runtime.js +8 -1
  31. package/dist/artifacts/ArtifactPaths.js +51 -0
  32. package/dist/artifacts/ArtifactProvider.js +130 -0
  33. package/dist/artifacts/ArtifactReader.js +9 -0
  34. package/dist/artifacts/AsarArtifactReader.js +62 -0
  35. package/dist/artifacts/DirectoryArtifactReader.js +107 -0
  36. package/dist/artifacts/MachOSliceArtifactReader.js +66 -0
  37. package/dist/artifacts/SafeOutputTree.js +199 -0
  38. package/dist/artifacts/StreamBytes.js +10 -0
  39. package/dist/artifacts/ZipArtifactReader.js +109 -0
  40. package/dist/cli.js +229 -21
  41. package/dist/cliEvidenceCommands.js +68 -0
  42. package/dist/cliLogging.js +21 -0
  43. package/dist/config.js +35 -1
  44. package/dist/contracts/artifactComparisonExample.js +95 -0
  45. package/dist/contracts/artifactToolContracts.js +84 -0
  46. package/dist/contracts/enhancedInputs.js +4 -0
  47. package/dist/contracts/functionComparisonExample.js +57 -0
  48. package/dist/contracts/investigationExamples.js +118 -0
  49. package/dist/contracts/nativeToolContracts.js +52 -0
  50. package/dist/contracts/processCaptureExample.js +25 -0
  51. package/dist/contracts/toolContractExamples.js +69 -0
  52. package/dist/contracts/toolContracts.js +99 -36
  53. package/dist/contracts/toolOutputSchemas.js +156 -82
  54. package/dist/contracts/unknownContractExamples.js +33 -0
  55. package/dist/domain/artifactComparison.js +273 -0
  56. package/dist/domain/artifactGraph.js +194 -0
  57. package/dist/domain/artifactInventoryEvidence.js +150 -0
  58. package/dist/domain/binaryTarget.js +55 -0
  59. package/dist/domain/bundleComparison.js +266 -0
  60. package/dist/domain/callPath.js +346 -0
  61. package/dist/domain/changedBehavior.js +294 -0
  62. package/dist/domain/errors.js +199 -5
  63. package/dist/domain/evidence.js +41 -10
  64. package/dist/domain/evidenceBundle.js +187 -6
  65. package/dist/domain/functionComparison.js +201 -0
  66. package/dist/domain/functionComparisonNormalization.js +112 -0
  67. package/dist/domain/functionComparisonResults.js +54 -0
  68. package/dist/domain/functionComparisonSchemas.js +82 -0
  69. package/dist/domain/functionDossierEvidence.js +171 -0
  70. package/dist/domain/hopperValues.js +35 -8
  71. package/dist/domain/nativeInspection.js +142 -0
  72. package/dist/domain/processCapture.js +152 -54
  73. package/dist/domain/processComparison.js +106 -0
  74. package/dist/domain/reconstructionUnknowns.js +90 -0
  75. package/dist/domain/reconstructionVerification.js +285 -0
  76. package/dist/domain/reconstructionVerificationSchemas.js +126 -0
  77. package/dist/domain/referenceSourceClassification.js +496 -0
  78. package/dist/domain/referenceSourceGraph.js +376 -0
  79. package/dist/domain/referenceSourceImportParsing.js +235 -0
  80. package/dist/domain/referenceSourcePolicy.js +1 -0
  81. package/dist/domain/residualUnknown.js +239 -0
  82. package/dist/domain/staticRuntimeCorrelation.js +375 -0
  83. package/dist/hopper/BridgeLauncher.js +39 -3
  84. package/dist/hopper/HopperClient.js +14 -5
  85. package/dist/hopper/HopperProvider.js +57 -22
  86. package/dist/hopper/protocol.js +13 -2
  87. package/dist/identity.js +1 -0
  88. package/dist/main.js +5 -1
  89. package/dist/native/CommandRunner.js +156 -0
  90. package/dist/native/NativeMacOSProvider.js +306 -0
  91. package/dist/native/NativeMachoInspection.js +135 -0
  92. package/dist/native/parsers/codesign.js +55 -0
  93. package/dist/native/parsers/demangle.js +26 -0
  94. package/dist/native/parsers/dyldInfo.js +25 -0
  95. package/dist/native/parsers/lipo.js +67 -0
  96. package/dist/native/parsers/otool.js +193 -0
  97. package/dist/native/parsers/plist.js +23 -0
  98. package/dist/reference/ReferenceSourceReader.js +73 -0
  99. package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
  100. package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
  101. package/dist/reference/ReferenceSourceReaderFile.js +119 -0
  102. package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
  103. package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
  104. package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
  105. package/dist/server/createServer.js +31 -5
  106. package/dist/server/recordDerivedEvidence.js +10 -0
  107. package/dist/server/registerArtifactComparisonTool.js +62 -0
  108. package/dist/server/registerArtifactTools.js +6 -0
  109. package/dist/server/registerBundleComparisonTool.js +47 -0
  110. package/dist/server/registerEnhancedTools.js +64 -12
  111. package/dist/server/registerEvidenceTools.js +36 -0
  112. package/dist/server/registerFunctionComparisonTool.js +68 -0
  113. package/dist/server/registerInvestigationTools.js +224 -0
  114. package/dist/server/registerNativeTools.js +6 -0
  115. package/dist/server/registerOfficialTools.js +47 -14
  116. package/dist/server/registerProcessComparisonTool.js +106 -0
  117. package/dist/server/registerSessionTools.js +179 -70
  118. package/dist/server/sessionEvidence.js +28 -0
  119. package/dist/server/sessionToolPolicies.js +64 -0
  120. package/dist/server/toolRegistrationOptions.js +7 -0
  121. package/dist/server/toolResult.js +8 -5
  122. package/install.sh +198 -0
  123. package/package.json +18 -1
  124. package/scripts/rea.mjs +5 -1
  125. package/skills/rea-analysis/SKILL.md +77 -2
@@ -5,6 +5,8 @@ import { basename, dirname, extname, join } from "node:path";
5
5
  import { promisify } from "node:util";
6
6
  import { HopperCancelledError, HopperStartError } from "../domain/errors.js";
7
7
  import { err, ok } from "../domain/result.js";
8
+ import { cleanupOwnedProcessGroup, } from "../application/ProcessOwnership.js";
9
+ import writeFileAtomic from "write-file-atomic";
8
10
  const execFileAsync = promisify(execFile);
9
11
  const HOPPER_BACKGROUND_STARTUP_MS = 5_000;
10
12
  /**
@@ -17,8 +19,9 @@ const HOPPER_BACKGROUND_STARTUP_MS = 5_000;
17
19
  * supported launcher; callers must treat possible foreground UI as an upstream
18
20
  * Hopper constraint, not as evidence that analysis failed.
19
21
  *
20
- * The returned process is the short-lived launcher helper, not the Hopper app,
21
- * so REA does not claim ownership of the application's lifetime.
22
+ * REA owns only the short-lived launcher helper's run-token-authenticated
23
+ * process group. It does not claim ownership of the Hopper GUI process that
24
+ * macOS LaunchServices may create or reuse.
22
25
  */
23
26
  export class HopperApplicationLauncher {
24
27
  options;
@@ -30,6 +33,7 @@ export class HopperApplicationLauncher {
30
33
  const source = [
31
34
  `REA_SOCKET = ${JSON.stringify(session.socketPath)}`,
32
35
  `REA_TOKEN = ${JSON.stringify(session.token)}`,
36
+ `REA_RUN_ID = ${JSON.stringify(session.runId)}`,
33
37
  `exec(compile(open(${JSON.stringify(this.options.bridgeScriptPath)}, 'rb').read(), ${JSON.stringify(this.options.bridgeScriptPath)}, 'exec'))`,
34
38
  "",
35
39
  ].join("\n");
@@ -57,6 +61,8 @@ export class HopperApplicationLauncher {
57
61
  return err(new HopperCancelledError());
58
62
  const child = spawn(this.options.launcherPath, args, {
59
63
  stdio: ["ignore", "ignore", "pipe"],
64
+ detached: process.platform !== "win32",
65
+ env: { ...process.env, REA_PROCESS_RUN_ID: session.runId },
60
66
  });
61
67
  const started = await new Promise((resolve) => {
62
68
  const onSpawn = () => {
@@ -72,13 +78,43 @@ export class HopperApplicationLauncher {
72
78
  });
73
79
  if (!started.ok)
74
80
  return started;
75
- return ok({ process: child, ownsProcessLifetime: false });
81
+ const pid = child.pid;
82
+ if (pid === undefined)
83
+ return err(new HopperStartError({ cause: new Error("missing launcher PID") }));
84
+ const ownership = {
85
+ schema_version: 1,
86
+ run_id: session.runId,
87
+ pid,
88
+ process_group_id: pid,
89
+ parent_pid: process.pid,
90
+ launcher: this.options.launcherPath,
91
+ created_at: new Date().toISOString(),
92
+ };
93
+ try {
94
+ await writeFileAtomic(join(session.directory, "ownership.json"), `${JSON.stringify(ownership)}\n`, { encoding: "utf8", mode: 0o600 });
95
+ }
96
+ catch (cause) {
97
+ await cleanupOwnedProcessGroup(ownedProcessGroup(session, pid, this.options.launcherPath));
98
+ return err(new HopperStartError({ cause }));
99
+ }
100
+ return ok({
101
+ process: child,
102
+ ownsProcessLifetime: true,
103
+ cleanup: () => cleanupOwnedProcessGroup(ownedProcessGroup(session, pid, this.options.launcherPath)),
104
+ });
76
105
  }
77
106
  catch (cause) {
78
107
  return err(new HopperStartError({ cause }));
79
108
  }
80
109
  }
81
110
  }
111
+ const ownedProcessGroup = (session, pid, launcherPath) => ({
112
+ runId: session.runId,
113
+ leaderPid: pid,
114
+ processGroupId: pid,
115
+ expectedCommand: launcherPath,
116
+ expectedParentPid: process.pid,
117
+ });
82
118
  const prepareHopperApplication = async (launcherPath, signal) => {
83
119
  const appBundle = hopperApplicationBundle(launcherPath);
84
120
  if (appBundle === undefined)
@@ -1,4 +1,4 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
2
  import { access, mkdtemp, rm } from "node:fs/promises";
3
3
  import { createConnection } from "node:net";
4
4
  import { tmpdir } from "node:os";
@@ -57,10 +57,12 @@ export class HopperClient {
57
57
  }
58
58
  const socketPath = join(this.#directory, "bridge.sock");
59
59
  this.#token = randomBytes(32).toString("hex");
60
+ const runId = randomUUID();
60
61
  const launched = await this.#options.launcher.launch({
61
62
  directory: this.#directory,
62
63
  socketPath,
63
64
  token: this.#token,
65
+ runId,
64
66
  }, signal === undefined ? {} : { signal });
65
67
  if (!launched.ok) {
66
68
  await this.close();
@@ -81,7 +83,7 @@ export class HopperClient {
81
83
  await this.close();
82
84
  return health;
83
85
  }
84
- const parsed = parseServerInfo(health.value);
86
+ const parsed = parseServerInfo(health.value, runId);
85
87
  if (!parsed.ok)
86
88
  await this.close();
87
89
  return parsed;
@@ -109,7 +111,13 @@ export class HopperClient {
109
111
  socket?.destroy();
110
112
  this.#socket = undefined;
111
113
  if (this.#launch?.ownsProcessLifetime === true) {
112
- this.#launch.process.kill("SIGTERM");
114
+ if (this.#launch.cleanup !== undefined) {
115
+ const cleanup = await this.#launch.cleanup();
116
+ if (!cleanup.cleaned)
117
+ this.#logger.warn({ reason: cleanup.reason }, "Owned launcher cleanup failed closed");
118
+ }
119
+ else
120
+ this.#launch.process.kill("SIGTERM");
113
121
  }
114
122
  this.#launch = undefined;
115
123
  if (this.#directory !== undefined) {
@@ -302,12 +310,13 @@ const connectOnce = async (socketPath) => new Promise((resolve) => {
302
310
  resolve(err(new HopperStartError({ cause })));
303
311
  });
304
312
  });
305
- const parseServerInfo = (value) => {
313
+ const parseServerInfo = (value, expectedRunId) => {
306
314
  if (typeof value === "object" &&
307
315
  value !== null &&
308
316
  !Array.isArray(value) &&
309
317
  value.name === "REA Hopper bridge" &&
310
- typeof value.version === "string") {
318
+ typeof value.version === "string" &&
319
+ value.run_id === expectedRunId) {
311
320
  return ok({ name: value.name, version: value.version });
312
321
  }
313
322
  return err(new HopperProtocolError("Hopper bridge health result is invalid"));
@@ -1,36 +1,54 @@
1
1
  import { fileURLToPath } from "node:url";
2
+ import { createAnalysisExecution } from "../application/AnalysisProvider.js";
3
+ import { AnalysisCapabilityUnavailableError } from "../domain/errors.js";
4
+ import { err } from "../domain/result.js";
5
+ import { ENHANCED_TOOL_CONTRACTS, OFFICIAL_TOOL_CONTRACTS, } from "../contracts/toolContracts.js";
2
6
  import { HopperApplicationLauncher } from "./BridgeLauncher.js";
3
7
  import { HopperClient } from "./HopperClient.js";
4
- const IDENTITY = {
8
+ const IDENTITY = Object.freeze({
5
9
  id: "hopper",
6
10
  name: "Hopper Disassembler",
7
11
  version: null,
8
- };
9
- const OPERATIONS = [
10
- "direct-analysis",
11
- "decompilation",
12
- "disassembly",
13
- "cross-references",
14
- "containing-procedure-resolution",
15
- "procedure-references",
16
- "analysis-metadata-mutation",
12
+ });
13
+ const MUTATING_OPERATIONS = new Set([
14
+ "set_address_name",
15
+ "set_addresses_names",
16
+ "set_bookmark",
17
+ "set_comment",
18
+ "set_inline_comment",
19
+ "unset_bookmark",
20
+ ]);
21
+ const PROVIDER_TOOL_CONTRACTS = [
22
+ ...OFFICIAL_TOOL_CONTRACTS,
23
+ ...ENHANCED_TOOL_CONTRACTS.filter((contract) => contract.name === "analyze_function"),
17
24
  ];
18
- const CAPABILITIES = OPERATIONS.map((operation) => ({
19
- operation,
20
- version: 1,
25
+ const CAPABILITIES = Object.freeze(PROVIDER_TOOL_CONTRACTS.map((contract) => Object.freeze({
26
+ provider: IDENTITY,
27
+ operation: contract.name,
28
+ inputContractVersion: 1,
29
+ outputContractVersion: 1,
21
30
  available: true,
22
- pagination: "none",
31
+ reason: null,
32
+ pagination: "offset" in contract.inputSchema.shape ? "offset" : "none",
23
33
  exhaustive: false,
24
- effects: {
25
- mutatesArtifact: operation === "analysis-metadata-mutation",
34
+ effects: Object.freeze({
35
+ mutatesArtifact: MUTATING_OPERATIONS.has(contract.name),
26
36
  launchesProcess: true,
27
37
  mayShowUi: true,
28
38
  mayAccessNetwork: false,
29
- mayWriteFilesystem: operation === "analysis-metadata-mutation",
30
- requiresPrivileges: false,
31
- },
32
- limitations: ["Results depend on Hopper's completed static analysis."],
33
- }));
39
+ mayWriteFilesystem: MUTATING_OPERATIONS.has(contract.name),
40
+ changesPermissions: false,
41
+ requiresRoot: false,
42
+ }),
43
+ limits: Object.freeze({
44
+ maxResults: null,
45
+ maxPayloadBytes: null,
46
+ timeoutMs: null,
47
+ }),
48
+ limitations: Object.freeze([
49
+ "Results depend on Hopper's completed static analysis.",
50
+ ]),
51
+ })));
34
52
  /** Concrete analysis provider backed by REA's private Hopper bridge. */
35
53
  export class HopperProvider {
36
54
  config;
@@ -46,6 +64,11 @@ export class HopperProvider {
46
64
  return CAPABILITIES;
47
65
  }
48
66
  createClient(target) {
67
+ if (target.kind !== "executable" && target.kind !== "database")
68
+ return {
69
+ execute: (operation) => Promise.resolve(err(new AnalysisCapabilityUnavailableError(IDENTITY.id, operation, `Hopper cannot open ${target.kind} targets directly. Inventory or extract the artifact first.`))),
70
+ close: () => Promise.resolve(),
71
+ };
49
72
  const client = new HopperClient({
50
73
  launcher: new HopperApplicationLauncher({
51
74
  launcherPath: this.config.hopperLauncherPath,
@@ -59,7 +82,19 @@ export class HopperProvider {
59
82
  logger: this.logger.child({ layer: "bridge" }),
60
83
  });
61
84
  return {
62
- execute: (operation, parameters, options) => client.callTool(operation, parameters, options),
85
+ execute: async (operation, parameters, options) => {
86
+ const result = await client.callTool(operation, parameters, options);
87
+ return result.ok
88
+ ? {
89
+ ok: true,
90
+ value: createAnalysisExecution(result.value, IDENTITY, {
91
+ limitations: [
92
+ "Results depend on Hopper's completed static analysis.",
93
+ ],
94
+ }),
95
+ }
96
+ : result;
97
+ },
63
98
  close: () => client.close(),
64
99
  };
65
100
  }
@@ -9,7 +9,18 @@ const responseSchema = z.union([
9
9
  }),
10
10
  z.object({
11
11
  id: z.number().int().nonnegative(),
12
- error: z.object({ code: z.number().int(), message: z.string() }),
12
+ error: z.object({
13
+ code: z.number().int(),
14
+ message: z.string(),
15
+ type: z
16
+ .enum([
17
+ "remote",
18
+ "authorization",
19
+ "invalid_request",
20
+ "bridge_exception",
21
+ ])
22
+ .default("remote"),
23
+ }),
13
24
  }),
14
25
  ]);
15
26
  /** Parse one complete Hopper NDJSON response line. */
@@ -28,5 +39,5 @@ export const parseResponseLine = (line) => {
28
39
  };
29
40
  /** Project a parsed response into its result or expected remote failure. */
30
41
  export const responseResult = (response) => "error" in response
31
- ? err(new HopperRemoteError(response.error.code, response.error.message))
42
+ ? err(new HopperRemoteError(response.error.code, response.error.message, response.error.type))
32
43
  : ok(response.result);
package/dist/identity.js CHANGED
@@ -10,5 +10,6 @@ export const PRODUCT_IDENTITY = {
10
10
  mcpCommand: "npx -y rea-agents mcp",
11
11
  mcpServerKey: "rea",
12
12
  skillName: "rea-analysis",
13
+ skillVersion: "10",
13
14
  configFileName: "rea.json",
14
15
  };
package/dist/main.js CHANGED
@@ -33,7 +33,11 @@ export const run = async () => {
33
33
  }
34
34
  let handle;
35
35
  try {
36
- handle = serveStdio(() => createServer(session, session, logger, config.value.processExecutionPolicy), {
36
+ handle = serveStdio(() => createServer(session, session, {
37
+ logger,
38
+ processPolicy: config.value.processExecutionPolicy,
39
+ evidenceFilePolicy: config.value.evidenceFilePolicy,
40
+ }), {
37
41
  onerror: () => {
38
42
  serverLogger.error("MCP stdio transport error");
39
43
  process.stderr.write("MCP stdio transport error\n");
@@ -0,0 +1,156 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { realpath } from "node:fs/promises";
4
+ import { spawn } from "node:child_process";
5
+ import { err, ok } from "../domain/result.js";
6
+ export class NativeCommandFailure extends Error {
7
+ tool;
8
+ reason;
9
+ exitCode;
10
+ constructor(tool, reason, exitCode = null, options) {
11
+ super(`Native command ${tool} failed: ${reason}`, options);
12
+ this.tool = tool;
13
+ this.reason = reason;
14
+ this.exitCode = exitCode;
15
+ }
16
+ }
17
+ /** Run allowlisted Xcode tools directly with bounded output and no shell. */
18
+ export class XcrunCommandRunner {
19
+ #resolved = new Map();
20
+ run(tool, arguments_, options) {
21
+ if (!ALLOWED_TOOLS.has(tool))
22
+ return Promise.resolve(err(new NativeCommandFailure(tool, "unavailable")));
23
+ return this.#resolve(tool).then(async (resolved) => {
24
+ if (!resolved.ok)
25
+ return resolved;
26
+ const captured = await captureProcess(resolved.value.path, arguments_, tool, options);
27
+ return captured.ok
28
+ ? ok({
29
+ ...captured.value,
30
+ tool,
31
+ executable: resolved.value.path,
32
+ executableSha256: resolved.value.sha256,
33
+ toolVersion: null,
34
+ versionReason: "Tool exposes no uniform stable version flag; executable digest identifies it.",
35
+ arguments: [...arguments_],
36
+ })
37
+ : captured;
38
+ });
39
+ }
40
+ #resolve(tool) {
41
+ const existing = this.#resolved.get(tool);
42
+ if (existing !== undefined)
43
+ return existing;
44
+ const pending = resolveTool(tool);
45
+ this.#resolved.set(tool, pending);
46
+ return pending;
47
+ }
48
+ }
49
+ const ALLOWED_TOOLS = new Set([
50
+ "codesign",
51
+ "dyld_info",
52
+ "dwarfdump",
53
+ "file",
54
+ "lipo",
55
+ "nm",
56
+ "otool",
57
+ "plutil",
58
+ "strings",
59
+ "swift-demangle",
60
+ "vtool",
61
+ ]);
62
+ const resolveTool = async (tool) => {
63
+ const found = await captureProcess("/usr/bin/xcrun", ["--find", tool], "xcrun", { timeoutMs: 5_000, maxOutputBytes: 64 * 1024 });
64
+ if (!found.ok || found.value.exitCode !== 0)
65
+ return err(new NativeCommandFailure(tool, "unavailable"));
66
+ const candidate = found.value.stdout.trim();
67
+ if (!candidate.startsWith("/"))
68
+ return err(new NativeCommandFailure(tool, "unavailable"));
69
+ try {
70
+ const path = await realpath(candidate);
71
+ return ok({ path, sha256: await hashFile(path) });
72
+ }
73
+ catch (cause) {
74
+ return err(new NativeCommandFailure(tool, "io", null, { cause }));
75
+ }
76
+ };
77
+ const captureProcess = (executable, arguments_, tool, options) => new Promise((resolve) => {
78
+ if (options.signal?.aborted === true) {
79
+ resolve(err(new NativeCommandFailure(tool, "cancelled")));
80
+ return;
81
+ }
82
+ const runToken = randomUUID();
83
+ const child = spawn(executable, [...arguments_], {
84
+ shell: false,
85
+ stdio: ["ignore", "pipe", "pipe"],
86
+ env: {
87
+ PATH: "/usr/bin:/bin:/usr/sbin:/sbin",
88
+ LC_ALL: "C",
89
+ LANG: "C",
90
+ REA_NATIVE_RUN_TOKEN: runToken,
91
+ },
92
+ });
93
+ const stdout = [];
94
+ const stderr = [];
95
+ let stdoutBytes = 0;
96
+ let stderrBytes = 0;
97
+ let settled = false;
98
+ let reason;
99
+ const finish = (result) => {
100
+ if (settled)
101
+ return;
102
+ settled = true;
103
+ clearTimeout(timeout);
104
+ options.signal?.removeEventListener("abort", onAbort);
105
+ resolve(result);
106
+ };
107
+ const stop = (failure) => {
108
+ reason ??= failure;
109
+ child.kill("SIGKILL");
110
+ };
111
+ const onAbort = () => stop("cancelled");
112
+ options.signal?.addEventListener("abort", onAbort, { once: true });
113
+ const timeout = setTimeout(() => stop("timeout"), options.timeoutMs);
114
+ child.stdout.on("data", (chunk) => {
115
+ stdoutBytes += chunk.length;
116
+ if (stdoutBytes + stderrBytes > options.maxOutputBytes)
117
+ stop("output-limit");
118
+ else
119
+ stdout.push(chunk);
120
+ });
121
+ child.stderr.on("data", (chunk) => {
122
+ stderrBytes += chunk.length;
123
+ if (stdoutBytes + stderrBytes > options.maxOutputBytes)
124
+ stop("output-limit");
125
+ else
126
+ stderr.push(chunk);
127
+ });
128
+ child.once("error", (cause) => finish(err(new NativeCommandFailure(tool, "io", null, { cause }))));
129
+ child.once("close", (code, signal) => {
130
+ if (reason !== undefined) {
131
+ finish(err(new NativeCommandFailure(tool, reason, code)));
132
+ return;
133
+ }
134
+ if (code !== 0 && options.acceptNonZero !== true) {
135
+ finish(err(new NativeCommandFailure(tool, "nonzero-exit", code)));
136
+ return;
137
+ }
138
+ finish(ok({
139
+ stdout: Buffer.concat(stdout).toString("utf8"),
140
+ stderr: Buffer.concat(stderr).toString("utf8"),
141
+ stdoutBytes,
142
+ stderrBytes,
143
+ stdoutTruncated: false,
144
+ stderrTruncated: false,
145
+ exitCode: code,
146
+ signal,
147
+ }));
148
+ });
149
+ });
150
+ const hashFile = (path) => new Promise((resolve, reject) => {
151
+ const hash = createHash("sha256");
152
+ const stream = createReadStream(path);
153
+ stream.on("data", (chunk) => hash.update(chunk));
154
+ stream.once("error", reject);
155
+ stream.once("end", () => resolve(hash.digest("hex")));
156
+ });