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
@@ -1,9 +1,9 @@
1
1
  /** Generated from package.json and package-lock.json; do not edit. */
2
2
  export const PACKAGE_METADATA = {
3
3
  name: "rea-agents",
4
- version: "2.5.0", // x-release-please-version
5
- serverSdkVersion: "2.0.0-beta.4",
6
- clientSdkVersion: "2.0.0-beta.4",
7
- coreSdkVersion: "2.0.0-beta.4",
4
+ version: "3.0.0", // x-release-please-version
5
+ serverSdkVersion: "2.0.0",
6
+ clientSdkVersion: "2.0.0",
7
+ coreSdkVersion: "2.0.0",
8
8
  skillVersion: "23",
9
9
  };
@@ -17,4 +17,4 @@ export const GHIDRA_MAX_QUEUED_REQUESTS = 32;
17
17
  /** Maximum encoded response line accepted from the Java bridge. */
18
18
  export const GHIDRA_MAX_LINE_BYTES = 1024 * 1024;
19
19
  /** Version of REA's Java bridge handshake and request protocol. */
20
- export const GHIDRA_BRIDGE_VERSION = 5;
20
+ export const GHIDRA_BRIDGE_VERSION = 6;
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { AnalysisInputError, AnalysisOutputError } from "../domain/errors.js";
3
3
  import { functionInstructionWindowSchema, functionDossierSchema, } from "../domain/hopperValues.js";
4
4
  import { jsonObjectSchema, jsonValueSchema, } from "../domain/jsonValue.js";
5
+ import { nativeApiBoundarySchema } from "../domain/nativeApiBoundary.js";
5
6
  import { err, ok } from "../domain/result.js";
6
7
  import { ghidraBoundedIdentifierSchema, ghidraCanonicalAddressSchema, } from "./GhidraInventoryValues.js";
7
8
  /** Read-only function operations admitted by the Ghidra adapter. */
@@ -191,7 +192,32 @@ const procedureInfo = z
191
192
  classification,
192
193
  })
193
194
  .strict();
