rea-agents 0.2.1 → 0.4.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 (122) hide show
  1. package/README.md +124 -20
  2. package/bridge/hopper_bridge.py +141 -13
  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 +164 -14
  8. package/dist/application/CompositeProvider.js +73 -0
  9. package/dist/application/DirectAnalysis.js +27 -3
  10. package/dist/application/Doctor.js +35 -6
  11. package/dist/application/EnhancedTools.js +8 -7
  12. package/dist/application/EvidenceBundleCommands.js +29 -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 +84 -0
  21. package/dist/application/ProcessSampling.js +284 -0
  22. package/dist/application/RealHopperAssertions.js +105 -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 +153 -6
  41. package/dist/config.js +35 -1
  42. package/dist/contracts/artifactComparisonExample.js +95 -0
  43. package/dist/contracts/artifactToolContracts.js +84 -0
  44. package/dist/contracts/enhancedInputs.js +4 -0
  45. package/dist/contracts/functionComparisonExample.js +57 -0
  46. package/dist/contracts/investigationExamples.js +118 -0
  47. package/dist/contracts/nativeToolContracts.js +52 -0
  48. package/dist/contracts/processCaptureExample.js +25 -0
  49. package/dist/contracts/toolContractExamples.js +69 -0
  50. package/dist/contracts/toolContracts.js +99 -36
  51. package/dist/contracts/toolOutputSchemas.js +154 -82
  52. package/dist/contracts/unknownContractExamples.js +33 -0
  53. package/dist/domain/artifactComparison.js +273 -0
  54. package/dist/domain/artifactGraph.js +194 -0
  55. package/dist/domain/artifactInventoryEvidence.js +150 -0
  56. package/dist/domain/binaryTarget.js +55 -0
  57. package/dist/domain/bundleComparison.js +266 -0
  58. package/dist/domain/callPath.js +346 -0
  59. package/dist/domain/changedBehavior.js +294 -0
  60. package/dist/domain/errors.js +192 -4
  61. package/dist/domain/evidence.js +41 -10
  62. package/dist/domain/evidenceBundle.js +187 -6
  63. package/dist/domain/functionComparison.js +201 -0
  64. package/dist/domain/functionComparisonNormalization.js +112 -0
  65. package/dist/domain/functionComparisonResults.js +54 -0
  66. package/dist/domain/functionComparisonSchemas.js +82 -0
  67. package/dist/domain/functionDossierEvidence.js +171 -0
  68. package/dist/domain/hopperValues.js +14 -7
  69. package/dist/domain/nativeInspection.js +142 -0
  70. package/dist/domain/processCapture.js +152 -54
  71. package/dist/domain/processComparison.js +106 -0
  72. package/dist/domain/reconstructionUnknowns.js +90 -0
  73. package/dist/domain/reconstructionVerification.js +285 -0
  74. package/dist/domain/reconstructionVerificationSchemas.js +126 -0
  75. package/dist/domain/referenceSourceClassification.js +496 -0
  76. package/dist/domain/referenceSourceGraph.js +376 -0
  77. package/dist/domain/referenceSourceImportParsing.js +235 -0
  78. package/dist/domain/residualUnknown.js +239 -0
  79. package/dist/domain/staticRuntimeCorrelation.js +375 -0
  80. package/dist/hopper/BridgeLauncher.js +40 -3
  81. package/dist/hopper/HopperClient.js +14 -5
  82. package/dist/hopper/HopperProvider.js +57 -22
  83. package/dist/identity.js +1 -0
  84. package/dist/main.js +5 -1
  85. package/dist/native/CommandRunner.js +156 -0
  86. package/dist/native/NativeMacOSProvider.js +306 -0
  87. package/dist/native/NativeMachoInspection.js +135 -0
  88. package/dist/native/parsers/codesign.js +55 -0
  89. package/dist/native/parsers/demangle.js +26 -0
  90. package/dist/native/parsers/dyldInfo.js +25 -0
  91. package/dist/native/parsers/lipo.js +67 -0
  92. package/dist/native/parsers/otool.js +193 -0
  93. package/dist/native/parsers/plist.js +23 -0
  94. package/dist/reference/ReferenceSourceReader.js +73 -0
  95. package/dist/reference/ReferenceSourceReaderEntries.js +206 -0
  96. package/dist/reference/ReferenceSourceReaderErrors.js +19 -0
  97. package/dist/reference/ReferenceSourceReaderFile.js +119 -0
  98. package/dist/reference/ReferenceSourceReaderPaths.js +23 -0
  99. package/dist/reference/ReferenceSourceReaderTypes.js +2 -0
  100. package/dist/reference/ReferenceSourceReaderValidate.js +71 -0
  101. package/dist/server/createServer.js +31 -5
  102. package/dist/server/recordDerivedEvidence.js +10 -0
  103. package/dist/server/registerArtifactComparisonTool.js +62 -0
  104. package/dist/server/registerArtifactTools.js +6 -0
  105. package/dist/server/registerBundleComparisonTool.js +47 -0
  106. package/dist/server/registerEnhancedTools.js +64 -12
  107. package/dist/server/registerEvidenceTools.js +36 -0
  108. package/dist/server/registerFunctionComparisonTool.js +68 -0
  109. package/dist/server/registerInvestigationTools.js +224 -0
  110. package/dist/server/registerNativeTools.js +6 -0
  111. package/dist/server/registerOfficialTools.js +47 -14
  112. package/dist/server/registerProcessComparisonTool.js +106 -0
  113. package/dist/server/registerSessionTools.js +179 -70
  114. package/dist/server/sessionEvidence.js +28 -0
  115. package/dist/server/sessionToolPolicies.js +64 -0
  116. package/dist/server/toolRegistrationOptions.js +7 -0
  117. package/dist/server/toolResult.js +8 -5
  118. package/install.sh +198 -0
  119. package/package.json +19 -2
  120. package/scripts/rea.mjs +5 -1
  121. package/skills/rea-analysis/SKILL.md +77 -2
  122. /package/dist/{application/HopperToolPort.js → domain/referenceSourcePolicy.js} +0 -0
