rea-agents 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +21 -0
  2. package/bridge/hopper_bridge.py +12 -3
  3. package/dist/application/ArtifactGraphConstruction.js +1 -1
  4. package/dist/application/ArtifactInventory.js +98 -26
  5. package/dist/application/AuthorizedArtifactInventory.js +2 -2
  6. package/dist/application/BinarySession.js +83 -0
  7. package/dist/application/CapabilityInventory.js +127 -0
  8. package/dist/application/ClientRegistrationStatus.js +69 -0
  9. package/dist/application/CrossVersionInventory.js +19 -8
  10. package/dist/application/CrossVersionInvestigation.js +68 -5
  11. package/dist/application/DirectAnalysis.js +97 -9
  12. package/dist/application/Doctor.js +74 -2
  13. package/dist/application/PermissionAuthority.js +183 -0
  14. package/dist/application/PermissionConfiguration.js +26 -0
  15. package/dist/application/ProcessCaptureError.js +13 -0
  16. package/dist/application/ProcessCaptureLifecycle.js +3 -1
  17. package/dist/application/ProcessCli.js +23 -10
  18. package/dist/application/ProcessHarness.js +4 -1
  19. package/dist/application/ProgressReporter.js +30 -0
  20. package/dist/application/ProjectPermissionStore.js +110 -0
  21. package/dist/application/Setup.js +2 -41
  22. package/dist/application/SupportedClients.js +41 -0
  23. package/dist/application/Uninstall.js +1 -1
  24. package/dist/application/Upgrade.js +2 -0
  25. package/dist/application/runtime.js +1 -1
  26. package/dist/artifacts/ArtifactProvider.js +13 -3
  27. package/dist/catalogIdentity.js +121 -0
  28. package/dist/cli.js +46 -3
  29. package/dist/cliEvidenceCommands.js +42 -1
  30. package/dist/cliInvestigationCommands.js +57 -6
  31. package/dist/cliLogging.js +20 -3
  32. package/dist/cliPolicyCommands.js +115 -0
  33. package/dist/cliProcessCommands.js +4 -14
  34. package/dist/config.js +63 -3
  35. package/dist/contracts/artifactToolContracts.js +14 -1
  36. package/dist/contracts/errorSchemas.js +98 -0
  37. package/dist/contracts/toolContracts.js +10 -1
  38. package/dist/contracts/toolOutputSchemas.js +62 -30
  39. package/dist/domain/artifactComparison.js +38 -11
  40. package/dist/domain/artifactGraph.js +25 -1
  41. package/dist/domain/artifactInventoryEvidence.js +18 -0
  42. package/dist/domain/changedBehavior.js +5 -3
  43. package/dist/domain/errors.js +216 -3
  44. package/dist/domain/investigationWorkspace.js +131 -4
  45. package/dist/domain/permissionPolicy.js +157 -0
  46. package/dist/domain/processComparison.js +1 -0
  47. package/dist/domain/reconstructionVerification.js +1 -1
  48. package/dist/domain/reconstructionVerificationSchemas.js +1 -0
  49. package/dist/domain/staticRuntimeCorrelation.js +5 -1
  50. package/dist/generatedPackageMetadata.js +9 -0
  51. package/dist/hopper/BridgeLauncher.js +7 -3
  52. package/dist/hopper/HopperClient.js +32 -5
  53. package/dist/identity.js +10 -3
  54. package/dist/logger.js +2 -2
  55. package/dist/main.js +95 -7
  56. package/dist/server/createServer.js +52 -5
  57. package/dist/server/mcpProgress.js +23 -0
  58. package/dist/server/registerArtifactComparisonTool.js +6 -2
  59. package/dist/server/registerBundleComparisonTool.js +6 -2
  60. package/dist/server/registerEnhancedTools.js +18 -1
  61. package/dist/server/registerEvidenceResources.js +302 -0
  62. package/dist/server/registerEvidenceTools.js +58 -1
  63. package/dist/server/registerFunctionComparisonTool.js +6 -2
  64. package/dist/server/registerInvestigationTools.js +83 -10
  65. package/dist/server/registerOfficialTools.js +19 -1
  66. package/dist/server/registerProcessComparisonTool.js +7 -3
  67. package/dist/server/registerSessionStatusTool.js +44 -3
  68. package/dist/server/registerSessionTools.js +129 -7
  69. package/dist/server/runDerivedOperation.js +35 -0
  70. package/dist/server/toolResult.js +27 -3
  71. package/dist/serverIdentity.js +70 -0
  72. package/package.json +6 -4