194
- const ghidraFunctionDossier = functionDossierSchema.superRefine((value, context) => {
195
+ const ghidraNativeApiBoundary = nativeApiBoundarySchema.superRefine((value, context) => {
196
+ if (!value.available)
197
+ return;
198
+ if (value.provenance !== "ghidra-high-function")
199
+ context.addIssue({
200
+ code: "custom",
201
+ message: "Ghidra native API observations require HighFunction provenance",
202
+ });
203
+ const addresses = value.jump_tables.flatMap((table) => [
204
+ table.dispatch_address,
205
+ ...table.data_sources.map(({ address }) => address),
206
+ ...table.mappings.flatMap(({ target_address, data_addresses }) => [
207
+ target_address,
208
+ ...data_addresses,
209
+ ]),
210
+ ]);
211
+ if (addresses.some((address) => !ghidraCanonicalAddressSchema.safeParse(address).success))
212
+ context.addIssue({
213
+ code: "custom",
214
+ message: "Ghidra native API observation contains a non-canonical address",
215
+ });
216
+ });
217
+ const ghidraFunctionDossier = functionDossierSchema
218
+ .extend({ native_api: ghidraNativeApiBoundary })
219
+ .strict()
220
+ .superRefine((value, context) => {
195
221
  const addresses = [
196
222
  value.procedure.address,
197
223
  ...value.comments.items.map(({ address }) => address),
@@ -1,4 +1,4 @@
1
- import { ENHANCED_TOOL_CONTRACTS, OFFICIAL_TOOL_CONTRACTS, } from "../contracts/toolContracts.js";
1
+ import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
2
2
  import { GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS, GHIDRA_MAX_LINE_BYTES, GHIDRA_REQUEST_TIMEOUT_MS, } from "./GhidraDefaults.js";
3
3
  import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js";
4
4
  import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js";
@@ -8,10 +8,7 @@ export const GHIDRA_PROVIDER_IDENTITY = Object.freeze({
8
8
  name: "Ghidra",
9
9
  version: null,
10
10
  });
11
- const providerContractByName = new Map([...OFFICIAL_TOOL_CONTRACTS, ...ENHANCED_TOOL_CONTRACTS].map((contract) => [
12
- contract.name,
13
- contract,
14
- ]));
11
+ const providerContractByName = new Map(GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy" || kind === "enhanced").map((contract) => [contract.name, contract]));
15
12
  /** Provider-neutral read-only contracts implemented by the Ghidra adapter. */
16
13
  export const GHIDRA_PROVIDER_TOOL_CONTRACTS = Object.freeze([...GHIDRA_INVENTORY_OPERATIONS, ...GHIDRA_FUNCTION_OPERATIONS].map((operation) => {
17
14
  const contract = providerContractByName.get(operation);
@@ -137,39 +134,44 @@ export const limitationsFor = (operation) => {
137
134
  }
138
135
  };
139
136
  /** Provider-neutral capabilities advertised by every non-Windows Ghidra session. */
140
- export const CAPABILITIES = Object.freeze(GHIDRA_PROVIDER_TOOL_CONTRACTS.map((contract) => Object.freeze({
141
- provider: GHIDRA_PROVIDER_IDENTITY,
142
- operation: contract.name,
143
- inputContractVersion: 1,
144
- outputContractVersion: 1,
145
- available: true,
146
- reason: null,
147
- pagination: PAGINATED_OPERATIONS.has(contract.name)
148
- ? "offset"
149
- : "none",
150
- exhaustive: !PAGINATED_OPERATIONS.has(contract.name),
151
- effects: Object.freeze({
152
- mutatesArtifact: false,
153
- launchesProcess: true,
154
- mayShowUi: false,
155
- mayAccessNetwork: false,
156
- mayWriteFilesystem: true,
157
- changesPermissions: false,
158
- requiresRoot: false,
159
- }),
160
- limits: Object.freeze({
161
- maxResults: PAGINATED_OPERATIONS.has(contract.name)
162
- ? SEARCH_OPERATIONS.has(contract.name)
163
- ? 100
164
- : 500
165
- : null,
166
- maxPayloadBytes: GHIDRA_MAX_LINE_BYTES,
167
- timeoutMs: DECOMPILE_OPERATIONS.has(contract.name)
168
- ? GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS
169
- : GHIDRA_REQUEST_TIMEOUT_MS,
170
- }),
171
- limitations: Object.freeze(limitationsFor(contract.name)),
172
- })));
137
+ export const CAPABILITIES = Object.freeze(GHIDRA_PROVIDER_TOOL_CONTRACTS.map((contract) => {
138
+ const operation = contract.analysisOperation;
139
+ if (operation === null)
140
+ throw new TypeError(`Missing analysis operation for ${contract.name}`);
141
+ return Object.freeze({
142
+ provider: GHIDRA_PROVIDER_IDENTITY,
143
+ operation,
144
+ inputContractVersion: 1,
145
+ outputContractVersion: 1,
146
+ available: true,
147
+ reason: null,
148
+ pagination: PAGINATED_OPERATIONS.has(contract.name)
149
+ ? "offset"
150
+ : "none",
151
+ exhaustive: !PAGINATED_OPERATIONS.has(contract.name),
152
+ effects: Object.freeze({
153
+ mutatesArtifact: false,
154
+ launchesProcess: true,
155
+ mayShowUi: false,
156
+ mayAccessNetwork: false,
157
+ mayWriteFilesystem: true,
158
+ changesPermissions: false,
159
+ requiresRoot: false,
160
+ }),
161
+ limits: Object.freeze({
162
+ maxResults: PAGINATED_OPERATIONS.has(contract.name)
163
+ ? SEARCH_OPERATIONS.has(contract.name)
164
+ ? 100
165
+ : 500
166
+ : null,
167
+ maxPayloadBytes: GHIDRA_MAX_LINE_BYTES,
168
+ timeoutMs: DECOMPILE_OPERATIONS.has(contract.name)
169
+ ? GHIDRA_DECOMPILE_REQUEST_TIMEOUT_MS
170
+ : GHIDRA_REQUEST_TIMEOUT_MS,
171
+ }),
172
+ limitations: Object.freeze(limitationsFor(contract.name)),
173
+ });
174
+ }));
173
175
  /** Capabilities advertised for the experimental Windows x64 P0 boundary. */
174
176
  export const WINDOWS_P0_CAPABILITIES = Object.freeze(CAPABILITIES.map((capability) => Object.freeze({
175
177
  ...capability,
@@ -15,7 +15,7 @@ import { connectHopperSocketOnce } from "./HopperSocketConnection.js";
15
15
  import { hopperLauncherFailureDiagnostic } from "./HopperProcessDiagnostic.js";
16
16
  import { HopperRequestQueue, } from "./HopperRequestQueue.js";
17
17
  import { HopperResponseStream } from "./HopperResponseStream.js";
18
- import { responseResult } from "./protocol.js";
18
+ import { responseResult, } from "./protocol.js";
19
19
  const SHUTDOWN_TIMEOUT_MS = 30_000;
20
20
  const MAX_QUEUED_REQUESTS = 64;
21
21
  const SESSION_ROOT = process.platform === "darwin" ? "/tmp" : tmpdir();
@@ -76,7 +76,7 @@ export class HopperClient {
76
76
  });
77
77
  });
78
78
  this.#responses = new HopperResponseStream({
79
- accept: (response) => this.#requests.accept(response.id, responseResult(response)),
79
+ accept: (message) => this.#acceptBridgeMessage(message),
80
80
  hasQueued: (id) => this.#requests.hasQueued(id),
81
81
  nextRequestId: () => this.#nextId,
82
82
  abort: (message, cause) => this.#abortProtocol(message, cause),
@@ -333,6 +333,30 @@ export class HopperClient {
333
333
  socket.on("error", this.#onSocketError);
334
334
  socket.on("close", this.#onSocketClose);
335
335
  }
336
+ #acceptBridgeMessage(message) {
337
+ if (!("event" in message))
338
+ return this.#requests.accept(message.id, responseResult(message));
339
+ if (!this.#requests.acceptEvent(message))
340
+ return false;
341
+ this.#emitBridgeDiagnostic(message);
342
+ return true;
343
+ }
344
+ #emitBridgeDiagnostic(message) {
345
+ if (message.event.type !== "diagnostic")
346
+ return;
347
+ try {
348
+ this.#options.onDiagnostic?.({
349
+ type: "bridge-diagnostic",
350
+ request_id: message.id,
351
+ code: message.event.error.code,
352
+ category: message.event.error.type,
353
+ message: message.event.error.message,
354
+ });
355
+ }
356
+ catch {
357
+ this.#logger.warn({ requestId: message.id }, "Hopper bridge diagnostic consumer rejected an event");
358
+ }
359
+ }
336
360
  #detachSocket(socket) {
337
361
  socket.off("data", this.#onSocketData);
338
362
  socket.off("error", this.#onSocketError);
@@ -3,7 +3,7 @@ import { accessSync, constants } from "node:fs";
3
3
  import { createAnalysisExecution } from "../application/AnalysisProvider.js";
4
4
  import { AnalysisCapabilityUnavailableError } from "../domain/errors.js";
5
5
  import { err } from "../domain/result.js";
6
- import { ENHANCED_TOOL_CONTRACTS, OFFICIAL_TOOL_CONTRACTS, } from "../contracts/toolContracts.js";
6
+ import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
7
7
  import { HopperApplicationLauncher } from "./BridgeLauncher.js";
8
8
  import { hopperLoaderArgsForTarget, resolveHopperAnalysisProfile, } from "./HopperAnalysisProfile.js";
9
9
  import { HopperClient } from "./HopperClient.js";
@@ -24,36 +24,47 @@ const MUTATING_OPERATIONS = new Set([
24
24
  ]);
25
25
  /** Tool contracts implemented directly by the Hopper adapter. */
26
26
  export const HOPPER_PROVIDER_TOOL_CONTRACTS = Object.freeze([
27
- ...OFFICIAL_TOOL_CONTRACTS,
28
- ...ENHANCED_TOOL_CONTRACTS.filter((contract) => contract.name === "analyze_function"),
27
+ ...GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "official-proxy"),
28
+ ...GENERATED_MCP_TOOL_CATALOG.filter(({ name }) => name === "analyze_function"),
29
29
  ]);
30
- const CAPABILITIES = Object.freeze(HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract) => Object.freeze({
31
- provider: IDENTITY,
32
- operation: contract.name,
33
- inputContractVersion: 1,
34
- outputContractVersion: 1,
35
- available: true,
36
- reason: null,
37
- pagination: "offset" in contract.inputSchema.shape ? "offset" : "none",
38
- exhaustive: false,
39
- effects: Object.freeze({
40
- mutatesArtifact: MUTATING_OPERATIONS.has(contract.name),
41
- launchesProcess: true,
42
- mayShowUi: true,
43
- mayAccessNetwork: false,
44
- mayWriteFilesystem: MUTATING_OPERATIONS.has(contract.name),
45
- changesPermissions: false,
46
- requiresRoot: false,
47
- }),
48
- limits: Object.freeze({
49
- maxResults: null,
50
- maxPayloadBytes: contract.name === "read_bytes" ? 4_096 : null,
51
- timeoutMs: null,
52
- }),
53
- limitations: Object.freeze([
54
- "Results depend on Hopper's completed static analysis.",
55
- ]),
56
- })));
30
+ const CAPABILITIES = Object.freeze(HOPPER_PROVIDER_TOOL_CONTRACTS.map((contract) => {
31
+ const operation = contract.analysisOperation;
32
+ if (operation === null)
33
+ throw new TypeError(`Missing analysis operation for ${contract.name}`);
34
+ return Object.freeze({
35
+ provider: IDENTITY,
36
+ operation,
37
+ inputContractVersion: 1,
38
+ outputContractVersion: 1,
39
+ available: true,
40
+ reason: null,
41
+ pagination: isRecord(contract.inputSchema.properties) &&
42
+ "offset" in contract.inputSchema.properties
43
+ ? "offset"
44
+ : "none",
45
+ exhaustive: false,
46
+ effects: Object.freeze({
47
+ mutatesArtifact: MUTATING_OPERATIONS.has(contract.name),
48
+ launchesProcess: true,
49
+ mayShowUi: true,
50
+ mayAccessNetwork: false,
51
+ mayWriteFilesystem: MUTATING_OPERATIONS.has(contract.name),
52
+ changesPermissions: false,
53
+ requiresRoot: false,
54
+ }),
55
+ limits: Object.freeze({
56
+ maxResults: null,
57
+ maxPayloadBytes: contract.name === "read_bytes" ? 4_096 : null,
58
+ timeoutMs: null,
59
+ }),
60
+ limitations: Object.freeze([
61
+ "Results depend on Hopper's completed static analysis.",
62
+ ]),
63
+ });
64
+ }));
65
+ function isRecord(value) {
66
+ return typeof value === "object" && value !== null && !Array.isArray(value);
67
+ }
57
68
  /** Concrete analysis provider backed by REA's private Hopper bridge. */
58
69
  export class HopperProvider {
59
70
  config;
@@ -178,6 +189,7 @@ export class HopperProvider {
178
189
  }),
179
190
  ...(context === undefined ? {} : { runId: context.runId }),
180
191
  logger: this.logger.child({ layer: "bridge" }),
192
+ onDiagnostic: (diagnostic) => this.logger.info(diagnostic, "Hopper bridge reported a diagnostic"),
181
193
  });
182
194
  return {
183
195
  execute: async (operation, parameters, options) => {
@@ -59,6 +59,28 @@ export class HopperRequestQueue {
59
59
  this.#drain();
60
60
  return true;
61
61
  }
62
+ /** Forward one correlated bridge event without releasing the wire request. */
63
+ acceptEvent(event) {
64
+ const entry = this.#active;
65
+ if (entry === undefined || entry.id !== event.id)
66
+ return false;
67
+ if (event.event.type === "progress" && !entry.callerSettled) {
68
+ if (event.event.terminal === true && entry.heartbeat !== undefined) {
69
+ clearInterval(entry.heartbeat);
70
+ entry.heartbeat = undefined;
71
+ }
72
+ this.#reportUpdate(entry, {
73
+ phase: event.event.phase,
74
+ completed: event.event.completed,
75
+ total: event.event.total,
76
+ message: event.event.message,
77
+ ...(event.event.terminal === undefined
78
+ ? {}
79
+ : { terminal: event.event.terminal }),
80
+ });
81
+ }
82
+ return true;
83
+ }
62
84
  /** Fail the active wire request and every request still waiting in the FIFO. */
63
85
  failAll(error) {
64
86
  const active = this.#active;
@@ -175,13 +197,19 @@ export class HopperRequestQueue {
175
197
  entry.signal.removeEventListener("abort", entry.onAbort);
176
198
  }
177
199
  #report(entry, message) {
178
- void entry.progress
179
- ?.report({
200
+ this.#reportUpdate(entry, {
180
201
  phase: "hopper_request",
181
202
  completed: 0,
182
203
  total: 1,
183
204
  message,
184
- })
185
- .catch(() => undefined);
205
+ });
206
+ }
207
+ #reportUpdate(entry, update) {
208
+ try {
209
+ void Promise.resolve(entry.progress?.report(update)).catch(() => undefined);
210
+ }
211
+ catch {
212
+ // Progress observation cannot change the request outcome.
213
+ }
186
214
  }
187
215
  }
@@ -1,4 +1,4 @@
1
- import { parseResponseLine } from "./protocol.js";
1
+ import { parseBridgeMessageLine, } from "./protocol.js";
2
2
  const MAX_LINE_BYTES = 10 * 1024 * 1024;
3
3
  /** Incrementally validates bounded Hopper NDJSON response frames. */
4
4
  export class HopperResponseStream {
@@ -29,7 +29,7 @@ export class HopperResponseStream {
29
29
  this.#buffer = "";
30
30
  }
31
31
  #acceptLine(line) {
32
- const parsed = parseResponseLine(line);
32
+ const parsed = parseBridgeMessageLine(line);
33
33
  if (!parsed.ok) {
34
34
  this.options.abort(parsed.error.message, parsed.error);
35
35
  return false;
@@ -2,30 +2,72 @@ import { z } from "zod";
2
2
  import { HopperProtocolError, HopperRemoteError } from "../domain/errors.js";
3
3
  import { jsonValueSchema } from "../domain/jsonValue.js";
4
4
  import { err, ok } from "../domain/result.js";
5
+ const remoteErrorSchema = z
6
+ .object({
7
+ code: z.number().int(),
8
+ message: z.string().max(512),
9
+ type: z
10
+ .enum([
11
+ "remote",
12
+ "authorization",
13
+ "invalid_request",
14
+ "capability_unavailable",
15
+ "bridge_exception",
16
+ ])
17
+ .default("remote"),
18
+ })
19
+ .strict();
5
20
  const responseSchema = z.union([
6
- z.object({
21
+ z
22
+ .object({
7
23
  id: z.number().int().nonnegative(),
8
24
  result: jsonValueSchema,
9
- }),
10
- z.object({
25
+ })
26
+ .strict(),
27
+ z
28
+ .object({
11
29
  id: z.number().int().nonnegative(),
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
- "capability_unavailable",
21
- "bridge_exception",
22
- ])
23
- .default("remote"),
24
- }),
25
- }),
30
+ error: remoteErrorSchema,
31
+ })
32
+ .strict(),
26
33
  ]);
