rea-agents 2.5.0 → 3.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 (88) hide show
  1. package/README.md +14 -4
  2. package/bridge/ghidra/ReaGhidraBridge.java +477 -4
  3. package/bridge/hopper_bridge.py +54 -4
  4. package/dist/application/AnalysisContextQueries.js +130 -0
  5. package/dist/application/AnalysisSnapshotCache.js +4 -2
  6. package/dist/application/ArtifactInventory/scanCanonical.js +1 -1
  7. package/dist/application/BinarySession.js +5 -4
  8. package/dist/application/BinarySessionComposition.js +21 -0
  9. package/dist/application/BinarySessionRecords.js +79 -9
  10. package/dist/application/CapabilityInventory.js +16 -2
  11. package/dist/application/ClientRegistrationStatus.js +4 -1
  12. package/dist/application/DirectAnalysis.js +6 -2
  13. package/dist/application/EnhancedTools.js +37 -7
  14. package/dist/application/EvidenceLedger.js +45 -16
  15. package/dist/application/JavaScriptArtifactAnalysis.js +13 -4
  16. package/dist/application/LazyAnalysisProvider.js +77 -0
  17. package/dist/application/LoopbackReplay.js +1 -1
  18. package/dist/application/NativeApiInspection.js +80 -0
  19. package/dist/application/SessionProviderRouter.js +6 -6
  20. package/dist/application/SetupClientConfiguration.js +4 -2
  21. package/dist/application/runtime.js +26 -9
  22. package/dist/browser/PlaywrightBrowserScenarioProvider.js +8 -2
  23. package/dist/catalogIdentity.js +3 -3
  24. package/dist/cli/artifactCommands.js +5 -3
  25. package/dist/cli/coreAnalysisCommands.js +41 -0
  26. package/dist/cliCommandNames.js +1 -0
  27. package/dist/contracts/artifactToolContracts.js +8 -3
  28. package/dist/contracts/enhancedInputs.js +10 -0
  29. package/dist/contracts/functionWorkflowToolContracts.js +30 -0
  30. package/dist/contracts/promptContracts.js +14 -15
  31. package/dist/contracts/toolContractExamples.js +3 -0
  32. package/dist/contracts/toolContracts.js +21 -4
  33. package/dist/contracts/toolEffects.js +4 -0
  34. package/dist/contracts/toolOutputSchemaGroups.js +50 -10
  35. package/dist/domain/analysisErrorPresentation.js +8 -2
  36. package/dist/domain/analysisErrorProjection.js +1 -0
  37. package/dist/domain/bytecodeProvider.js +179 -0
  38. package/dist/domain/conformancePackage.js +169 -0
  39. package/dist/domain/conformanceReplay.js +104 -0
  40. package/dist/domain/conformanceTrustGate.js +193 -0
  41. package/dist/domain/customProtocolCapture.js +137 -0
  42. package/dist/domain/eventProcessTree.js +214 -0
  43. package/dist/domain/evidenceBundle.js +47 -0
  44. package/dist/domain/hopperStartupFailure.js +1 -1
  45. package/dist/domain/hopperValues.js +4 -1
  46. package/dist/domain/inputIssueProjection.js +7 -1
  47. package/dist/domain/javascriptSemanticAnalysis.js +8 -12
  48. package/dist/domain/javascriptSourceParser.js +15 -0
  49. package/dist/domain/javascriptStaticAnalysis.js +8 -12
  50. package/dist/domain/mobileApplicationInvestigation.js +159 -0
  51. package/dist/domain/nativeApiBoundary.js +130 -0
  52. package/dist/domain/objcSwiftMetadata.js +150 -0
  53. package/dist/domain/packageAnalysis.js +123 -0
  54. package/dist/domain/peInspection.js +196 -0
  55. package/dist/domain/protocolCapture.js +243 -0
  56. package/dist/dotnet/ManagedStaticProvider.js +34 -5
  57. package/dist/generatedMcpToolCatalog.js +37 -0
  58. package/dist/generatedPackageMetadata.js +4 -4
  59. package/dist/ghidra/GhidraDefaults.js +1 -1
  60. package/dist/ghidra/GhidraFunctionValues.js +27 -1
  61. package/dist/ghidra/GhidraProviderCapabilities.js +40 -38
  62. package/dist/hopper/HopperClient.js +26 -2
  63. package/dist/hopper/HopperProvider.js +42 -30
  64. package/dist/hopper/HopperRequestQueue.js +32 -4
  65. package/dist/hopper/HopperResponseStream.js +2 -2
  66. package/dist/hopper/protocol.js +63 -21
  67. package/dist/main/transport.js +2 -5
  68. package/dist/main.js +14 -12
  69. package/dist/mcpDoctor.js +2 -2
  70. package/dist/mcpStartupPolicy.js +14 -0
  71. package/dist/server/LazyToolCatalog.js +134 -0
  72. package/dist/server/createServer.js +64 -171
  73. package/dist/server/hydrateServerTools.js +191 -0
  74. package/dist/server/registerEnhancedTools.js +13 -3
  75. package/dist/server/registerEvidenceResources.js +50 -101
  76. package/dist/server/registerInvestigationTools.js +1 -2
  77. package/dist/server/registerJavaScriptApplicationGraphResource.js +0 -4
  78. package/dist/server/registerReconstructionObligationLedgerResource.js +0 -7
  79. package/dist/server/registerReconstructionReadinessResource.js +0 -7
  80. package/dist/server/registerSessionRecordTools.js +82 -12
  81. package/dist/server/registerSessionStatusTool.js +1 -1
  82. package/dist/server/registerSessionTools.js +21 -1
  83. package/dist/server/toolRegistrationOptions.js +21 -6
  84. package/dist/server/toolResult.js +12 -0
  85. package/package.json +21 -10
  86. package/scripts/package-runner-bootstrap.mjs +72 -0
  87. package/scripts/rea.mjs +11 -1
  88. package/skills/reverse-engineer-anything/SKILL.md +10 -3