@@ -0,0 +1,26 @@
1
+ import { ConfigurationError } from "../domain/errors.js";
2
+ import { err, ok } from "../domain/result.js";
3
+ import { createPermissionAuthority, } from "./PermissionAuthority.js";
4
+ import { readProjectPermissionStore } from "./ProjectPermissionStore.js";
5
+ /** Load the same administrator and optional project policy for CLI and MCP. */
6
+ export const loadConfiguredPermissionAuthority = async (config) => {
7
+ const authority = await createPermissionAuthority(config.permissionCeilings, config.administratorPermissionGrants);
8
+ if (!authority.ok)
9
+ return err(new ConfigurationError("Permission policy could not be loaded", {
10
+ cause: authority.error,
11
+ }));
12
+ if (config.permissionProjectRoot === undefined ||
13
+ config.permissionProjectStore === undefined)
14
+ return ok(authority.value);
15
+ const project = await readProjectPermissionStore(config.permissionProjectStore, config.permissionProjectRoot);
16
+ if (!project.ok)
17
+ return err(new ConfigurationError("Project permission policy could not be loaded", {
18
+ cause: project.error,
19
+ }));
20
+ const replaced = await authority.value.replaceProjectGrants(project.value?.grants ?? []);
21
+ return replaced.ok
22
+ ? ok(authority.value)
23
+ : err(new ConfigurationError("Project permission policy could not be applied", {
24
+ cause: replaced.error,
25
+ }));
26
+ };
@@ -4,14 +4,27 @@ export class ProcessCaptureError extends AnalysisError {
4
4
  _tag = "ProcessCaptureError";
5
5
  userMessage;
6
6
  userCategory;
7
+ reason;
8
+ cleanupIncomplete;
9
+ cleanupResources;
7
10
  constructor(message, options) {
8
11
  super(message, options);
9
12
  this.userMessage = options?.userMessage;
10
13
  this.userCategory = options?.userCategory;
14
+ this.reason =
15
+ options?.reason ??
16
+ (options?.userCategory === "permission_required"
17
+ ? "permission_required"
18
+ : options?.userCategory === "cancelled"
19
+ ? "cancelled"
20
+ : "capture_failed");
21
+ this.cleanupIncomplete = this.reason === "cleanup_incomplete";
22
+ this.cleanupResources = options?.cleanupResources ?? [];
11
23
  }
12
24
  }
13
25
  /** Caller-visible cancellation without exposing capture implementation state. */
14
26
  export const processCaptureCancelled = () => new ProcessCaptureError("process capture was cancelled", {
15
27
  userCategory: "cancelled",
28
+ reason: "cancelled",
16
29
  userMessage: "Process capture was cancelled. Start it again when ready.",
17
30
  });
@@ -249,7 +249,9 @@ export const resolveProcessResult = (capture, executionFailure, cleanupFailure)
249
249
  cause: executionFailure,
250
250
  });
251
251
  if (cleanupFailure !== undefined)
252
- throw new ProcessCaptureError(cleanupFailure);
252
+ throw new ProcessCaptureError(cleanupFailure, {
253
+ reason: "cleanup_incomplete",
254
+ });
253
255
  if (capture === undefined)
254
256
  throw new ProcessCaptureError("process capture produced no result");
255
257
  return capture;
