rea-agents 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +55 -12
  2. package/bridge/hopper_bridge.py +12 -3
  3. package/dist/application/AnalysisSnapshotCache.js +8 -6
  4. package/dist/application/ArtifactGraphConstruction.js +12 -3
  5. package/dist/application/ArtifactInventory.js +100 -27
  6. package/dist/application/AuthorizedArtifactInventory.js +4 -3
  7. package/dist/application/BinarySession.js +92 -4
  8. package/dist/application/BoundedJsonFiles.js +6 -29
  9. package/dist/application/BrowserEvidence.js +35 -0
  10. package/dist/application/BrowserObservationPort.js +1 -0
  11. package/dist/application/BrowserObservationService.js +52 -0
  12. package/dist/application/CapabilityInventory.js +134 -0
  13. package/dist/application/ClientRegistrationStatus.js +69 -0
  14. package/dist/application/ConfiguredRoots.js +19 -0
  15. package/dist/application/CrossVersionInventory.js +19 -8
  16. package/dist/application/CrossVersionInvestigation.js +74 -5
  17. package/dist/application/DeferredFileAuthorization.js +61 -0
  18. package/dist/application/DirectAnalysis.js +117 -9
  19. package/dist/application/Doctor.js +74 -2
  20. package/dist/application/EvidenceLedger.js +6 -5
  21. package/dist/application/InvestigationWorkspaceStore.js +2 -26
  22. package/dist/application/PermissionAuthority.js +183 -0
  23. package/dist/application/PermissionConfiguration.js +26 -0
  24. package/dist/application/ProcessCaptureAuthority.js +3 -2
  25. package/dist/application/ProcessCaptureError.js +13 -0
  26. package/dist/application/ProcessCaptureLifecycle.js +21 -9
  27. package/dist/application/ProcessCheckpoints.js +1 -1
  28. package/dist/application/ProcessCli.js +40 -15
  29. package/dist/application/ProcessHarness.js +10 -20
  30. package/dist/application/ProcessNormalization.js +17 -2
  31. package/dist/application/ProcessOwnership.js +4 -3
  32. package/dist/application/ProcessSampling.js +52 -17
  33. package/dist/application/ProgressReporter.js +30 -0
  34. package/dist/application/ProjectPermissionStore.js +213 -0
  35. package/dist/application/ReferenceSourceImportPolicy.js +3 -2
  36. package/dist/application/Setup.js +2 -41
  37. package/dist/application/SupportedClients.js +41 -0
  38. package/dist/application/TerminalRenderer.js +1 -1
  39. package/dist/application/Uninstall.js +1 -1
  40. package/dist/application/Upgrade.js +2 -0
  41. package/dist/application/runtime.js +1 -1
  42. package/dist/artifacts/ArtifactProvider.js +13 -3
  43. package/dist/artifacts/MachOSliceArtifactReader.js +1 -1
  44. package/dist/artifacts/NativeDmgArtifactReader.js +1 -1
  45. package/dist/artifacts/SafeOutputTree.js +3 -3
  46. package/dist/browser/CdpBrowserProvider.js +158 -0
  47. package/dist/browser/CdpCaptureDocuments.js +140 -0
  48. package/dist/browser/CdpCaptureEvents.js +248 -0
  49. package/dist/browser/CdpCaptureStorage.js +53 -0
  50. package/dist/browser/CdpCaptureValues.js +70 -0
  51. package/dist/browser/CdpConnection.js +218 -0
  52. package/dist/browser/CdpEndpoint.js +114 -0
  53. package/dist/browser/CdpOptionalCommand.js +15 -0
  54. package/dist/browser/CdpPageCapture.js +273 -0
  55. package/dist/catalogIdentity.js +123 -0
  56. package/dist/cli.js +48 -3
  57. package/dist/cliBrowserCommands.js +136 -0
  58. package/dist/cliEvidenceCommands.js +42 -1
  59. package/dist/cliInvestigationCommands.js +42 -6
  60. package/dist/cliLogging.js +20 -3
  61. package/dist/cliPolicyCommands.js +118 -0
  62. package/dist/cliProcessCommands.js +4 -14
  63. package/dist/config.js +107 -3
  64. package/dist/contracts/artifactToolContracts.js +14 -1
  65. package/dist/contracts/browserToolContracts.js +80 -0
  66. package/dist/contracts/errorSchemas.js +100 -0
  67. package/dist/contracts/toolContracts.js +13 -6
  68. package/dist/contracts/toolOutputSchemas.js +62 -30
  69. package/dist/domain/artifactComparison.js +38 -11
  70. package/dist/domain/artifactGraph.js +25 -1
  71. package/dist/domain/artifactInventoryEvidence.js +18 -0
  72. package/dist/domain/browserObservation.js +361 -0
  73. package/dist/domain/changedBehavior.js +9 -18
  74. package/dist/domain/errors.js +257 -3
  75. package/dist/domain/investigationWorkspace.js +131 -4
  76. package/dist/domain/jsonLimits.js +26 -0
  77. package/dist/domain/permissionPolicy.js +161 -0
  78. package/dist/domain/processCapture.js +3 -1
  79. package/dist/domain/processComparison.js +23 -34
  80. package/dist/domain/reconstructionVerification.js +3 -16
  81. package/dist/domain/reconstructionVerificationSchemas.js +3 -0
  82. package/dist/domain/staticRuntimeCorrelation.js +7 -1
  83. package/dist/domain/symbolAnalysis.js +1 -1
  84. package/dist/generatedPackageMetadata.js +9 -0
  85. package/dist/hopper/BridgeLauncher.js +7 -3
  86. package/dist/hopper/HopperClient.js +32 -5
  87. package/dist/identity.js +10 -3
  88. package/dist/logger.js +2 -2
  89. package/dist/main.js +101 -7
  90. package/dist/native/CommandRunner.js +11 -4
  91. package/dist/server/createServer.js +59 -5
  92. package/dist/server/mcpProgress.js +23 -0
  93. package/dist/server/registerArtifactComparisonTool.js +6 -2
  94. package/dist/server/registerBrowserTools.js +36 -0
  95. package/dist/server/registerBundleComparisonTool.js +6 -2
  96. package/dist/server/registerEnhancedTools.js +18 -1
  97. package/dist/server/registerEvidenceResources.js +302 -0
  98. package/dist/server/registerEvidenceTools.js +58 -1
  99. package/dist/server/registerFunctionComparisonTool.js +6 -2
  100. package/dist/server/registerInvestigationTools.js +78 -10
  101. package/dist/server/registerOfficialTools.js +19 -1
  102. package/dist/server/registerProcessComparisonTool.js +9 -3
  103. package/dist/server/registerSessionStatusTool.js +44 -3
  104. package/dist/server/registerSessionTools.js +144 -9
  105. package/dist/server/runDerivedOperation.js +43 -0
  106. package/dist/server/toolResult.js +27 -3
  107. package/dist/serverIdentity.js +70 -0
  108. package/package.json +7 -4
  109. package/scripts/rea.mjs +1 -1
  110. package/skills/rea-analysis/SKILL.md +18 -3