27
- /** Parse one complete Hopper NDJSON response line. */
28
- export const parseResponseLine = (line) => {
34
+ const eventSchema = z
35
+ .object({
36
+ id: z.number().int().nonnegative(),
37
+ event: z.discriminatedUnion("type", [
38
+ z
39
+ .object({
40
+ type: z.literal("progress"),
41
+ phase: z.string().min(1).max(64),
42
+ completed: z.number().nonnegative().finite(),
43
+ total: z.number().nonnegative().finite().nullable(),
44
+ message: z.string().min(1).max(512),
45
+ terminal: z.boolean().optional(),
46
+ })
47
+ .strict(),
48
+ z
49
+ .object({
50
+ type: z.literal("diagnostic"),
51
+ error: remoteErrorSchema,
52
+ })
53
+ .strict(),
54
+ ]),
55
+ })
56
+ .strict()
57
+ .superRefine((message, context) => {
58
+ if (message.event.type === "progress" &&
59
+ message.event.total !== null &&
60
+ message.event.completed > message.event.total) {
61
+ context.addIssue({
62
+ code: "custom",
63
+ message: "Progress total cannot be less than completed",
64
+ path: ["event", "total"],
65
+ });
66
+ }
67
+ });
68
+ const messageSchema = z.union([responseSchema, eventSchema]);
69
+ /** Parse one complete Hopper NDJSON response or event line. */
70
+ export const parseBridgeMessageLine = (line) => {
29
71
  let decoded;
30
72
  try {
31
73
  decoded = JSON.parse(line);
@@ -33,10 +75,10 @@ export const parseResponseLine = (line) => {
33
75
  catch (cause) {
34
76
  return err(new HopperProtocolError("Hopper returned malformed JSON", { cause }));
35
77
  }
36
- const parsed = responseSchema.safeParse(decoded);
78
+ const parsed = messageSchema.safeParse(decoded);
37
79
  return parsed.success
38
80
  ? ok(parsed.data)
39
- : err(new HopperProtocolError("Hopper returned a response outside the bridge contract", { cause: parsed.error }));
81
+ : err(new HopperProtocolError("Hopper returned a message outside the bridge contract", { cause: parsed.error }));
40
82
  };
41
83
  /** Project a parsed response into its result or expected remote failure. */
42
84
  export const responseResult = (response) => "error" in response
@@ -1,4 +1,4 @@
1
- import { createServer } from "../server/createServer.js";
1
+ import { createServer, } from "../server/createServer.js";
2
2
  import { MCP_CONNECTION_LOST, MCP_CONNECTION_START_FAILED, } from "./messages.js";
3
3
  export const startMcpTransport = async (dependencies, session, serverContext) => {
4
4
  const liveServers = new Set();
@@ -13,10 +13,7 @@ export const startMcpTransport = async (dependencies, session, serverContext) =>
13
13
  investigationInputRoots: serverContext.runtimeState.investigationRoots,
14
14
  analysisSnapshotFilePolicy: serverContext.runtimeState.snapshotPolicy,
15
15
  permissionAuthority: serverContext.permissionAuthority,
16
- browserObservation: serverContext.browserObservation,
17
- browserScenarioCapture: serverContext.browserScenarioCapture,
18
- electronObservation: serverContext.electronObservation,
19
- javascriptRuntimeObservation: serverContext.javascriptRuntimeObservation,
16
+ loadOptionalProviders: serverContext.loadOptionalProviders,
20
17
  artifactIntegrityContinueEnabled: () => serverContext.runtimeState.currentConfig
21
18
  .artifactIntegrityContinueEnabled,
22
19
  javascriptReplayPolicy: serverContext.runtimeState.javascriptReplayPolicy,
package/dist/main.js CHANGED
@@ -7,10 +7,6 @@ import { createBinarySession } from "./application/runtime.js";
7
7
  import { createLogger } from "./logger.js";
8
8
  import { projectAnalysisError } from "./domain/errors.js";
9
9
  import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
10
- import { CdpBrowserProvider } from "./browser/CdpBrowserProvider.js";
11
- import { CdpElectronProvider } from "./browser/CdpElectronProvider.js";
12
- import { V8InspectorProvider } from "./browser/V8InspectorProvider.js";
13
- import { PlaywrightBrowserScenarioProvider } from "./browser/PlaywrightBrowserScenarioProvider.js";
14
10
  import { SERVER_START_FAILED } from "./main/messages.js";
15
11
  import { createRuntimeState } from "./main/state.js";
16
12
  import { openInitialTarget } from "./main/startup.js";
@@ -60,10 +56,6 @@ export const run = async (dependencies = runtimeDependencies()) => {
60
56
  return 1;
61
57
  }
62
58
  const session = createBinarySession(config.value, logger);
63
- const browserObservation = new CdpBrowserProvider();
64
- const browserScenarioCapture = new PlaywrightBrowserScenarioProvider();
65
- const electronObservation = new CdpElectronProvider();
66
- const javascriptRuntimeObservation = new V8InspectorProvider();
67
59
  const opened = await openInitialTarget(session, config.value, serverLogger, dependencies.writeStderr);
68
60
  if (!opened.ok)
69
61
  return opened.exitCode;
@@ -71,10 +63,20 @@ export const run = async (dependencies = runtimeDependencies()) => {
71
63
  const transport = await startMcpTransport(dependencies, session, {
72
64
  logger,
73
65
  serverLogger,
74
- browserObservation,
75
- browserScenarioCapture,
76
- electronObservation,
77
- javascriptRuntimeObservation,
66
+ loadOptionalProviders: async () => {
67
+ const [{ CdpBrowserProvider }, { PlaywrightBrowserScenarioProvider }, { CdpElectronProvider }, { V8InspectorProvider },] = await Promise.all([
68
+ import("./browser/CdpBrowserProvider.js"),
69
+ import("./browser/PlaywrightBrowserScenarioProvider.js"),
70
+ import("./browser/CdpElectronProvider.js"),
71
+ import("./browser/V8InspectorProvider.js"),
72
+ ]);
73
+ return {
74
+ browserObservation: new CdpBrowserProvider(),
75
+ browserScenarioCapture: new PlaywrightBrowserScenarioProvider(),
76
+ electronObservation: new CdpElectronProvider(),
77
+ javascriptRuntimeObservation: new V8InspectorProvider(),
78
+ };
79
+ },
78
80
  permissionAuthority: permissionAuthority.value,
79
81
  runtimeState,
80
82
  });
package/dist/mcpDoctor.js CHANGED
@@ -5,12 +5,12 @@ import { resolve } from "node:path";
5
5
  import { CATALOG_IDENTITY, MCP_RESOURCE_CATALOG, MCP_RESOURCE_TEMPLATE_CATALOG, } from "./catalogIdentity.js";
6
6
  import { PROMPT_CONTRACTS } from "./contracts/promptContracts.js";
7
7
  import { PRODUCT_IDENTITY } from "./identity.js";
8
- const DEFAULT_DEADLINE_MS = 20_000;
8
+ import { MCP_STARTUP_POLICY } from "./mcpStartupPolicy.js";
9
9
  const STDERR_LIMIT_BYTES = 64 * 1_024;
10
10
  const OUTPUT_FORMATS = ["toon", "json", "yaml", "md", "jsonl"];
11
11
  /** Diagnose the actual production stdio adapter with one bounded SDK session. */
12
12
  export const runProductionMcpDoctor = async (options) => {
13
- const deadline = Date.now() + (options.deadlineMs ?? DEFAULT_DEADLINE_MS);
13
+ const deadline = Date.now() + (options.deadlineMs ?? MCP_STARTUP_POLICY.doctorDeadlineMs);
14
14
  const controller = new AbortController();
15
15
  const timer = setTimeout(() => controller.abort(new Error("Production MCP doctor deadline expired")), Math.max(1, deadline - Date.now()));
16
16
  const stderr = boundedStderr();
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Supported MCP startup deadlines and regression budgets.
3
+ *
4
+ * The Codex timeout is caller-facing configuration. The smaller phase budgets
5
+ * preserve operational headroom, while the doctor deadline covers additional
6
+ * inventory requests after initialization.
7
+ */
8
+ export const MCP_STARTUP_POLICY = {
9
+ codexStartupTimeoutSeconds: 30,
10
+ initializeBudgetMs: 15_000,
11
+ toolsListBudgetMs: 5_000,
12
+ firstCatalogBudgetMs: 20_000,
13
+ doctorDeadlineMs: 30_000,
14
+ };