@@ -1,21 +1,13 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { parseConfig } from "../config.js";
3
- import { AnalysisError, projectAnalysisError } from "../domain/errors.js";
3
+ import { AnalysisError, AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "../domain/errors.js";
4
4
  import { createEvidence, parseEvidence } from "../domain/evidence.js";
5
5
  import { jsonValueSchema } from "../domain/jsonValue.js";
6
6
  import { compareProcessCaptures, LEGACY_PROCESS_CAPTURE_MESSAGE, parseProcessScenario, parseProcessCapture, } from "../domain/processCapture.js";
7
7
  import { captureProcessScenario } from "./ProcessHarness.js";
8
8
  import { PROCESS_PROVIDER, createProcessCaptureEvidence, } from "./ProcessEvidence.js";
9
+ import { loadConfiguredPermissionAuthority } from "./PermissionConfiguration.js";
9
10
  const MAX_INPUT_BYTES = 64 * 1024 * 1024;
10
- /** Distinguish a safe process-command failure from successful Evidence. */
11
- export const isProcessCliErrorOutput = (value) => typeof value === "object" &&
12
- value !== null &&
13
- "error" in value &&
14
- value.error === "Process command failed" &&
15
- "category" in value &&
16
- typeof value.category === "string" &&
17
- "message" in value &&
18
- typeof value.message === "string";
19
11
  /** Capture one JSON scenario through the same policy and evidence contract as MCP. */
20
12
  export const captureProcessScenarioFile = async (path) => {
21
13
  try {
@@ -30,6 +22,27 @@ export const captureProcessScenarioFile = async (path) => {
30
22
  const config = parseConfig(process.env);
31
23
  if (!config.ok)
32
24
  return cliAnalysisError(config.error);
25
+ const authority = await loadConfiguredPermissionAuthority(config.value);
26
+ if (!authority.ok)
27
+ return cliAnalysisError(authority.error);
28
+ const authorized = await authority.value.authorize({
29
+ capability: "process_capture",
30
+ roots: [scenario.working_directory, ...scenario.filesystem_roots],
31
+ executables: [scenario.executable],
32
+ environment_names: [
33
+ ...Object.keys(scenario.environment),
34
+ ...scenario.inherit_environment,
35
+ ],
36
+ network: scenario.network_access === "host" ? "external" : "none",
37
+ mount: false,
38
+ operation_identity: `capture_process_scenario:${scenario.executable}`,
39
+ }, "read");
40
+ if (!authorized.ok)
41
+ return cliAnalysisError(authorized.error instanceof PermissionRequiredError
42
+ ? authorized.error
43
+ : new AnalysisProtocolError(authorized.error.message, {
44
+ cause: authorized.error,
45
+ }));
33
46
  const captured = await captureProcessScenario(scenario, config.value.processExecutionPolicy);
34
47
  if (!captured.ok)
35
48
  return cliAnalysisError(captured.error);
@@ -98,7 +98,10 @@ const cleanupFailedStartup = async (options) => {
98
98
  sampledProcessGroupIds: [],
99
99
  });
100
100
  if (cleanupFailure !== undefined)
101
- throw new ProcessCaptureError(cleanupFailure, { cause: options.cause });
101
+ throw new ProcessCaptureError(cleanupFailure, {
102
+ cause: options.cause,
103
+ reason: "cleanup_incomplete",
104
+ });
102
105
  throw options.cause;
103
106
  };
104
107
  const createTerminalRenderer = (scenario, temporaryRoot, terminalPid) => new TerminalRenderer({
@@ -0,0 +1,30 @@
1
+ /** Create a monotonic, rate-bounded reporter around a surface-specific sink. */
2
+ export const createProgressReporter = (sink, options = {}) => {
3
+ const minimumIntervalMs = options.minimumIntervalMs ?? 100;
4
+ const now = options.now ?? Date.now;
5
+ let lastCompleted = -1;
6
+ let lastSentAt = Number.NEGATIVE_INFINITY;
7
+ let sequence = 0;
8
+ return {
9
+ async report(update) {
10
+ if (!Number.isFinite(update.completed) || update.completed < 0)
11
+ throw new TypeError("Progress completed must be a nonnegative number");
12
+ if (update.total !== null && update.total < update.completed)
13
+ throw new TypeError("Progress total cannot be less than completed");
14
+ if (update.completed < lastCompleted)
15
+ throw new TypeError("Progress cannot move backwards");
16
+ lastCompleted = update.completed;
17
+ const observedAt = now();
18
+ if (update.terminal !== true &&
19
+ observedAt - lastSentAt < minimumIntervalMs)
20
+ return;
21
+ lastSentAt = observedAt;
22
+ sequence += 1;
23
+ await sink({ ...update, sequence });
24
+ },
25
+ };
26
+ };
27
+ /** Surface that deliberately ignores progress while preserving one workflow API. */
28
+ export const silentProgressReporter = {
29
+ report: () => Promise.resolve(),
30
+ };
@@ -0,0 +1,110 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readFile, realpath, stat } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import writeFileAtomic from "write-file-atomic";
5
+ import { z } from "zod";
6
+ import { err, ok } from "../domain/result.js";
7
+ const grantSchema = z.object({
8
+ grant_id: z.string().min(1),
9
+ capability: z.enum([
10
+ "process_capture",
11
+ "evidence_read",
12
+ "evidence_write",
13
+ "investigation_input",
14
+ "investigation_workspace_read",
15
+ "investigation_workspace_write",
16
+ "snapshot_read",
17
+ "snapshot_write",
18
+ "artifact_extract",
19
+ "native_mount",
20
+ "reference_read",
21
+ ]),
22
+ roots: z.array(z.string()),
23
+ executables: z.array(z.string()),
24
+ environment_names: z.array(z.string()),
25
+ network: z.enum(["none", "loopback", "external"]),
26
+ mount: z.boolean(),
27
+ lifetime: z.literal("project"),
28
+ operation_identity: z.string().nullable(),
29
+ expires_at: z.iso.datetime().nullable(),
30
+ });
31
+ const storeSchema = z.object({
32
+ schema_version: z.literal(1),
33
+ project_id: z.string().regex(/^project_[a-f0-9]{64}$/u),
34
+ project_root: z.string(),
35
+ grants: z.array(grantSchema).max(1_000),
36
+ });
37
+ /** Owner-only project policy persistence failure. */
38
+ export class ProjectPermissionStoreError extends Error {
39
+ reason;
40
+ _tag = "ProjectPermissionStoreError";
41
+ constructor(reason, options) {
42
+ super(`Project permission store failed: ${reason}`, options);
43
+ this.reason = reason;
44
+ }
45
+ }
46
+ /** Derive relocation-explicit project identity from its canonical root. */
47
+ const identifyPermissionProject = async (projectRoot) => {
48
+ try {
49
+ const root = await realpath(projectRoot);
50
+ return ok({
51
+ id: `project_${createHash("sha256").update(root).digest("hex")}`,
52
+ root,
53
+ });
54
+ }
55
+ catch (cause) {
56
+ return err(new ProjectPermissionStoreError("project_not_found", { cause }));
57
+ }
58
+ };
59
+ /** Read and validate an owner-only store bound to the requested project. */
60
+ export const readProjectPermissionStore = async (path, projectRoot) => {
61
+ const project = await identifyPermissionProject(projectRoot);
62
+ if (!project.ok)
63
+ return project;
64
+ try {
65
+ const metadata = await stat(path);
66
+ if ((metadata.mode & 0o077) !== 0)
67
+ return err(new ProjectPermissionStoreError("not_owner_only"));
68
+ const parsed = storeSchema.safeParse(JSON.parse(await readFile(path, "utf8")));
69
+ if (!parsed.success ||
70
+ parsed.data.project_id !== project.value.id ||
71
+ parsed.data.project_root !== project.value.root)
72
+ return err(new ProjectPermissionStoreError("invalid"));
73
+ return ok(parsed.data);
74
+ }
75
+ catch (cause) {
76
+ if (isNotFound(cause))
77
+ return ok(null);
78
+ return err(new ProjectPermissionStoreError("io", { cause }));
79
+ }
80
+ };
81
+ /** Atomically replace explicit project grants with owner-only permissions. */
82
+ export const writeProjectPermissionStore = async (path, projectRoot, grants) => {
83
+ const project = await identifyPermissionProject(projectRoot);
84
+ if (!project.ok)
85
+ return project;
86
+ const candidate = storeSchema.safeParse({
87
+ schema_version: 1,
88
+ project_id: project.value.id,
89
+ project_root: project.value.root,
90
+ grants,
91
+ });
92
+ if (!candidate.success)
93
+ return err(new ProjectPermissionStoreError("invalid", { cause: candidate.error }));
94
+ try {
95
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
96
+ await writeFileAtomic(path, `${JSON.stringify(candidate.data, null, 2)}\n`, {
97
+ encoding: "utf8",
98
+ mode: 0o600,
99
+ });
100
+ const metadata = await stat(path);
101
+ if ((metadata.mode & 0o077) !== 0) {
102
+ return err(new ProjectPermissionStoreError("not_owner_only"));
103
+ }
104
+ return ok(candidate.data);
105
+ }
106
+ catch (cause) {
107
+ return err(new ProjectPermissionStoreError("io", { cause }));
108
+ }
109
+ };
110
+ const isNotFound = (cause) => cause instanceof Error && "code" in cause && cause.code === "ENOENT";
@@ -1,6 +1,6 @@
1
1
  import { access, copyFile, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join, resolve } from "node:path";
3
+ import { dirname, resolve } from "node:path";
4
4
  import { z } from "zod";
5
5
  import writeFileAtomic from "write-file-atomic";
6
6
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
@@ -10,6 +10,7 @@ import { runDoctor, systemDoctorHost } from "./Doctor.js";
10
10
  import { installLinuxHopper, readLinuxDistribution, } from "./LinuxHopper.js";
11
11
  import { installMacHopper } from "./MacHopper.js";
12
12
  import { configureDetectedClients } from "./SetupClients.js";
13
+ import { supportedClients } from "./SupportedClients.js";
13
14
  import { installCanonicalSkill } from "./SetupSkill.js";
14
15
  import { setupPlan } from "./SetupPlan.js";
15
16
  import { setupInstallFailure, } from "./SetupInstallFailure.js";
@@ -160,46 +161,6 @@ export const detectClients = async (home) => {
160
161
  detected.push(candidate);
161
162
  return detected;
162
163
  };
163
- /** Describe every client location that setup or uninstall may own. */
164
- export const supportedClients = (home) => [
165
- {
166
- name: "claude_code",
167
- configPath: join(home, ".claude.json"),
168
- markerPath: join(home, ".claude"),
169
- },
170
- {
171
- name: "claude_desktop",
172
- configPath: join(home, "Library/Application Support/Claude/claude_desktop_config.json"),
173
- markerPath: join(home, "Library/Application Support/Claude"),
174
- },
175
- {
176
- name: "codex",
177
- configPath: join(home, ".codex/config.toml"),
178
- markerPath: join(home, ".codex"),
179
- format: "toml",
180
- },
181
- {
182
- name: "cursor",
183
- configPath: join(home, ".cursor/mcp.json"),
184
- markerPath: join(home, ".cursor"),
185
- },
186
- {
187
- name: "gemini_cli",
188
- configPath: join(home, ".gemini/settings.json"),
189
- markerPath: join(home, ".gemini"),
190
- },
191
- {
192
- name: "windsurf",
193
- configPath: join(home, ".codeium/windsurf/mcp_config.json"),
194
- markerPath: join(home, ".codeium/windsurf"),
195
- },
196
- {
197
- name: "devin",
198
- configPath: join(home, ".devin"),
199
- markerPath: join(home, ".devin"),
200
- format: "unsupported",
201
- },
202
- ];
203
164
  /** Back up, atomically update, and semantically read back one JSON MCP configuration. */
204
165
  export const configureJsonClient = async (client, hopperPath, command = [
205
166
  "npx",
@@ -0,0 +1,41 @@
1
+ import { join } from "node:path";
2
+ /** Describe every client location that setup, doctor, or uninstall may inspect. */
3
+ export const supportedClients = (home) => [
4
+ {
5
+ name: "claude_code",
6
+ configPath: join(home, ".claude.json"),
7
+ markerPath: join(home, ".claude"),
8
+ },
9
+ {
10
+ name: "claude_desktop",
11
+ configPath: join(home, "Library/Application Support/Claude/claude_desktop_config.json"),
12
+ markerPath: join(home, "Library/Application Support/Claude"),
13
+ },
14
+ {
15
+ name: "codex",
16
+ configPath: join(home, ".codex/config.toml"),
17
+ markerPath: join(home, ".codex"),
18
+ format: "toml",
19
+ },
20
+ {
21
+ name: "cursor",
22
+ configPath: join(home, ".cursor/mcp.json"),
23
+ markerPath: join(home, ".cursor"),
24
+ },
25
+ {
26
+ name: "gemini_cli",
27
+ configPath: join(home, ".gemini/settings.json"),
28
+ markerPath: join(home, ".gemini"),
29
+ },
30
+ {
31
+ name: "windsurf",
32
+ configPath: join(home, ".codeium/windsurf/mcp_config.json"),
33
+ markerPath: join(home, ".codeium/windsurf"),
34
+ },
35
+ {
36
+ name: "devin",
37
+ configPath: join(home, ".devin"),
38
+ markerPath: join(home, ".devin"),
39
+ format: "unsupported",
40
+ },
41
+ ];
@@ -5,7 +5,7 @@ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
5
5
  import writeFileAtomic from "write-file-atomic";
6
6
  import { z } from "zod";
7
7
  import { PRODUCT_IDENTITY } from "../identity.js";
8
- import { supportedClients } from "./Setup.js";
8
+ import { supportedClients } from "./SupportedClients.js";
9
9
  const systemFileSystem = {
10
10
  readText: (path) => readFile(path, "utf8"),
11
11
  copy: (source, destination) => copyFile(source, destination),
@@ -42,6 +42,8 @@ export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), out
42
42
  versionCheck: latestVersion === undefined ? "unavailable" : "available",
43
43
  installMethod: "npm",
44
44
  command: UPGRADE_COMMAND,
45
+ clientRestartRequired: true,
46
+ remediation: "Rerun rea setup to refresh registrations and the skill, then restart clients that may retain an older MCP server.",
45
47
  };
46
48
  };
47
49
  /** Create the npm registry and subprocess effects for a production upgrade. */
@@ -11,7 +11,7 @@ import { silentLogger } from "../logger.js";
11
11
  */
12
12
  export const createBinarySession = (config, logger = silentLogger) => {
13
13
  return new BinarySession(new CompositeProvider([
14
- new ArtifactProvider(config.artifactNativeMountEnabled),
14
+ new ArtifactProvider(config.artifactNativeMountEnabled, config.artifactIntegrityContinueEnabled),
15
15
  new NativeMacOSProvider(),
16
16
  new HopperProvider(config, logger),
17
17
  ]), { snapshotsEnabled: config.hopperLoaderArgs.length === 0 });
@@ -10,8 +10,10 @@ const IDENTITY = Object.freeze(ARTIFACT_GRAPH_PROVIDER);
10
10
  /** Read-only inventory and exclusively owned extraction provider. */
11
11
  export class ArtifactProvider {
12
12
  nativeMountEnabled;
13
- constructor(nativeMountEnabled = false) {
13
+ integrityContinueEnabled;
14
+ constructor(nativeMountEnabled = false, integrityContinueEnabled = false) {
14
15
  this.nativeMountEnabled = nativeMountEnabled;
16
+ this.integrityContinueEnabled = integrityContinueEnabled;
15
17
  }
16
18
  #capabilities = Object.freeze(ARTIFACT_TOOL_CONTRACTS.map((contract) => Object.freeze({
17
19
  provider: IDENTITY,
@@ -48,15 +50,17 @@ export class ArtifactProvider {
48
50
  return this.#capabilities;
49
51
  }
50
52
  createClient(target) {
51
- return new ArtifactClient(target, this.nativeMountEnabled);
53
+ return new ArtifactClient(target, this.nativeMountEnabled, this.integrityContinueEnabled);
52
54
  }
53
55
  }
54
56
  class ArtifactClient {
55
57
  target;
56
58
  nativeMountEnabled;
57
- constructor(target, nativeMountEnabled) {
59
+ integrityContinueEnabled;
60
+ constructor(target, nativeMountEnabled, integrityContinueEnabled) {
58
61
  this.target = target;
59
62
  this.nativeMountEnabled = nativeMountEnabled;
63
+ this.integrityContinueEnabled = integrityContinueEnabled;
60
64
  }
61
65
  async execute(operation, parameters, options) {
62
66
  if (operation === "health")
@@ -99,6 +103,12 @@ class ArtifactClient {
99
103
  nativeMountApproved: parsed.native_mount_approved === true,
100
104
  nativeMountEnabled: this.nativeMountEnabled,
101
105
  },
106
+ integrity: {
107
+ mode: parsed.integrity_policy,
108
+ approved: parsed.integrity_continue_approved === true,
109
+ enabled: this.integrityContinueEnabled,
110
+ maxMismatches: parsed.max_integrity_mismatches,
111
+ },
102
112
  });
103
113
  return ok(createAnalysisExecution(result, IDENTITY, {
104
114
  rawResult: null,
@@ -0,0 +1,121 @@
1
+ import { createHash } from "node:crypto";
2
+ import canonicalize from "canonicalize";
3
+ import { z } from "zod";
4
+ import { PROMPT_CONTRACTS } from "./contracts/promptContracts.js";
5
+ import { TOOL_CONTRACTS } from "./contracts/toolContracts.js";
6
+ /** Explicit CLI command inventory, kept separate from MCP tool counts. */
7
+ export const CLI_COMMAND_NAMES = [
8
+ "analyze",
9
+ "inspect",
10
+ "decompile",
11
+ "setup",
12
+ "doctor",
13
+ "uninstall",
14
+ "upgrade",
15
+ "xrefs",
16
+ "trace",
17
+ "capabilities",
18
+ "providers",
19
+ "function",
20
+ "search",
21
+ "import-reference-source",
22
+ "inventory-artifact",
23
+ "extract-artifact",
24
+ "inspect-macho",
25
+ "inspect-signature",
26
+ "list-architectures",
27
+ "inspect-plist",
28
+ "demangle-swift",
29
+ "evidence-import",
30
+ "evidence-export",
31
+ "compare",
32
+ "investigate-versions",
33
+ "capture-process",
34
+ "compare-process-captures",
35
+ "policy",
36
+ ];
37
+ const toolCatalog = TOOL_CONTRACTS.map((contract) => ({
38
+ name: contract.name,
39
+ surface: contract.kind,
40
+ description: contract.description,
41
+ annotations: contract.annotations,
42
+ input_schema: z.toJSONSchema(contract.inputSchema, {
43
+ unrepresentable: "any",
44
+ }),
45
+ output_schema: z.toJSONSchema(contract.outputSchema, {
46
+ unrepresentable: "any",
47
+ }),
48
+ })).sort((left, right) => left.name.localeCompare(right.name));
49
+ const promptCatalog = PROMPT_CONTRACTS.map((contract) => ({
50
+ name: contract.name,
51
+ title: contract.title,
52
+ description: contract.description,
53
+ arguments: contract.arguments,
54
+ steps: contract.steps,
55
+ })).sort((left, right) => left.name.localeCompare(right.name));
56
+ const resourceCatalog = [
57
+ { name: "server-identity", uri: "rea://server/identity" },
58
+ { name: "active-residual-unknowns", uri: "rea://unknowns/active" },
59
+ ];
60
+ const resourceTemplateCatalog = [
61
+ { name: "session-evidence", uri_template: "rea://evidence/{evidenceId}" },
62
+ {
63
+ name: "evidence-section",
64
+ uri_template: "rea://evidence/{evidenceId}/section/{section}",
65
+ },
66
+ { name: "residual-unknown", uri_template: "rea://unknown/{unknownId}" },
67
+ {
68
+ name: "analysis-snapshot",
69
+ uri_template: "rea://snapshot/{snapshotDigest}",
70
+ },
71
+ {
72
+ name: "artifact-page",
73
+ uri_template: "rea://artifact/{manifestId}/{collection}",
74
+ },
75
+ {
76
+ name: "function-dossier",
77
+ uri_template: "rea://function/{targetSha256}/{address}",
78
+ },
79
+ {
80
+ name: "investigation-workspace-revision",
81
+ uri_template: "rea://workspace/{workspaceId}/revision/{revision}",
82
+ },
83
+ ];
84
+ const digest = (value) => {
85
+ const encoded = canonicalize(value);
86
+ if (encoded === undefined)
87
+ throw new TypeError("Catalog is not canonical JSON");
88
+ return createHash("sha256").update(encoded).digest("hex");
89
+ };
90
+ /** Stable, schema-sensitive identity for every public REA surface. */
91
+ export const CATALOG_IDENTITY = {
92
+ counts: {
93
+ cli_commands: CLI_COMMAND_NAMES.length,
94
+ mcp_tools: toolCatalog.length,
95
+ mcp_prompts: promptCatalog.length,
96
+ mcp_resources: resourceCatalog.length,
97
+ mcp_resource_templates: resourceTemplateCatalog.length,
98
+ },
99
+ digests: {
100
+ tools_sha256: digest(toolCatalog),
101
+ prompts_sha256: digest(promptCatalog),
102
+ resources_sha256: digest({ resourceCatalog, resourceTemplateCatalog }),
103
+ combined_sha256: digest({
104
+ cli: CLI_COMMAND_NAMES,
105
+ tools: toolCatalog,
106
+ prompts: promptCatalog,
107
+ resources: resourceCatalog,
108
+ resource_templates: resourceTemplateCatalog,
109
+ }),
110
+ },
111
+ tools: toolCatalog.map(({ name, surface, annotations }) => ({
112
+ name,
113
+ surface,
114
+ annotations: {
115
+ read_only: annotations.readOnlyHint ?? false,
116
+ destructive: annotations.destructiveHint ?? false,
117
+ idempotent: annotations.idempotentHint ?? false,
118
+ open_world: annotations.openWorldHint ?? true,
119
+ },
120
+ })),
121
+ };