@@ -0,0 +1,130 @@
1
+ import { AnalysisOutputError } from "../domain/errors.js";
2
+ import { err, ok } from "../domain/result.js";
3
+ const parameters = (document, address) => ({
4
+ ...(document === undefined ? {} : { document }),
5
+ ...(address === undefined ? {} : { address }),
6
+ });
7
+ const executionOptions = (signal) => signal === undefined ? {} : { signal };
8
+ /** Compose the provider's volatile selection state into one coherent query. */
9
+ export const getNavigationContext = async (analysis, input, signal) => {
10
+ const document = input.document ??
11
+ (await analysis.execute("current_document", {}, executionOptions(signal)));
12
+ if (typeof document !== "string" && !document.ok)
13
+ return document;
14
+ const documentName = typeof document === "string" ? document : document.value.result;
15
+ const documentArgs = typeof documentName === "string" ? parameters(documentName) : {};
16
+ const address = await analysis.execute("current_address", documentArgs, executionOptions(signal));
17
+ if (!address.ok)
18
+ return address;
19
+ if (typeof address.value.result !== "string")
20
+ return ok({
21
+ document: documentName,
22
+ address: address.value.result,
23
+ procedure: null,
24
+ });
25
+ const procedure = await analysis.execute("resolve_containing_procedure", parameters(typeof documentName === "string" ? documentName : undefined, address.value.result), executionOptions(signal));
26
+ if (!procedure.ok) {
27
+ if (!isMissingCurrentProcedure(procedure.error))
28
+ return procedure;
29
+ return ok({
30
+ document: documentName,
31
+ address: address.value.result,
32
+ procedure: null,
33
+ });
34
+ }
35
+ return ok({
36
+ document: documentName,
37
+ address: address.value.result,
38
+ procedure: containingProcedureName(procedure.value.result),
39
+ });
40
+ };
41
+ /** Inspect bounded provider-neutral facets at one explicit reproducible address. */
42
+ export const inspectAddressContext = async (analysis, input, signal) => {
43
+ const document = await resolveDocument(analysis, input.document, signal);
44
+ if (!document.ok)
45
+ return document;
46
+ const args = parameters(document.value, input.address);
47
+ const operations = [
48
+ "address_name",
49
+ "resolve_containing_procedure",
50
+ "comment",
51
+ "inline_comment",
52
+ "list_bookmarks",
53
+ ];
54
+ const results = await Promise.all(operations.map((operation) => analysis.execute(operation, operation === "list_bookmarks" ? parameters(document.value) : args, executionOptions(signal))));
55
+ for (const result of results) {
56
+ if (!result.ok && isRequestLevelFailure(result.error))
57
+ return result;
58
+ }
59
+ const name = facet(results[0], operations[0]);
60
+ const procedure = facet(results[1], operations[1]);
61
+ const comment = facet(results[2], operations[2]);
62
+ const inlineComment = facet(results[3], operations[3]);
63
+ const bookmarks = facet(results[4], operations[4]);
64
+ return ok({
65
+ address: input.address,
66
+ document: document.value,
67
+ name,
68
+ procedure,
69
+ comment,
70
+ inline_comment: inlineComment,
71
+ bookmarks: bookmarks.state === "available"
72
+ ? {
73
+ state: "available",
74
+ value: matchingBookmarks(bookmarks.value, resolvedAddress(results[1], input.address)),
75
+ }
76
+ : bookmarks,
77
+ });
78
+ };
79
+ const resolveDocument = async (analysis, document, signal) => {
80
+ if (document !== undefined)
81
+ return ok(document);
82
+ const current = await analysis.execute("current_document", {}, executionOptions(signal));
83
+ if (!current.ok)
84
+ return current;
85
+ return typeof current.value.result === "string"
86
+ ? ok(current.value.result)
87
+ : err(new AnalysisOutputError("current_document", "expected a document identity string"));
88
+ };
89
+ const facet = (result, operation) => result?.ok === true
90
+ ? { state: "available", value: result.value.result }
91
+ : {
92
+ state: "unavailable",
93
+ reason: result?.error.message ?? `${operation} returned no result`,
94
+ remediation: `Call ${operation} directly to inspect provider availability and diagnostics.`,
95
+ };
96
+ const isMissingCurrentProcedure = (error) => /(?:no procedure exists|not in a procedure)/iu.test(error.message);
97
+ const isRequestLevelFailure = (error) => error._tag === "AnalysisCancelledError" ||
98
+ error._tag === "AnalysisTimeoutError" ||
99
+ error._tag === "NoBinaryOpenError" ||
100
+ error._tag === "PermissionRequiredError";
101
+ const containingProcedureName = (value) => {
102
+ if (typeof value !== "object" || value === null || Array.isArray(value))
103
+ return null;
104
+ if (value.found !== true)
105
+ return null;
106
+ const procedure = value.procedure;
107
+ if (typeof procedure !== "object" ||
108
+ procedure === null ||
109
+ Array.isArray(procedure))
110
+ return null;
111
+ return typeof procedure.name === "string" ? procedure.name : null;
112
+ };
113
+ const resolvedAddress = (result, fallback) => {
114
+ if (result?.ok !== true)
115
+ return fallback;
116
+ const value = result.value.result;
117
+ if (typeof value !== "object" || value === null || Array.isArray(value))
118
+ return fallback;
119
+ return typeof value.query_address === "string"
120
+ ? value.query_address
121
+ : fallback;
122
+ };
123
+ const matchingBookmarks = (value, address) => {
124
+ if (!Array.isArray(value))
125
+ return [];
126
+ return value.filter((bookmark) => typeof bookmark === "object" &&
127
+ bookmark !== null &&
128
+ !Array.isArray(bookmark) &&
129
+ bookmark.address === address);
130
+ };
@@ -1,7 +1,7 @@
1
1
  import { EvidenceIntegrityError, NoBinaryOpenError, } from "../domain/errors.js";