@@ -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
  }
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
+ });
@@ -0,0 +1,306 @@
1
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
2
+ import { realpath, stat } from "node:fs/promises";
3
+ import { parse as parseXmlPlist } from "plist";
4
+ import { z } from "zod";
5
+ import { createAnalysisExecution, } from "../application/AnalysisProvider.js";
6
+ import { NATIVE_TOOL_CONTRACTS, } from "../contracts/nativeToolContracts.js";
7
+ import { AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisOutputError, AnalysisTimeoutError, ProviderAdapterError, } from "../domain/errors.js";
8
+ import { jsonValueSchema } from "../domain/jsonValue.js";
9
+ import { demangleSwiftSchema, inspectPlistSchema, inspectSignatureSchema, listArchitecturesSchema, } from "../domain/nativeInspection.js";
10
+ import { err, ok } from "../domain/result.js";
11
+ import { NativeCommandFailure, XcrunCommandRunner, } from "./CommandRunner.js";
12
+ import { parseCodeSignature } from "./parsers/codesign.js";
13
+ import { parseDemangledSymbols } from "./parsers/demangle.js";
14
+ import { parseLipoArchitectures } from "./parsers/lipo.js";
15
+ import { parsePlistJson } from "./parsers/plist.js";
16
+ import { architectureLocations, inspectNativeMacho, } from "./NativeMachoInspection.js";
17
+ const IDENTITY = Object.freeze({
18
+ id: "native-macos",
19
+ name: "macOS native inspection utilities",
20
+ version: null,
21
+ });
22
+ const TIMEOUT_MS = 30_000;
23
+ const OUTPUT_LIMIT = 4 * 1024 * 1024;
24
+ /** Read-only semantic provider composed from Xcode command-line utilities. */
25
+ export class NativeMacOSProvider {
26
+ runner;
27
+ #capabilities;
28
+ constructor(runner = new XcrunCommandRunner(), platform = process.platform) {
29
+ this.runner = runner;
30
+ const available = platform === "darwin";
31
+ this.#capabilities = Object.freeze(NATIVE_TOOL_CONTRACTS.map((contract) => {
32
+ const availability = available
33
+ ? { available: true, reason: null }
34
+ : {
35
+ available: false,
36
+ reason: "Native macOS utilities require macOS.",
37
+ };
38
+ return Object.freeze({
39
+ provider: IDENTITY,
40
+ operation: contract.name,
41
+ inputContractVersion: 1,
42
+ outputContractVersion: 1,
43
+ ...availability,
44
+ pagination: "none",
45
+ exhaustive: contract.name !== "inspect_macho",
46
+ effects: Object.freeze({
47
+ mutatesArtifact: false,
48
+ launchesProcess: true,
49
+ mayShowUi: false,
50
+ mayAccessNetwork: false,
51
+ mayWriteFilesystem: false,
52
+ changesPermissions: false,
53
+ requiresRoot: false,
54
+ }),
55
+ limits: Object.freeze({
56
+ maxResults: contract.name === "demangle_swift" ? 500 : null,
57
+ maxPayloadBytes: OUTPUT_LIMIT,
58
+ timeoutMs: TIMEOUT_MS,
59
+ }),
60
+ limitations: Object.freeze([
61
+ "Availability and textual formats depend on the installed macOS/Xcode toolchain.",
62
+ ]),
63
+ });
64
+ }));
65
+ }
66
+ identity() {
67
+ return IDENTITY;
68
+ }
69
+ capabilities() {
70
+ return this.#capabilities;
71
+ }
72
+ createClient(target) {
73
+ return new NativeMacOSClient(target, this.runner);
74
+ }
75
+ }
76
+ class NativeMacOSClient {
77
+ target;
78
+ runner;
79
+ constructor(target, runner) {
80
+ this.target = target;
81
+ this.runner = runner;
82
+ }
83
+ async execute(operation, parameters, options) {
84
+ if (operation === "health")
85
+ return ok(createAnalysisExecution(null, IDENTITY));
86
+ if (!isNativeOperation(operation))
87
+ return err(new AnalysisCapabilityUnavailableError(IDENTITY.id, operation, "Operation is not implemented by native macOS tools."));
88
+ try {
89
+ const observation = await this.#dispatch(operation, parameters, options?.signal);
90
+ return observation.ok
91
+ ? ok(createAnalysisExecution(observation.value.result, IDENTITY, {
92
+ rawResult: { provenance: observation.value.provenance },
93
+ limitations: observation.value.limitations,
94
+ locations: observation.value.locations,
95
+ }))
96
+ : observation;
97
+ }
98
+ catch (cause) {
99
+ return err(new AnalysisOutputError(operation, "Native output parsing failed", {
100
+ cause,
101
+ }));
102
+ }
103
+ }
104
+ close() {
105
+ return Promise.resolve();
106
+ }
107
+ #dispatch(operation, parameters, signal) {
108
+ switch (operation) {
109
+ case "inspect_macho":
110
+ return this.#inspectMacho(signal);
111
+ case "inspect_signature":
112
+ return this.#inspectSignature(signal);
113
+ case "inspect_plist":
114
+ return this.#inspectPlist(parameters, signal);
115
+ case "list_architectures":
116
+ return this.#listArchitectures(signal);
117
+ case "demangle_swift":
118
+ return this.#demangle(parameters, signal);
119
+ }
120
+ }
121
+ async #listArchitectures(signal) {
122
+ const capture = await this.#run("list_architectures", "lipo", ["-detailed_info", this.target.path], { signal });
123
+ if (!capture.ok)
124
+ return capture;
125
+ const provenance = [invocation(capture.value, this.target.path)];
126
+ const result = listArchitecturesSchema.parse({
127
+ architectures: {
128
+ items: parseLipoArchitectures(capture.value.stdout),
129
+ total: parseLipoArchitectures(capture.value.stdout).length,
130
+ exhaustive: true,
131
+ limitations: [],
132
+ },
133
+ provenance,
134
+ limitations: [],
135
+ });
136
+ return ok({
137
+ result: jsonValueSchema.parse(result),
138
+ provenance,
139
+ limitations: [],
140
+ locations: architectureLocations(result.architectures.items),
141
+ });
142
+ }
143
+ async #demangle(parameters, signal) {
144
+ const symbols = z.array(z.string()).safeParse(parameters.symbols);
145
+ if (!symbols.success)
146
+ return err(new AnalysisOutputError("demangle_swift", "symbols were not parsed strings"));
147
+ const capture = await this.#run("demangle_swift", "swift-demangle", ["--compact", ...symbols.data], { signal });
148
+ if (!capture.ok)
149
+ return capture;
150
+ const provenance = [invocation(capture.value, this.target.path)];
151
+ const result = demangleSwiftSchema.parse({
152
+ symbols: parseDemangledSymbols(symbols.data, capture.value.stdout),
153
+ provenance,
154
+ limitations: [],
155
+ });
156
+ return ok({
157
+ result: jsonValueSchema.parse(result),
158
+ provenance,
159
+ limitations: [],
160
+ locations: [],
161
+ });
162
+ }
163
+ async #inspectSignature(signal) {
164
+ const display = await this.#run("inspect_signature", "codesign", ["-d", "--verbose=4", this.target.path], { signal, acceptNonZero: true });
165
+ if (!display.ok)
166
+ return display;
167
+ const requirements = await this.#run("inspect_signature", "codesign", ["-d", "-r-", this.target.path], { signal, acceptNonZero: true });
168
+ if (!requirements.ok)
169
+ return requirements;
170
+ const entitlements = await this.#run("inspect_signature", "codesign", ["-d", "--entitlements", ":-", this.target.path], { signal, acceptNonZero: true });
171
+ if (!entitlements.ok)
172
+ return entitlements;
173
+ const unsigned = /not signed at all|code object is not signed/iu.test(`${display.value.stdout}\n${display.value.stderr}`);
174
+ const parsed = parseCodeSignature(`${display.value.stdout}\n${display.value.stderr}`, unsigned);
175
+ const requirementText = /designated\s*=>\s*(.+)$/mu.exec(`${requirements.value.stdout}\n${requirements.value.stderr}`)?.[1] ?? null;
176
+ const entitlementValue = parseEntitlements(`${entitlements.value.stdout}\n${entitlements.value.stderr}`);
177
+ const provenance = [
178
+ display.value,
179
+ requirements.value,
180
+ entitlements.value,
181
+ ].map((capture) => invocation(capture, this.target.path));
182
+ const result = inspectSignatureSchema.parse({
183
+ ...parsed,
184
+ designated_requirement: requirementText,
185
+ entitlements: entitlementValue,
186
+ provenance,
187
+ });
188
+ return ok({
189
+ result: jsonValueSchema.parse(result),
190
+ provenance,
191
+ limitations: result.limitations,
192
+ locations: [],
193
+ });
194
+ }
195
+ async #inspectPlist(parameters, signal) {
196
+ const requested = parameters.relative_path;
197
+ if (typeof requested !== "string")
198
+ return err(new AnalysisOutputError("inspect_plist", "relative_path was not a string"));
199
+ const plist = await resolvePlistPath(this.target, requested);
200
+ if (!plist.ok)
201
+ return plist;
202
+ const classified = await this.#run("inspect_plist", "file", ["-b", plist.value], { signal });
203
+ if (!classified.ok)
204
+ return classified;
205
+ const capture = await this.#run("inspect_plist", "plutil", ["-convert", "json", "-o", "-", "--", plist.value], { signal });
206
+ if (!capture.ok)
207
+ return capture;
208
+ const parsed = parsePlistJson(capture.value.stdout);
209
+ const provenance = [classified.value, capture.value].map((item) => invocation(item, plist.value, "$PLIST"));
210
+ const result = inspectPlistSchema.parse({
211
+ format: /binary property list/iu.test(classified.value.stdout)
212
+ ? "binary"
213
+ : /XML|text/iu.test(classified.value.stdout)
214
+ ? "xml"
215
+ : "unknown",
216
+ ...parsed,
217
+ source_path: requested,
218
+ provenance,
219
+ limitations: [],
220
+ });
221
+ return ok({
222
+ result: jsonValueSchema.parse(result),
223
+ provenance,
224
+ limitations: [],
225
+ locations: [{ kind: "artifact-path", path: requested }],
226
+ });
227
+ }
228
+ async #inspectMacho(signal) {
229
+ if (this.target.format !== "mach-o")
230
+ return err(new AnalysisCapabilityUnavailableError(IDENTITY.id, "inspect_macho", "Active artifact is not Mach-O."));
231
+ return inspectNativeMacho({
232
+ target: this.target,
233
+ ...(signal === undefined ? {} : { signal }),
234
+ run: (tool, arguments_, commandSignal) => this.#run("inspect_macho", tool, arguments_, {
235
+ signal: commandSignal,
236
+ }),
237
+ invocation: (capture) => invocation(capture, this.target.path),
238
+ });
239
+ }
240
+ async #run(operation, tool, arguments_, options = {}) {
241
+ const captured = await this.runner.run(tool, arguments_, {
242
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
243
+ timeoutMs: TIMEOUT_MS,
244
+ maxOutputBytes: OUTPUT_LIMIT,
245
+ acceptNonZero: options.acceptNonZero ?? false,
246
+ });
247
+ if (captured.ok)
248
+ return captured;
249
+ return err(translateCommandFailure(operation, captured.error));
250
+ }
251
+ }
252
+ const isNativeOperation = (operation) => NATIVE_TOOL_CONTRACTS.some(({ name }) => name === operation);
253
+ const translateCommandFailure = (operation, failure) => {
254
+ if (failure.reason === "unavailable")
255
+ return new AnalysisCapabilityUnavailableError(IDENTITY.id, operation, `${failure.tool} is unavailable through xcrun.`);
256
+ if (failure.reason === "cancelled")
257
+ return new AnalysisCancelledError(operation);
258
+ if (failure.reason === "timeout")
259
+ return new AnalysisTimeoutError(operation, TIMEOUT_MS);
260
+ return new ProviderAdapterError(IDENTITY.id, operation, { cause: failure });
261
+ };
262
+ const invocation = (capture, artifactPath, alias = "$ARTIFACT") => ({
263
+ tool: capture.tool,
264
+ command: [
265
+ capture.executable,
266
+ ...capture.arguments.map((argument) => argument === artifactPath ? alias : argument),
267
+ ],
268
+ tool_version: capture.toolVersion,
269
+ version_reason: capture.versionReason,
270
+ executable_sha256: capture.executableSha256,
271
+ exit: { code: capture.exitCode, signal: capture.signal },
272
+ stdout_bytes: capture.stdoutBytes,
273
+ stderr_bytes: capture.stderrBytes,
274
+ stdout_truncated: capture.stdoutTruncated,
275
+ stderr_truncated: capture.stderrTruncated,
276
+ });
277
+ const parseEntitlements = (output) => {
278
+ const start = output.indexOf("<?xml");
279
+ const end = output.lastIndexOf("</plist>");
280
+ if (start < 0 || end < start)
281
+ return null;
282
+ return jsonValueSchema.parse(parseXmlPlist(output.slice(start, end + "</plist>".length)));
283
+ };
284
+ const resolvePlistPath = async (target, requested) => {
285
+ try {
286
+ const source = target.sourcePath ?? target.path;
287
+ const sourceMetadata = await stat(source);
288
+ const root = sourceMetadata.isDirectory() ? source : dirname(target.path);
289
+ const candidate = resolve(root, requested);
290
+ if (!within(root, candidate))
291
+ return err(new ProviderAdapterError(IDENTITY.id, "inspect_plist"));
292
+ const canonicalRoot = await realpath(root);
293
+ const canonical = await realpath(candidate);
294
+ if (!within(canonicalRoot, canonical))
295
+ return err(new ProviderAdapterError(IDENTITY.id, "inspect_plist"));
296
+ return ok(canonical);
297
+ }
298
+ catch (cause) {
299
+ return err(new ProviderAdapterError(IDENTITY.id, "inspect_plist", { cause }));
300
+ }
301
+ };
302
+ const within = (root, candidate) => {
303
+ const child = relative(root, candidate);
304
+ return (child === "" ||
305
+ (!child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute(child)));
306
+ };