@@ -2,8 +2,8 @@ import { compareArtifacts } from "../domain/artifactComparison.js";
2
2
  import { findChangedBehavior } from "../domain/changedBehavior.js";
3
3
  import { createEvidence } from "../domain/evidence.js";
4
4
  import { createEvidenceBundle, } from "../domain/evidenceBundle.js";
5
- import { AnalysisInputError, EvidenceIntegrityError, InvestigationWorkspaceError, } from "../domain/errors.js";
6
- import { createInvestigationRunIdentity, createInvestigationWorkspace, crossVersionInvestigationInputSchema, investigationRunSchema, investigationRunSummarySchema, reviseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
5
+ import { AnalysisCancelledError, AnalysisInputError, EvidenceIntegrityError, InvestigationWorkspaceError, } from "../domain/errors.js";
6
+ import { createLegacyInvestigationRunIdentity, createInvestigationRunIdentity, createInvestigationWorkspace, crossVersionInvestigationInputSchema, investigationRunSchema, investigationRunSummarySchema, reviseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
7
7
  import { jsonValueSchema } from "../domain/jsonValue.js";
8
8
  import { err, ok } from "../domain/result.js";
9
9
  import { AUTOMATIC_RUN_LIMITATION, createInventoryEvidencePages, runLimitations, scanVersions, targetFor, } from "./CrossVersionInventory.js";
@@ -22,15 +22,32 @@ export const runCrossVersionInvestigation = async (input, policy, execution) =>
22
22
  const preflight = validateWorkspaceRequest(initial.value, parsed.data);
23
23
  if (!preflight.ok)
24
24
  return preflight;
25
- const snapshots = await scanVersions(parsed.data, execution.inputRoots, execution.signal);
25
+ if (isCancelled(execution.signal))
26
+ return err(new AnalysisCancelledError("find_changed_behavior"));
27
+ await execution.progress?.report({
28
+ phase: "scan_versions",
29
+ completed: 0,
30
+ total: 4,
31
+ message: "Scanning both version inputs",
32
+ });
33
+ const snapshots = await scanVersions(parsed.data, execution.inputRoots, execution.signal, execution.integrityContinueEnabled);
26
34
  if (!snapshots.ok)
27
35
  return snapshots;
36
+ await execution.progress?.report({
37
+ phase: "scan_versions",
38
+ completed: 1,
39
+ total: 4,
40
+ message: "Version inputs scanned",
41
+ });
28
42
  return continueInvestigation({
29
43
  input: parsed.data,
30
44
  initial: initial.value,
31
45
  snapshots: snapshots.value,
32
46
  policy,
33
47
  session: execution.session,
48
+ signal: execution.signal,
49
+ progress: execution.progress,
50
+ authorizeWorkspaceWrite: execution.authorizeWorkspaceWrite,
34
51
  });
35
52
  };
36
53
  const continueInvestigation = async (context) => {
@@ -40,13 +57,34 @@ const continueInvestigation = async (context) => {
40
57
  left,
41
58
  right,
42
59
  options: context.input.options,
60
+ integrity_policy: context.input.integrity_policy,
61
+ integrity_continue_approved: context.input.integrity_continue_approved,
62
+ max_integrity_mismatches: context.input.max_integrity_mismatches,
63
+ });
64
+ const legacyIdentity = createLegacyInvestigationRunIdentity({
65
+ left,
66
+ right,
67
+ options: context.input.options,
43
68
  });
44
- const existing = context.initial?.runs.find(({ run_id: id }) => id === identity.runId);
69
+ const existing = context.initial?.runs.find((run) => run.run_id === identity.runId ||
70
+ (context.input.integrity_policy === "fail" &&
71
+ !context.input.integrity_continue_approved &&
72
+ context.input.max_integrity_mismatches === 10 &&
73
+ "legacy_request_identity" in run &&
74
+ run.max_integrity_mismatches === 10 &&
75
+ run.run_id === legacyIdentity.runId));
45
76
  if (context.initial !== null && existing?.status === "complete")
46
77
  return completeOutcome(context.initial, existing, true, context.session);
78
+ if (context.authorizeWorkspaceWrite !== undefined) {
79
+ const authorized = await context.authorizeWorkspaceWrite();
80
+ if (!authorized.ok)
81
+ return authorized;
82
+ }
47
83
  const pages = createInventoryEvidencePages(context.input, context.snapshots);
48
84
  if (!pages.ok)
49
85
  return pages;
86
+ if (isCancelled(context.signal))
87
+ return err(new AnalysisCancelledError("find_changed_behavior"));
50
88
  const inventoried = await ensureInventoryCheckpoint({
51
89
  input: context.input,
52
90
  current: context.initial,
@@ -57,11 +95,36 @@ const continueInvestigation = async (context) => {
57
95
  });
58
96
  if (!inventoried.ok)
59
97
  return inventoried;
98
+ await context.progress?.report({
99
+ phase: "inventory_versions",
100
+ completed: 2,
101
+ total: 4,
102
+ message: "Both artifact inventories checkpointed",
103
+ });
104
+ if (isCancelled(context.signal))
105
+ return err(new AnalysisCancelledError("find_changed_behavior"));
60
106
  const compared = await ensureComparisonCheckpoint(context.input, inventoried.value, context.policy);
61
107
  if (!compared.ok)
62
108
  return compared;
63
- return finalizeInvestigation(context.input, compared.value, context.policy, context.session);
109
+ await context.progress?.report({
110
+ phase: "compare_artifacts",
111
+ completed: 3,
112
+ total: 4,
113
+ message: "Artifact comparison checkpointed",
114
+ });
115
+ if (isCancelled(context.signal))
116
+ return err(new AnalysisCancelledError("find_changed_behavior"));
117
+ const finalized = await finalizeInvestigation(context.input, compared.value, context.policy, context.session);
118
+ await context.progress?.report({
119
+ phase: "changed_behavior",
120
+ completed: 4,
121
+ total: 4,
122
+ message: finalized.ok ? "Investigation completed" : "Investigation failed",
123
+ terminal: true,
124
+ });
125
+ return finalized;
64
126
  };
127
+ const isCancelled = (signal) => signal?.aborted === true;
65
128
  const ensureInventoryCheckpoint = async (input) => {
66
129
  if (input.existing !== undefined) {
67
130
  const consistent = validateInventoryCheckpoint(input.existing, input.pages);
@@ -77,6 +140,9 @@ const ensureInventoryCheckpoint = async (input) => {
77
140
  left,
78
141
  right,
79
142
  options: input.input.options,
143
+ integrity_policy: input.input.integrity_policy,
144
+ integrity_continue_approved: input.input.integrity_continue_approved,
145
+ max_integrity_mismatches: input.input.max_integrity_mismatches,
80
146
  });
81
147
  const run = investigationRunSchema.parse({
82
148
  schema_version: 1,
@@ -85,6 +151,9 @@ const ensureInventoryCheckpoint = async (input) => {
85
151
  left,
86
152
  right,
87
153
  options: input.input.options,
154
+ integrity_policy: input.input.integrity_policy,
155
+ integrity_continue_approved: input.input.integrity_continue_approved,
156
+ max_integrity_mismatches: input.input.max_integrity_mismatches,
88
157
  status: "running",
89
158
  completed_stages: ["inventory_left", "inventory_right"],
90
159
  left_inventory_evidence_ids: evidenceIds(input.pages.left),
@@ -0,0 +1,61 @@
1
+ import { AnalysisProtocolError, PermissionRequiredError, } from "../domain/errors.js";
2
+ import { err, ok } from "../domain/result.js";
3
+ import { PermissionPathError, } from "./PermissionAuthority.js";
4
+ /** Authorize one root-scoped operation and project policy path failures. */
5
+ export const authorizeRootPermission = async (authority, request) => {
6
+ const authorized = await authority.authorize({
7
+ capability: request.capability,
8
+ roots: request.roots,
9
+ executables: [],
10
+ environment_names: [],
11
+ network: "none",
12
+ mount: false,
13
+ operation_identity: `${request.operation}:${request.capability}:${request.roots.join(":")}`,
14
+ }, request.access);
15
+ return authorized.ok
16
+ ? ok(null)
17
+ : err(authorized.error instanceof PermissionRequiredError
18
+ ? authorized.error
19
+ : new AnalysisProtocolError(authorized.error.message, {
20
+ cause: authorized.error,
21
+ }));
22
+ };
23
+ /** Authorize an existing file read and defer write authority until requested. */
24
+ export const authorizeFileReadWithDeferredWrite = async (authority, input) => {
25
+ const read = await authority.authorize({
26
+ capability: input.readCapability,
27
+ roots: [input.path],
28
+ executables: [],
29
+ environment_names: [],
30
+ network: "none",
31
+ mount: false,
32
+ operation_identity: `${input.operation}:${input.readCapability}:${input.path}`,
33
+ }, "read");
34
+ let writeAuthorization;
35
+ if (!read.ok) {
36
+ if (!(read.error instanceof PermissionPathError) ||
37
+ read.error.reason !== "not_found")
38
+ return err(read.error instanceof PermissionRequiredError
39
+ ? read.error
40
+ : new AnalysisProtocolError(read.error.message, {
41
+ cause: read.error,
42
+ }));
43
+ writeAuthorization = authorizeRootPermission(authority, {
44
+ capability: input.writeCapability,
45
+ roots: [input.path],
46
+ access: "write",
47
+ operation: input.operation,
48
+ });
49
+ const authorized = await writeAuthorization;
50
+ if (!authorized.ok)
51
+ return authorized;
52
+ }
53
+ return ok({
54
+ authorizeWrite: () => (writeAuthorization ??= authorizeRootPermission(authority, {
55
+ capability: input.writeCapability,
56
+ roots: [input.path],
57
+ access: "write",
58
+ operation: input.operation,
59
+ })),
60
+ });
61
+ };
@@ -1,14 +1,18 @@
1
1
  import { parseConfig } from "../config.js";
2
+ import { jsonObjectSchema } from "../domain/jsonValue.js";
3
+ import { createServerIdentity } from "../serverIdentity.js";
2
4
  import { EnhancedTools } from "./EnhancedTools.js";
3
5
  import { createBinarySession } from "./runtime.js";
4
6
  import { silentLogger } from "../logger.js";
5
7
  import { createEvidence } from "../domain/evidence.js";
6
- import { EvidenceIntegrityError, projectAnalysisError, } from "../domain/errors.js";
8
+ import { EvidenceIntegrityError, AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "../domain/errors.js";
7
9
  import { access } from "node:fs/promises";
8
10
  import { readAnalysisSnapshot, writeAnalysisSnapshot, } from "./AnalysisSnapshotFiles.js";
9
11
  import { parseBinaryTarget } from "../domain/binaryTarget.js";
10
12
  import { snapshotEvidenceForQuery, snapshotMatchesTarget, } from "../domain/analysisSnapshot.js";
11
13
  import { err, ok } from "../domain/result.js";
14
+ import { loadConfiguredPermissionAuthority } from "./PermissionConfiguration.js";
15
+ import { authorizeFileReadWithDeferredWrite, } from "./DeferredFileAuthorization.js";
12
16
  const WORKFLOW_PROVIDER = {
13
17
  id: "rea-workflow",
14
18
  name: "REA composed investigation workflow",
@@ -19,12 +23,20 @@ const WORKFLOW_PROVIDER = {
19
23
  * Unlike MCP mode, every CLI invocation is intentionally isolated and does not
20
24
  * retain a target or bridge resources for a subsequent command.
21
25
  */
22
- export const runDirectAnalysis = async (path, tool, arguments_, options = {}) => runAnalysis(path, tool, arguments_, {
26
+ export const runDirectAnalysis = async (path, tool, arguments_, options = {}) => withProcessCancellation(options.signal, (signal) => runAnalysis(path, tool, arguments_, {
23
27
  logger: options.logger ?? silentLogger,
24
28
  snapshotPath: options.snapshotPath,
25
- });
29
+ signal,
30
+ ...(options.permissionAuthority === undefined
31
+ ? {}
32
+ : { permissionAuthority: options.permissionAuthority }),
33
+ }));
26
34
  /** Execute one provider-native semantic operation with atomic provenance. */
27
- export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger) => runAnalysis(path, tool, arguments_, { logger, snapshotPath: undefined });
35
+ export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger, signal) => withProcessCancellation(signal, (operationSignal) => runAnalysis(path, tool, arguments_, {
36
+ logger,
37
+ snapshotPath: undefined,
38
+ signal: operationSignal,
39
+ }));
28
40
  /** Describe configured providers without opening a target or launching Hopper. */
29
41
  export const runSessionStatus = async (logger = silentLogger) => {
30
42
  const config = parseConfig(process.env);
@@ -32,17 +44,97 @@ export const runSessionStatus = async (logger = silentLogger) => {
32
44
  return cliError(config.error);
33
45
  const session = createBinarySession(config.value, logger);
34
46
  try {
35
- return session.status();
47
+ return {
48
+ ...jsonObjectSchema.parse(session.status()),
49
+ server_identity: createServerIdentity({
50
+ startedAt: new Date().toISOString(),
51
+ }),
52
+ };
36
53
  }
37
54
  finally {
38
55
  await session.close();
39
56
  }
40
57
  };
58
+ const authorizeAnalysis = async (authority, tool, arguments_) => {
59
+ const requests = [];
60
+ if (tool === "extract_artifact" && typeof arguments_.output_root === "string")
61
+ requests.push({
62
+ capability: "artifact_extract",
63
+ path: arguments_.output_root,
64
+ access: "write",
65
+ });
66
+ for (const request of requests) {
67
+ const result = await authority.authorize({
68
+ capability: request.capability,
69
+ roots: [request.path],
70
+ executables: [],
71
+ environment_names: [],
72
+ network: "none",
73
+ mount: false,
74
+ operation_identity: `${tool}:${request.capability}:${request.path}`,
75
+ }, request.access);
76
+ if (!result.ok)
77
+ return err(result.error instanceof PermissionRequiredError
78
+ ? result.error
79
+ : new AnalysisProtocolError(result.error.message, {
80
+ cause: result.error,
81
+ }));
82
+ }
83
+ if (tool === "inventory_artifact" &&
84
+ arguments_.native_mount_approved === true) {
85
+ const result = await authority.authorize({
86
+ capability: "native_mount",
87
+ roots: [],
88
+ executables: [],
89
+ environment_names: [],
90
+ network: "none",
91
+ mount: true,
92
+ operation_identity: "inventory_artifact:native_mount",
93
+ }, "read");
94
+ if (!result.ok)
95
+ return err(result.error instanceof PermissionRequiredError
96
+ ? result.error
97
+ : new AnalysisProtocolError(result.error.message, {
98
+ cause: result.error,
99
+ }));
100
+ }
101
+ return ok(null);
102
+ };
103
+ const permissionAuthorityFor = (config, supplied) => supplied === undefined
104
+ ? loadConfiguredPermissionAuthority(config)
105
+ : Promise.resolve(ok(supplied));
106
+ const authorizeSnapshotAccess = async (authority, tool, snapshotPath) => snapshotPath === undefined
107
+ ? ok(undefined)
108
+ : authorizeFileReadWithDeferredWrite(authority, {
109
+ path: snapshotPath,
110
+ readCapability: "snapshot_read",
111
+ writeCapability: "snapshot_write",
112
+ operation: tool,
113
+ });
114
+ const authorizeDeferredWrite = (authorization) => authorization?.authorizeWrite() ?? Promise.resolve(ok(null));
115
+ const authorizeAnalysisRun = async (input) => {
116
+ const authority = await permissionAuthorityFor(input.config, input.suppliedAuthority);
117
+ if (!authority.ok)
118
+ return authority;
119
+ const operation = await authorizeAnalysis(authority.value, input.tool, input.arguments);
120
+ if (!operation.ok)
121
+ return operation;
122
+ return authorizeSnapshotAccess(authority.value, input.tool, input.snapshotPath);
123
+ };
41
124
  const runAnalysis = async (path, tool, arguments_, options) => {
42
- const { logger, snapshotPath } = options;
125
+ const { logger, signal, snapshotPath } = options;
43
126
  const config = parseConfig(process.env);
44
127
  if (!config.ok)
45
128
  return cliError(config.error);
129
+ const authorization = await authorizeAnalysisRun({
130
+ config: config.value,
131
+ suppliedAuthority: options.permissionAuthority,
132
+ tool,
133
+ arguments: arguments_,
134
+ snapshotPath,
135
+ });
136
+ if (!authorization.ok)
137
+ return cliError(authorization.error);
46
138
  const unavailable = snapshotUnavailable(snapshotPath, config.value.hopperLoaderArgs);
47
139
  if (unavailable !== undefined)
48
140
  return cliError(unavailable);
@@ -60,8 +152,11 @@ const runAnalysis = async (path, tool, arguments_, options) => {
60
152
  return cliError(prepared.error);
61
153
  if (prepared.value.evidence !== undefined)
62
154
  return prepared.value.evidence;
155
+ const writable = await authorizeDeferredWrite(authorization.value);
156
+ if (!writable.ok)
157
+ return cliError(writable.error);
63
158
  const { snapshot } = prepared.value;
64
- const opened = await session.open(path, snapshot === undefined ? {} : { snapshot });
159
+ const opened = await session.open(path, snapshot === undefined ? { signal } : { signal, snapshot });
65
160
  if (!opened.ok)
66
161
  return cliError(opened.error);
67
162
  let output;
@@ -69,7 +164,7 @@ const runAnalysis = async (path, tool, arguments_, options) => {
69
164
  if (tool === "binary_overview" ||
70
165
  tool === "analyze_function" ||
71
166
  tool === "trace_feature") {
72
- const result = await new EnhancedTools(session).execute(tool, arguments_);
167
+ const result = await new EnhancedTools(session).execute(tool, arguments_, signal);
73
168
  if (!result.ok)
74
169
  output = cliError(result.error);
75
170
  else {
@@ -86,7 +181,7 @@ const runAnalysis = async (path, tool, arguments_, options) => {
86
181
  }
87
182
  }
88
183
  else {
89
- const result = await session.execute(tool, arguments_);
184
+ const result = await session.execute(tool, arguments_, { signal });
90
185
  if (!result.ok)
91
186
  output = cliError(result.error);
92
187
  else {
@@ -117,6 +212,19 @@ const runAnalysis = async (path, tool, arguments_, options) => {
117
212
  await session.close();
118
213
  }
119
214
  };
215
+ const withProcessCancellation = async (suppliedSignal, operation) => {
216
+ if (suppliedSignal !== undefined)
217
+ return operation(suppliedSignal);
218
+ const controller = new AbortController();
219
+ const cancel = () => controller.abort();
220
+ process.once("SIGINT", cancel);
221
+ try {
222
+ return await operation(controller.signal);
223
+ }
224
+ finally {
225
+ process.off("SIGINT", cancel);
226
+ }
227
+ };
120
228
  const prepareSnapshot = async (options) => {
121
229
  const { path, tool, arguments: arguments_, snapshotPath, policy, provider, } = options;
122
230
  if (snapshotPath === undefined || !(await fileExists(snapshotPath)))
@@ -1,6 +1,5 @@
1
1
  import { constants } from "node:fs";
2
- import { access } from "node:fs/promises";
3
- import { readdir } from "node:fs/promises";
2
+ import { access, readFile, readdir } from "node:fs/promises";
4
3
  import { homedir } from "node:os";
5
4
  import { join } from "node:path";
6
5
  import { execFile } from "node:child_process";
@@ -9,6 +8,9 @@ import { parseBinaryTarget } from "../domain/binaryTarget.js";
9
8
  import { supportsNodeVersion } from "../domain/runtimeVersion.js";
10
9
  import { probeHomebrew } from "./homebrew.js";
11
10
  import { linuxHopperBinarySupported, linuxHopperLauncherPath, linuxSharedLibrariesAvailable, readLinuxDistribution, } from "./LinuxHopper.js";
11
+ import { CATALOG_IDENTITY } from "../catalogIdentity.js";
12
+ import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js";
13
+ import { readClientRegistrationStatuses, } from "./ClientRegistrationStatus.js";
12
14
  const execFileAsync = promisify(execFile);
13
15
  const DEFAULT_HOPPER = "/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
14
16
  const SYSTEM_LINUX_HOPPER = "/opt/hopper/bin/Hopper";
@@ -68,10 +70,54 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
68
70
  remediation: "Supply a readable local app or program path.",
69
71
  classification: "config_drift",
70
72
  }));
73
+ const installationPaths = (await host.installationPaths?.()) ?? [];
74
+ const installedSkillVersion = await host.installedSkillVersion?.();
75
+ const registrations = (await host.clientRegistrations?.()) ?? [];
76
+ for (const registration of registrations)
77
+ if (registration.state !== "aligned")
78
+ checks.push({
79
+ name: `registration:${registration.client}`,
80
+ ok: false,
81
+ classification: "config_drift",
82
+ detail: registration.config_path,
83
+ remediation: registration.remediation ??
84
+ "Run rea setup, then restart the affected client.",
85
+ });
71
86
  return {
72
87
  healthy: checks.every(({ ok }) => ok),
73
88
  ...(hopperPath === undefined ? {} : { hopperPath }),
74
89
  checks,
90
+ identity: {
91
+ cli_package_version: PRODUCT_IDENTITY.packageVersion,
92
+ expected_skill_version: PRODUCT_IDENTITY.skillVersion,
93
+ sdk: SDK_IDENTITY,
94
+ catalog: CATALOG_IDENTITY,
95
+ live_server: {
96
+ state: "unknown",
97
+ remediation: "Compare rea://server/identity or binary_session from the active client; an on-disk registration cannot prove the running server version.",
98
+ },
99
+ installations: {
100
+ paths: installationPaths,
101
+ state: installationPaths.length === 0
102
+ ? "unknown"
103
+ : installationPaths.length === 1
104
+ ? "single"
105
+ : "multiple",
106
+ },
107
+ skill: {
108
+ installed_version: installedSkillVersion ?? null,
109
+ state: installedSkillVersion === undefined
110
+ ? "unknown"
111
+ : installedSkillVersion === PRODUCT_IDENTITY.skillVersion
112
+ ? "aligned"
113
+ : "stale",
114
+ remediation: installedSkillVersion === undefined ||
115
+ installedSkillVersion === PRODUCT_IDENTITY.skillVersion
116
+ ? null
117
+ : "Run rea setup to update the installed REA skill.",
118
+ },
119
+ registrations,
120
+ },
75
121
  };
76
122
  };
77
123
  const unsupportedHopperRemediation = (configured) => configured
@@ -157,8 +203,34 @@ export const systemDoctorHost = () => ({
157
203
  }
158
204
  return paths;
159
205
  },
206
+ async installationPaths() {
207
+ try {
208
+ const command = process.platform === "win32" ? "where" : "which";
209
+ const arguments_ = process.platform === "win32" ? ["rea"] : ["-a", "rea"];
210
+ return uniqueLines((await execFileAsync(command, arguments_)).stdout);
211
+ }
212
+ catch {
213
+ return [];
214
+ }
215
+ },
216
+ async installedSkillVersion() {
217
+ try {
218
+ const content = await readFile(join(homedir(), ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md"), "utf8");
219
+ return /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1];
220
+ }
221
+ catch {
222
+ return undefined;
223
+ }
224
+ },
225
+ clientRegistrations: () => readClientRegistrationStatuses(homedir()),
160
226
  });
161
227
  const parseMajor = (version) => Number.parseInt(version.split(".")[0] ?? "0", 10);
228
+ const uniqueLines = (value) => [
229
+ ...new Set(value
230
+ .split(/\r?\n/u)
231
+ .map((line) => line.trim())
232
+ .filter((line) => line.length > 0)),
233
+ ];
162
234
  const check = (name, ok, detail, failure) => ({
163
235
  name,
164
236
  ok,
@@ -85,7 +85,7 @@ export class EvidenceLedger {
85
85
  }
86
86
  /** Export records in deterministic, semantically irrelevant ID order. */
87
87
  export() {
88
- return createEvidenceBundle([...this.#records.values()], [...this.#unknownRevisions.values()]);
88
+ return structuredClone(createEvidenceBundle([...this.#records.values()], [...this.#unknownRevisions.values()]));
89
89
  }
90
90
  /** Check whether one immutable Evidence ID is present in this ledger. */
91
91
  has(evidenceId) {
@@ -162,7 +162,7 @@ export class EvidenceLedger {
162
162
  if (!checked.ok)
163
163
  return checked;
164
164
  this.#commit(pendingRecords, pendingUnknowns, checked.value);
165
- return ok(unknown);
165
+ return ok(structuredClone(unknown));
166
166
  }
167
167
  /** Apply one approved full-state update using optimistic revision matching. */
168
168
  updateUnknown(input, mutationEvidence) {
@@ -187,7 +187,8 @@ export class EvidenceLedger {
187
187
  (filters.severity === undefined ||
188
188
  unknown.severity === filters.severity) &&
189
189
  (filters.domain === undefined || unknown.domain === filters.domain))
190
- .sort((left, right) => left.unknown_id.localeCompare(right.unknown_id));
190
+ .sort((left, right) => left.unknown_id.localeCompare(right.unknown_id))
191
+ .map((unknown) => structuredClone(unknown));
191
192
  }
192
193
  /** Return current resolution validity; imported invalid states are rejected. */
193
194
  verifyUnknownResolution(unknownId) {
@@ -197,7 +198,7 @@ export class EvidenceLedger {
197
198
  return ok({
198
199
  valid: unknown.status === "resolved",
199
200
  truthVerified: unknown.resolution?.disposition === "verified",
200
- unknown,
201
+ unknown: structuredClone(unknown),
201
202
  });
202
203
  }
203
204
  /** Clear records when the owning session closes. */
@@ -225,7 +226,7 @@ export class EvidenceLedger {
225
226
  if (!checked.ok)
226
227
  return checked;
227
228
  this.#commit(pendingRecords, pendingUnknowns, checked.value);
228
- return ok(unknown);
229
+ return ok(structuredClone(unknown));
229
230
  }
230
231
  #validateCandidate(records, unknowns) {
231
232
  if (records.size + unknowns.size > this.limits.maxRecords)
@@ -1,6 +1,7 @@
1
1
  import { lstat, open, readFile, realpath, unlink, } from "node:fs/promises";
2
2
  import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
3
3
  import writeFileAtomic from "write-file-atomic";
4
+ import { isJsonWithinLimits } from "../domain/jsonLimits.js";
4
5
  import { InvestigationWorkspaceError } from "../domain/errors.js";
5
6
  import { parseInvestigationWorkspace, serializeInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
6
7
  import { err, ok } from "../domain/result.js";
@@ -97,7 +98,7 @@ const readWorkspaceFile = async (destination, policy) => {
97
98
  catch (cause) {
98
99
  return err(new InvestigationWorkspaceError("read", "invalid-json", { cause }));
99
100
  }
100
- if (!withinJsonLimits(decoded, policy))
101
+ if (!isJsonWithinLimits(decoded, policy))
101
102
  return err(new InvestigationWorkspaceError("read", "too-large"));
102
103
  try {
103
104
  return ok(parseInvestigationWorkspace(decoded));
@@ -182,31 +183,6 @@ const validateNextRevision = (current, next, expectedRevision) => {
182
183
  ? ok(null)
183
184
  : err(new InvestigationWorkspaceError("update", "revision-conflict"));
184
185
  };
185
- const withinJsonLimits = (root, policy) => {
186
- const pending = [
187
- { value: root, depth: 0 },
188
- ];
189
- let nodes = 0;
190
- while (pending.length > 0) {
191
- const current = pending.pop();
192
- if (current === undefined)
193
- break;
194
- nodes += 1;
195
- if (nodes > policy.maxNodes || current.depth > policy.maxDepth)
196
- return false;
197
- if (typeof current.value === "string" &&
198
- current.value.length > policy.maxStringLength)
199
- return false;
200
- if (typeof current.value !== "object" || current.value === null)
201
- continue;
202
- for (const [key, value] of Object.entries(current.value)) {
203
- if (key.length > policy.maxStringLength)
204
- return false;
205
- pending.push({ value, depth: current.depth + 1 });
206
- }
207
- }
208
- return true;
209
- };
210
186
  const isFileNotFound = (cause) => errorCode(cause) === "ENOENT";
211
187
  const isAlreadyExists = (cause) => errorCode(cause) === "EEXIST";
212
188
  const errorCode = (cause) => typeof cause === "object" && cause !== null && "code" in cause