2
2
  import { err, ok } from "../domain/result.js";
3
3
  import { analysisQueryId, createAnalysisSnapshotEntry, snapshotBinding, snapshotMatchesProfile, snapshotMatchesTarget, snapshotTarget, } from "../domain/analysisSnapshot.js";
4
- import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
4
+ import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
5
5
  const STATEFUL_OPERATIONS = new Set([
6
6
  "health",
7
7
  "current_address",
@@ -19,7 +19,9 @@ const CURSOR_DEFAULT_OPERATIONS = new Set([
19
19
  "prev_address",
20
20
  "xrefs",
21
21
  ]);
22
- const DOCUMENT_SCOPED_OPERATIONS = new Set(OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name).filter((name) => name !== "current_document" &&
22
+ const DOCUMENT_SCOPED_OPERATIONS = new Set(GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy")
23
+ .map(({ name }) => name)
24
+ .filter((name) => name !== "current_document" &&
23
25
  name !== "list_documents" &&
24
26
  name !== "set_current_document"));
25
27
  /** Whether an operation is immutable and independent of provider UI state. */
@@ -12,7 +12,7 @@ export const scanCanonicalArtifactInventory = async (path, limits, options = {})
12
12
  const integrity = options.integrity ?? STRICT_INTEGRITY_POLICY;
13
13
  if (integrity.mode === "record-and-continue" &&
14
14
  (!integrity.approved || !integrity.enabled))
15
- throw new ArtifactReaderFailure("unavailable", "Integrity continuation requires explicit approval and operator policy");
15
+ throw new ArtifactReaderFailure("policy", "Integrity continuation requires explicit approval and operator policy");
16
16
  const metadata = await lstat(path);
17
17
  const rootFormat = await classifyRoot(path, metadata.isDirectory());
18
18
  const rootDigest = metadata.isDirectory()
@@ -2,15 +2,15 @@ import { randomUUID } from "node:crypto";
2
2
  import { analysisProfilesEqual, } from "../domain/analysisProfile.js";
3
3
  import { AnalysisCancelledError, ProviderAdapterError, } from "../domain/errors.js";
4
4
  import { err, ok } from "../domain/result.js";
5
- import { enhancedToolNameSchema } from "../contracts/enhancedInputs.js";
6
- import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
5
+ import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
7
6
  import { SessionProviderRouter, } from "./SessionProviderRouter.js";
8
7
  import { BinarySessionRecords } from "./BinarySessionRecords.js";
9
8
  import { binarySessionStatus } from "./BinarySessionStatus.js";
10
9
  import { resolveSessionOpen, } from "./BinarySessionOpen.js";
11
10
  import { prepareSessionExecution } from "./BinarySessionExecution.js";
12
11
  import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
13
- const OFFICIAL_OPERATIONS = new Set(OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name));
12
+ const OFFICIAL_OPERATIONS = new Set(GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy").map(({ name }) => name));
13
+ const ENHANCED_OPERATIONS = new Set(GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "enhanced").map(({ name }) => name));
14
14
  /**
15
15
  * Owns the single active target shared by CLI and MCP adapters.
16
16
  *
@@ -42,7 +42,7 @@ export class BinarySession extends BinarySessionRecords {
42
42
  const exact = route.capabilities?.get(operation)?.provider;
43
43
  if (exact !== undefined)
44
44
  selected = exact;
45
- else if (enhancedToolNameSchema.safeParse(operation).success) {
45
+ else if (ENHANCED_OPERATIONS.has(operation)) {
46
46
  const providers = new Map();
47
47
  for (const descriptor of route.capabilities?.values() ?? [])
48
48
  if (descriptor.available &&
@@ -112,6 +112,7 @@ export class BinarySession extends BinarySessionRecords {
112
112
  if (!imported.ok)
113
113
  return imported;
114
114
  }
115
+ this.resetSnapshotInvalidation();
115
116
  return ok(target);
116
117
  }
117
118
  await this.#drainCalls();
@@ -0,0 +1,21 @@
1
+ import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
2
+ import { BinarySession } from "./BinarySession.js";
3
+ import { SessionProviderRouter } from "./SessionProviderRouter.js";
4
+ const normalizeBinarySessionOptions = (optionsOrResolver) => typeof optionsOrResolver === "function"
5
+ ? { resolveAnalysisProfile: optionsOrResolver }
6
+ : (optionsOrResolver ?? {});
7
+ /**
8
+ * Compose one provider-neutral session from deep-provider selection and
9
+ * disjoint auxiliary operation families.
10
+ */
11
+ export const composeBinarySession = (registry, auxiliaryProviders = []) => new BinarySession(registry instanceof SessionProviderRouter
12
+ ? registry
13
+ : SessionProviderRouter.selectable(registry, auxiliaryProviders));
14
+ /**
15
+ * Compose a session through the same internal seam for focused adapter tests.
16
+ * The constructor remains private to this composition module, so callers test
17
+ * provider behavior without coupling to BinarySession construction.
18
+ */
19
+ export const composeBinarySessionFromFactory = (provider, optionsOrResolver) => new BinarySession(provider, normalizeBinarySessionOptions(optionsOrResolver));
20
+ /** Compose a focused session from any provider-shaped test seam. */
21
+ export const composeBinarySessionFromProvider = (provider, optionsOrResolver) => new BinarySession(provider, normalizeBinarySessionOptions(optionsOrResolver));
@@ -1,24 +1,37 @@
1
1
  import { createEvidence } from "../domain/evidence.js";
2
- import { evidenceBundleForTarget } from "../domain/evidenceBundle.js";
3
- import { EvidenceIntegrityError, } from "../domain/errors.js";
2
+ import { evidenceBundleForTarget, serializeEvidenceBundle, } from "../domain/evidenceBundle.js";
3
+ import { EvidenceIntegrityError, EvidenceLimitError, } from "../domain/errors.js";
4
4
  import { parseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
5
5
  import { parseReconstructionCoverageWorkspace, } from "../domain/reconstructionCoverage.js";
6
- import { err } from "../domain/result.js";
6
+ import { err, ok } from "../domain/result.js";
7
7
  import { AnalysisSnapshotCache } from "./AnalysisSnapshotCache.js";
8
8
  import { EvidenceLedger } from "./EvidenceLedger.js";
9
9
  import { UNKNOWN_REGISTRY_PROVIDER, unknownEvidenceLinks, unknownMutationEvidence, } from "./UnknownEvidence.js";
10
10
  /** Owns session evidence, snapshots, workspaces, and residual unknowns. */
11
11
  export class BinarySessionRecords {
12
+ static MAX_RETAINED_BUNDLES = 16;
13
+ static MAX_RETAINED_BUNDLE_BYTES = 64 * 1024 * 1024;
12
14
  #evidence = new EvidenceLedger({
13
15
  maxRecords: 10_000,
14
16
  maxBytes: 64 * 1024 * 1024,
15
17
  });
16
18
  #snapshot = new AnalysisSnapshotCache();
19
+ #retainedBundles = new Map();
20
+ #retainedBundleBytes = 0;
17
21
  #investigationWorkspaces = new Map();
18
22
  #coverageWorkspaces = new Map();
19
23
  #snapshotInvalidated = false;
24
+ #snapshotListeners = new Set();
25
+ /** Observe changes to the mutable current analysis snapshot resource. */
26
+ onAnalysisSnapshotChanged(listener) {
27
+ this.#snapshotListeners.add(listener);
28
+ return () => this.#snapshotListeners.delete(listener);
29
+ }
20
30
  recordEvidence(evidence) {
21
- return this.#evidence.record(evidence);
31
+ const recorded = this.#evidence.record(evidence);
32
+ if (recorded.ok && recorded.value === "added")
33
+ this.#emitSnapshotChanged();
34
+ return recorded;
22
35
  }
23
36
  hasEvidence(evidenceId) {
24
37
  return this.#evidence.has(evidenceId);
@@ -29,8 +42,38 @@ export class BinarySessionRecords {
29
42
  exportEvidenceBundle() {
30
43
  return this.#evidence.export();
31
44
  }
45
+ snapshotEvidenceBundle() {
46
+ const bundle = this.#evidence.export();
47
+ const encoded = serializeEvidenceBundle(bundle);
48
+ const digest = createHash("sha256").update(encoded).digest("hex");
49
+ const existing = this.#retainedBundles.get(digest);
50
+ if (existing === undefined) {
51
+ if (this.#retainedBundles.size >= BinarySessionRecords.MAX_RETAINED_BUNDLES)
52
+ return err(new EvidenceLimitError("records", BinarySessionRecords.MAX_RETAINED_BUNDLES));
53
+ const bytes = Buffer.byteLength(encoded);
54
+ if (this.#retainedBundleBytes + bytes >
55
+ BinarySessionRecords.MAX_RETAINED_BUNDLE_BYTES)
56
+ return err(new EvidenceLimitError("bytes", BinarySessionRecords.MAX_RETAINED_BUNDLE_BYTES));
57
+ this.#retainedBundles.set(digest, encoded);
58
+ this.#retainedBundleBytes += bytes;
59
+ }
60
+ return ok({
61
+ bundleDigest: digest,
62
+ bundleVersion: 2,
63
+ bytes: Buffer.byteLength(existing ?? encoded),
64
+ records: bundle.records.length,
65
+ unknowns: bundle.unknowns.length,
66
+ uri: `rea://evidence-bundle/${digest}`,
67
+ });
68
+ }
69
+ retainedEvidenceBundle(digest) {
70
+ return this.#retainedBundles.get(digest);
71
+ }
32
72
  importEvidenceBundle(bundle) {
33
- return this.#evidence.import(bundle);
73
+ const imported = this.#evidence.import(bundle);
74
+ if (imported.ok && imported.value > 0)
75
+ this.#emitSnapshotChanged();
76
+ return imported;
34
77
  }
35
78
  exportAnalysisSnapshot() {
36
79
  const active = this.activeAnalysisBinding();
@@ -48,36 +91,53 @@ export class BinarySessionRecords {
48
91
  const active = this.activeAnalysisBinding();
49
92
  if (active?.profile === null)
50
93
  return err(new EvidenceIntegrityError("Analysis snapshot profile_mismatch: the active target has no concrete analysis profile"));
51
- return this.#snapshot.import(snapshot, active === undefined
94
+ const imported = this.#snapshot.import(snapshot, active === undefined
52
95
  ? undefined
53
96
  : { target: active.target, profile: active.profile }, (bundle) => this.#evidence.import(bundle));
97
+ if (imported.ok)
98
+ this.#emitSnapshotChanged();
99
+ return imported;
54
100
  }
55
101
  matchesSnapshot(target, profile) {
56
102
  return this.#snapshot.matches(target, profile ?? undefined);
57
103
  }
58
104
  selectSnapshot(target, profile) {
59
105
  this.#snapshot.select(target, profile);
106
+ this.#emitSnapshotChanged();
60
107
  }
61
108
  lookupSnapshot(target, profile, operation, parameters) {
62
109
  return this.#snapshot.lookup(target, profile, operation, parameters);
63
110
  }
64
111
  recordSnapshot(input) {
65
112
  this.#snapshot.record(input);
113
+ this.#emitSnapshotChanged();
66
114
  }
67
115
  invalidateSnapshot() {
68
116
  this.#snapshot.clear();
69
117
  this.#snapshotInvalidated = true;
118
+ this.#emitSnapshotChanged();
70
119
  }
71
120
  resetSnapshotInvalidation() {
121
+ if (!this.#snapshotInvalidated)
122
+ return;
72
123
  this.#snapshotInvalidated = false;
124
+ this.#emitSnapshotChanged();
73
125
  }
74
126
  clearSnapshot() {
75
127
  this.#snapshot.clear();
128
+ this.#emitSnapshotChanged();
76
129
  }
77
130
  clearSessionRecords() {
78
131
  this.#evidence.clear();
79
132
  this.#snapshot.clear();
80
133
  this.#snapshotInvalidated = false;
134
+ this.#retainedBundles.clear();
135
+ this.#retainedBundleBytes = 0;
136
+ this.#emitSnapshotChanged();
137
+ }
138
+ #emitSnapshotChanged() {
139
+ for (const listener of this.#snapshotListeners)
140
+ void listener();
81
141
  }
82
142
  retainInvestigationWorkspace(workspace) {
83
143
  const parsed = parseInvestigationWorkspace(workspace);
@@ -111,10 +171,16 @@ export class BinarySessionRecords {
111
171
  }
112
172
  recordUnknown(input) {
113
173
  const target = this.activeAnalysisBinding()?.target;
114
- return this.#evidence.recordUnknown(input, unknownMutationEvidence(target, input));
174
+ const recorded = this.#evidence.recordUnknown(input, unknownMutationEvidence(target, input));
175
+ if (recorded.ok)
176
+ this.#emitSnapshotChanged();
177
+ return recorded;
115
178
  }
116
179
  recordEvidenceWithUnknown(evidence, input) {
117
- return this.#evidence.recordWithUnknown(evidence, input, unknownMutationEvidence(undefined, input));
180
+ const recorded = this.#evidence.recordWithUnknown(evidence, input, unknownMutationEvidence(undefined, input));
181
+ if (recorded.ok)
182
+ this.#emitSnapshotChanged();
183
+ return recorded;
118
184
  }
119
185
  updateUnknown(input) {
120
186
  const target = this.activeAnalysisBinding()?.target;
@@ -133,7 +199,10 @@ export class BinarySessionRecords {
133
199
  "Registry mutation evidence records analyst intent, not proof of the answer.",
134
200
  ],
135
201
  });
136
- return this.#evidence.updateUnknown(input, evidence);
202
+ const updated = this.#evidence.updateUnknown(input, evidence);
203
+ if (updated.ok)
204
+ this.#emitSnapshotChanged();
205
+ return updated;
137
206
  }
138
207
  listUnknowns(filters = {}) {
139
208
  return this.#evidence.listUnknowns(filters);
@@ -147,3 +216,4 @@ const sortedWorkspaces = (workspaces) => [...workspaces]
147
216
  .sort((left, right) => left.workspace_id.localeCompare(right.workspace_id) ||
148
217
  left.revision - right.revision)
149
218
  .map((workspace) => structuredClone(workspace));
219
+ import { createHash } from "node:crypto";
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
2
+ import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
3
3
  const statusSchema = z.object({
4
4
  open: z.boolean(),
5
5
  kind: z.enum(["executable", "database", "archive", "artifact"]).optional(),
@@ -38,6 +38,7 @@ const ENHANCED_REQUIREMENTS = {
38
38
  "list_strings",
39
39
  ],
40
40
  analyze_function: ["analyze_function"],
41
+ inspect_native_api: ["analyze_function"],
41
42
  trace_feature: [
42
43
  "list_strings",
43
44
  "list_procedures",
@@ -45,6 +46,13 @@ const ENHANCED_REQUIREMENTS = {
45
46
  "resolve_containing_procedure",
46
47
  ],
47
48
  };
49
+ const SESSION_COMPOSITION_REQUIREMENTS = {
50
+ get_navigation_context: [
51
+ "current_document",
52
+ "current_address",
53
+ "resolve_containing_procedure",
54
+ ],
55
+ };
48
56
  /** Build stable per-operation availability for discovery and tool visibility. */
49
57
  export const buildCapabilityInventory = (sessionStatus, policy, clientFeatures = NO_CLIENT_FEATURES) => {
50
58
  const status = statusSchema.parse(sessionStatus);
@@ -56,7 +64,7 @@ export const buildCapabilityInventory = (sessionStatus, policy, clientFeatures =
56
64
  availability_code: descriptor.availability_code ?? null,
57
65
  },
58
66
  ]));
59
- return TOOL_CONTRACTS.map((contract) => {
67
+ return GENERATED_MCP_TOOL_CATALOG.map((contract) => {
60
68
  const availability = availabilityFor({
61
69
  name: contract.name,
62
70
  kind: contract.kind,
@@ -101,6 +109,9 @@ const clientRequirementsFor = (name, clientFeatures) => {
101
109
  };
102
110
  };
103
111
  const availabilityFor = (context) => {
112
+ const sessionComposition = SESSION_COMPOSITION_REQUIREMENTS[context.name];
113
+ if (sessionComposition !== undefined)
114
+ return composedAvailabilityFor(sessionComposition, context.descriptors);
104
115
  const javascriptApplication = javascriptApplicationAvailability(context);
105
116
  if (javascriptApplication !== null)
106
117
  return javascriptApplication;
@@ -240,6 +251,9 @@ const composedAvailability = (name, descriptors) => {
240
251
  reason: "provider_missing",
241
252
  remediation: "No provider composition is declared for this operation.",
242
253
  };
254
+ return composedAvailabilityFor(requirements, descriptors);
255
+ };
256
+ const composedAvailabilityFor = (requirements, descriptors) => {
243
257
  const missing = requirements.find((operation) => !descriptors.has(operation));
244
258
  if (missing !== undefined)
245
259
  return {
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
3
3
  import { parse as parseToml } from "smol-toml";
4
4
  import { z } from "zod";
5
5
  import { PRODUCT_IDENTITY } from "../identity.js";
6
+ import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js";
6
7
  import { supportedClients } from "./SupportedClients.js";
7
8
  const objectSchema = z.record(z.string(), z.unknown());
8
9
  const registrationSchema = z
@@ -41,7 +42,9 @@ export const readClientRegistrationStatuses = async (home, currentCommandPath =
41
42
  };
42
43
  const registrationAligned = (registration, client, currentCommandPath) => {
43
44
  const command = [registration.command, ...registration.args];
44
- if (client === "codex" && registration.startup_timeout_sec !== 30)
45
+ if (client === "codex" &&
46
+ registration.startup_timeout_sec !==
47
+ MCP_STARTUP_POLICY.codexStartupTimeoutSeconds)
45
48
  return false;
46
49
  if (command.length === 4 &&
47
50
  command[0] === "npx" &&
@@ -182,6 +182,7 @@ const executeAnalysisTool = async (input) => {
182
182
  const { session, tool, signal, evidenceProfile } = input;
183
183
  if (tool === "binary_overview" ||
184
184
  tool === "analyze_function" ||
185
+ tool === "inspect_native_api" ||
185
186
  tool === "trace_feature") {
186
187
  const result = await new EnhancedTools(session).execute(tool, input.arguments, signal);
187
188
  if (!result.ok)
@@ -247,15 +248,18 @@ const prepareSnapshot = async (options) => {
247
248
  return err(new EvidenceIntegrityError("Analysis snapshot target does not match the requested binary"));
248
249
  return ok({ snapshot: loaded.value });
249
250
  };
250
- const replayProviderFor = (session, tool) => tool === "binary_overview" || tool === "trace_feature"
251
+ const replayProviderFor = (session, tool) => isWorkflowEvidenceTool(tool)
251
252
  ? REA_WORKFLOW_PROVIDER
252
253
  : session.providerIdentity(tool);
253
254
  const analysisProfileForEvidence = (session, tool) => {
254
- if (tool !== "binary_overview" && tool !== "trace_feature")
255
+ if (!isWorkflowEvidenceTool(tool))
255
256
  return session.analysisProfile(tool);
256
257
  const upstream = session.analysisProfile();
257
258
  return upstream === undefined ? undefined : workflowAnalysisProfile(upstream);
258
259
  };
260
+ const isWorkflowEvidenceTool = (tool) => tool === "binary_overview" ||
261
+ tool === "inspect_native_api" ||
262
+ tool === "trace_feature";
259
263
  const fileExists = async (path) => {
260
264
  try {
261
265
  await access(path);
@@ -1,12 +1,14 @@
1
1
  import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
2
2
  import { AnalysisCancelledError, AnalysisOutputError, projectAnalysisError, } from "../domain/errors.js";
3
- import { addressDistance, parseDocuments, parseFunctionDossier, parseListCount, parseRelatedAddresses, parseSegments, } from "../domain/hopperValues.js";
3
+ import { addressDistance, functionDossierSchema, parseDocuments, parseFunctionDossier, parseListCount, parseRelatedAddresses, parseSegments, } from "../domain/hopperValues.js";
4
4
  import { err, ok } from "../domain/result.js";
5
5
  import { categorizeSwiftTypes, discoverObjcClasses, discoverObjcProtocols, discoverSwiftClasses, } from "../domain/symbolAnalysis.js";
6
+ import { jsonValueSchema } from "../domain/jsonValue.js";
6
7
  import { invalidEnhancedInput, } from "./EnhancedToolTypes.js";
7
8
  import { readAllAddressed } from "./EnhancedToolPagination.js";
8
9
  import { traceCallPath } from "./CallPathTracing.js";
9
10
  import { traceLiteralFeature } from "./EnhancedLiteralTracing.js";
11
+ import { projectNativeApiInspection } from "./NativeApiInspection.js";
10
12
  /**
11
13
  * Composes direct provider operations into bounded reverse-engineering tools.
12
14
  * Direct callers use `execute`; adapters that own validation use
@@ -23,6 +25,8 @@ export class EnhancedTools {
23
25
  name === "find_code_for_string" ||
24
26
  name === "trace_call_path")
25
27
  return this.#executeTracing(name, input, signal);
28
+ if (name === "analyze_function" || name === "inspect_native_api")
29
+ return this.#executeFunctionAnalysis(name, input, signal);
26
30
  switch (name) {
27
31
  case "swift_classes": {
28
32
  const parsed = enhancedInputSchemas.swift_classes.safeParse(input);
@@ -64,12 +68,6 @@ export class EnhancedTools {
64
68
  ? this.executeValidated({ name, input: parsed.data }, signal)
65
69
  : invalidEnhancedInput(name, parsed.error);
66
70
  }
67
- case "analyze_function": {
68
- const parsed = enhancedInputSchemas.analyze_function.safeParse(input);
69
- return parsed.success
70
- ? this.executeValidated({ name, input: parsed.data }, signal)
71
- : invalidEnhancedInput(name, parsed.error);
72
- }
73
71
  }
74
72
  }
75
73
  /** Dispatch input already parsed by a trusted adapter boundary. */
@@ -93,6 +91,8 @@ export class EnhancedTools {
93
91
  return this.#binaryOverview(call.input, signal);
94
92
  case "analyze_function":
95
93
  return this.#analyzeFunction(call.input, signal);
94
+ case "inspect_native_api":
95
+ return this.#inspectNativeApi(call.input, signal);
96
96
  case "trace_feature":
97
97
  return traceLiteralFeature((name, arguments_, operationSignal) => this.#call(name, arguments_, operationSignal), call.input, "feature", signal);
98
98
  case "find_code_for_string":
@@ -115,10 +115,40 @@ export class EnhancedTools {
115
115
  input: enhancedInputSchemas[name].parse(parsed.data),
116
116
  }, signal);
117
117
  }
118
+ #executeFunctionAnalysis(name, input, signal) {
119
+ const parsed = enhancedInputSchemas[name].safeParse(input);
120
+ if (!parsed.success)
121
+ return invalidEnhancedInput(name, parsed.error);
122
+ if (name === "analyze_function")
123
+ return this.executeValidated({
124
+ name,
125
+ input: enhancedInputSchemas.analyze_function.parse(parsed.data),
126
+ }, signal);
127
+ return this.executeValidated({
128
+ name,
129
+ input: enhancedInputSchemas.inspect_native_api.parse(parsed.data),
130
+ }, signal);
131
+ }
118
132
  async #analyzeFunction(input, signal) {
119
133
  const result = await this.#call("analyze_function", input, signal);
120
134
  return result.ok ? parseFunctionDossier(result.value) : result;
121
135
  }
136
+ async #inspectNativeApi(input, signal) {
137
+ const analysisInput = enhancedInputSchemas.analyze_function.parse({
138
+ procedure: input.procedure,
139
+ include_assembly: true,
140
+ max_pseudocode_chars: input.max_pseudocode_chars,
141
+ max_instructions: input.max_instructions,
142
+ });
143
+ const result = await this.#call("analyze_function", analysisInput, signal);
144
+ if (!result.ok)
145
+ return result;
146
+ const parsed = parseFunctionDossier(result.value);
147
+ if (!parsed.ok)
148
+ return parsed;
149
+ const dossier = functionDossierSchema.parse(parsed.value);
150
+ return ok(jsonValueSchema.parse(projectNativeApiInspection(dossier)));
151
+ }
122
152
  async #swiftClasses(pattern, signal) {
123
153
  const procedures = await this.#allAddressed("list_procedures", signal);
124
154
  return procedures.ok