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
@@ -0,0 +1,123 @@
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
+ "list-browser-targets",
37
+ "inspect-web-page",
38
+ ];
39
+ const toolCatalog = TOOL_CONTRACTS.map((contract) => ({
40
+ name: contract.name,
41
+ surface: contract.kind,
42
+ description: contract.description,
43
+ annotations: contract.annotations,
44
+ input_schema: z.toJSONSchema(contract.inputSchema, {
45
+ unrepresentable: "any",
46
+ }),
47
+ output_schema: z.toJSONSchema(contract.outputSchema, {
48
+ unrepresentable: "any",
49
+ }),
50
+ })).sort((left, right) => left.name.localeCompare(right.name));
51
+ const promptCatalog = PROMPT_CONTRACTS.map((contract) => ({
52
+ name: contract.name,
53
+ title: contract.title,
54
+ description: contract.description,
55
+ arguments: contract.arguments,
56
+ steps: contract.steps,
57
+ })).sort((left, right) => left.name.localeCompare(right.name));
58
+ const resourceCatalog = [
59
+ { name: "server-identity", uri: "rea://server/identity" },
60
+ { name: "active-residual-unknowns", uri: "rea://unknowns/active" },
61
+ ];
62
+ const resourceTemplateCatalog = [
63
+ { name: "session-evidence", uri_template: "rea://evidence/{evidenceId}" },
64
+ {
65
+ name: "evidence-section",
66
+ uri_template: "rea://evidence/{evidenceId}/section/{section}",
67
+ },
68
+ { name: "residual-unknown", uri_template: "rea://unknown/{unknownId}" },
69
+ {
70
+ name: "analysis-snapshot",
71
+ uri_template: "rea://snapshot/{snapshotDigest}",
72
+ },
73
+ {
74
+ name: "artifact-page",
75
+ uri_template: "rea://artifact/{manifestId}/{collection}",
76
+ },
77
+ {
78
+ name: "function-dossier",
79
+ uri_template: "rea://function/{targetSha256}/{address}",
80
+ },
81
+ {
82
+ name: "investigation-workspace-revision",
83
+ uri_template: "rea://workspace/{workspaceId}/revision/{revision}",
84
+ },
85
+ ];
86
+ const digest = (value) => {
87
+ const encoded = canonicalize(value);
88
+ if (encoded === undefined)
89
+ throw new TypeError("Catalog is not canonical JSON");
90
+ return createHash("sha256").update(encoded).digest("hex");
91
+ };
92
+ /** Stable, schema-sensitive identity for every public REA surface. */
93
+ export const CATALOG_IDENTITY = {
94
+ counts: {
95
+ cli_commands: CLI_COMMAND_NAMES.length,
96
+ mcp_tools: toolCatalog.length,
97
+ mcp_prompts: promptCatalog.length,
98
+ mcp_resources: resourceCatalog.length,
99
+ mcp_resource_templates: resourceTemplateCatalog.length,
100
+ },
101
+ digests: {
102
+ tools_sha256: digest(toolCatalog),
103
+ prompts_sha256: digest(promptCatalog),
104
+ resources_sha256: digest({ resourceCatalog, resourceTemplateCatalog }),
105
+ combined_sha256: digest({
106
+ cli: CLI_COMMAND_NAMES,
107
+ tools: toolCatalog,
108
+ prompts: promptCatalog,
109
+ resources: resourceCatalog,
110
+ resource_templates: resourceTemplateCatalog,
111
+ }),
112
+ },
113
+ tools: toolCatalog.map(({ name, surface, annotations }) => ({
114
+ name,
115
+ surface,
116
+ annotations: {
117
+ read_only: annotations.readOnlyHint ?? false,
118
+ destructive: annotations.destructiveHint ?? false,
119
+ idempotent: annotations.idempotentHint ?? false,
120
+ open_world: annotations.openWorldHint ?? true,
121
+ },
122
+ })),
123
+ };
package/dist/cli.js CHANGED
@@ -14,8 +14,11 @@ import { importReferenceSource } from "./application/ReferenceSourceImport.js";
14
14
  import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
15
15
  import { registerProcessCommands } from "./cliProcessCommands.js";
16
16
  import { registerInvestigationCommands } from "./cliInvestigationCommands.js";
17
- import { projectAnalysisError } from "./domain/errors.js";
17
+ import { AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "./domain/errors.js";
18
18
  import { projectReferenceSourceImportError } from "./application/ReferenceSourceImportTypes.js";
19
+ import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
20
+ import { registerPolicyCommands } from "./cliPolicyCommands.js";
21
+ import { registerBrowserCommands } from "./cliBrowserCommands.js";
19
22
  /**
20
23
  * Build the one-shot Incur CLI without starting Hopper at import time.
21
24
  * Analysis commands acquire and close their own sessions; bare `mcp` and
@@ -26,7 +29,7 @@ export const createCli = () => {
26
29
  ? "silent"
27
30
  : parseLogLevel(process.env.REA_LOG_LEVEL));
28
31
  const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, {
29
- version: process.env.REA_PACKAGE_VERSION ?? "0.0.0-development",
32
+ version: PRODUCT_IDENTITY.packageVersion,
30
33
  description: "Reverse engineer anything from your terminal or agent.",
31
34
  mcp: {
32
35
  command: PRODUCT_IDENTITY.mcpCommand,
@@ -55,6 +58,8 @@ export const createCli = () => {
55
58
  registerEvidenceCommands(cli, logger);
56
59
  registerReferenceSourceCommand(cli, logger);
57
60
  registerProcessCommands(cli, logger);
61
+ registerPolicyCommands(cli, logger);
62
+ registerBrowserCommands(cli, logger);
58
63
  return cli;
59
64
  };
60
65
  const registerCoreCommands = (cli, logger) => {
@@ -135,7 +140,7 @@ const registerSetupCommands = (cli, logger) => {
135
140
  });
136
141
  cli.command("upgrade", {
137
142
  description: "Upgrade a global npm installation to the latest REA release",
138
- run: ({ formatExplicit }) => logCliCommand(logger, "upgrade", () => runUpgrade(process.env.REA_PACKAGE_VERSION ?? "0.0.0-development", systemUpgradeHost(), formatExplicit ? "structured" : "human")),
143
+ run: ({ formatExplicit }) => logCliCommand(logger, "upgrade", () => runUpgrade(PRODUCT_IDENTITY.packageVersion, systemUpgradeHost(), formatExplicit ? "structured" : "human")),
139
144
  });
140
145
  };
141
146
  const confirmSetup = async (actions) => {
@@ -270,6 +275,32 @@ const registerReferenceSourceCommand = (cli, logger) => {
270
275
  error: "Import failed",
271
276
  ...projectAnalysisError(config.error),
272
277
  };
278
+ const authority = await loadConfiguredPermissionAuthority(config.value);
279
+ if (!authority.ok)
280
+ return {
281
+ error: "Import failed",
282
+ ...projectAnalysisError(authority.error),
283
+ };
284
+ const authorized = await authority.value.authorize({
285
+ capability: "reference_read",
286
+ roots: [args.root],
287
+ executables: [],
288
+ environment_names: [],
289
+ network: "none",
290
+ mount: false,
291
+ operation_identity: `import_reference_source:${args.root}`,
292
+ }, "read");
293
+ if (!authorized.ok) {
294
+ const error = authorized.error instanceof PermissionRequiredError
295
+ ? authorized.error
296
+ : new AnalysisProtocolError(authorized.error.message, {
297
+ cause: authorized.error,
298
+ });
299
+ return {
300
+ error: "Import failed",
301
+ ...projectAnalysisError(error),
302
+ };
303
+ }
273
304
  const imported = await importReferenceSource({
274
305
  root: args.root,
275
306
  caller: "rea-cli",
@@ -295,7 +326,17 @@ const registerArtifactCommands = (cli, logger) => {
295
326
  options: z.object({
296
327
  offset: z.number().int().min(0).default(0),
297
328
  limit: z.number().int().min(1).max(500).default(100),
329
+ integrityPolicy: z.enum(["fail", "record-and-continue"]).default("fail"),
330
+ integrityContinueApproved: z.boolean().default(false),
331
+ maxIntegrityMismatches: z.number().int().min(1).max(100).default(10),
332
+ nativeMountApproved: z.boolean().default(false),
298
333
  }),
334
+ alias: {
335
+ integrityPolicy: "integrity-policy",
336
+ integrityContinueApproved: "integrity-continue-approved",
337
+ maxIntegrityMismatches: "max-integrity-mismatches",
338
+ nativeMountApproved: "native-mount-approved",
339
+ },
299
340
  run: ({ args, options }) => logCliCommand(logger, "inventory-artifact", () => runProviderAnalysis(args.path, "inventory_artifact", {
300
341
  node_offset: options.offset,
301
342
  node_limit: options.limit,
@@ -303,6 +344,10 @@ const registerArtifactCommands = (cli, logger) => {
303
344
  occurrence_limit: options.limit,
304
345
  edge_offset: options.offset,
305
346
  edge_limit: options.limit,
347
+ integrity_policy: options.integrityPolicy,
348
+ integrity_continue_approved: options.integrityContinueApproved,
349
+ max_integrity_mismatches: options.maxIntegrityMismatches,
350
+ native_mount_approved: options.nativeMountApproved,
306
351
  }, logger)),
307
352
  });
308
353
  cli.command("extract-artifact", {
@@ -0,0 +1,136 @@
1
+ import { Cli, z } from "incur";
2
+ import { inspectWebPage, listBrowserTargets, } from "./application/BrowserObservationService.js";
3
+ import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
4
+ import { CdpBrowserProvider } from "./browser/CdpBrowserProvider.js";
5
+ import { logCliCommand } from "./cliLogging.js";
6
+ import { parseConfig } from "./config.js";
7
+ import { inspectWebPageInputSchema, listBrowserTargetsInputSchema, } from "./domain/browserObservation.js";
8
+ import { AnalysisInputError, projectAnalysisError } from "./domain/errors.js";
9
+ const browserScopeOptions = {
10
+ allowedOrigins: z
11
+ .array(z.string().min(1))
12
+ .optional()
13
+ .describe("Exact origins to observe; defaults to REA_BROWSER_ALLOWED_ORIGINS_JSON"),
14
+ approved: z.boolean().default(false).describe("Approve passive observation"),
15
+ };
16
+ /** Register CLI equivalents of the passive browser MCP tools. */
17
+ export const registerBrowserCommands = (cli, logger) => {
18
+ registerTargetList(cli, logger);
19
+ registerPageInspection(cli, logger);
20
+ };
21
+ const registerTargetList = (cli, logger) => {
22
+ cli.command("list-browser-targets", {
23
+ description: "List approved pages from a user-owned loopback CDP browser",
24
+ args: z.object({
25
+ endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"),
26
+ }),
27
+ options: z.object({
28
+ ...browserScopeOptions,
29
+ offset: z.number().int().min(0).default(0),
30
+ limit: z.number().int().min(1).max(200).default(100),
31
+ }),
32
+ run: ({ args, options }) => logCliCommand(logger, "list-browser-targets", async () => {
33
+ const context = await browserContext();
34
+ if (!context.ok)
35
+ return context.error;
36
+ const parsed = listBrowserTargetsInputSchema.safeParse({
37
+ cdp_endpoint: args.endpoint,
38
+ allowed_origins: options.allowedOrigins ?? context.allowedBrowserOrigins,
39
+ approved: options.approved,
40
+ offset: options.offset,
41
+ limit: options.limit,
42
+ });
43
+ if (!parsed.success)
44
+ return cliError(new AnalysisInputError("list_browser_targets"));
45
+ const result = await listBrowserTargets(context.provider, context.authority, parsed.data);
46
+ return result.ok ? result.value : cliError(result.error);
47
+ }),
48
+ });
49
+ };
50
+ const registerPageInspection = (cli, logger) => {
51
+ cli.command("inspect-web-page", {
52
+ description: "Passively inspect one approved page through CDP",
53
+ args: z.object({
54
+ endpoint: z.string().describe("Configured loopback CDP HTTP endpoint"),
55
+ targetId: z.string().describe("Target ID from list-browser-targets"),
56
+ }),
57
+ options: z.object({
58
+ ...browserScopeOptions,
59
+ observationMs: z.number().int().min(0).max(10_000).default(500),
60
+ includeScriptSources: z.boolean().default(false),
61
+ includeStorageKeys: z.boolean().default(false),
62
+ maxFrames: z.number().int().min(1).max(1_000).default(200),
63
+ maxDomNodes: z.number().int().min(1).max(10_000).default(2_000),
64
+ maxAxNodes: z.number().int().min(1).max(10_000).default(2_000),
65
+ maxScripts: z.number().int().min(1).max(1_000).default(200),
66
+ maxResources: z.number().int().min(1).max(10_000).default(2_000),
67
+ maxWorkers: z.number().int().min(1).max(5_000).default(500),
68
+ maxStorageKeys: z.number().int().min(1).max(10_000).default(1_000),
69
+ maxScriptSourceBytes: z
70
+ .number()
71
+ .int()
72
+ .min(1)
73
+ .max(4 * 1_024 * 1_024)
74
+ .default(1_024 * 1_024),
75
+ maxTotalScriptSourceBytes: z
76
+ .number()
77
+ .int()
78
+ .min(1)
79
+ .max(16 * 1_024 * 1_024)
80
+ .default(4 * 1_024 * 1_024),
81
+ maxNetworkEvents: z.number().int().min(1).max(10_000).default(1_000),
82
+ maxConsoleEvents: z.number().int().min(1).max(2_000).default(200),
83
+ maxWebsocketEvents: z.number().int().min(1).max(5_000).default(500),
84
+ }),
85
+ run: ({ args, options }) => logCliCommand(logger, "inspect-web-page", async () => {
86
+ const context = await browserContext();
87
+ if (!context.ok)
88
+ return context.error;
89
+ const parsed = inspectWebPageInputSchema.safeParse({
90
+ cdp_endpoint: args.endpoint,
91
+ allowed_origins: options.allowedOrigins ?? context.allowedBrowserOrigins,
92
+ approved: options.approved,
93
+ target_id: args.targetId,
94
+ observation_ms: options.observationMs,
95
+ include_script_sources: options.includeScriptSources,
96
+ include_storage_keys: options.includeStorageKeys,
97
+ limits: {
98
+ max_frames: options.maxFrames,
99
+ max_dom_nodes: options.maxDomNodes,
100
+ max_ax_nodes: options.maxAxNodes,
101
+ max_scripts: options.maxScripts,
102
+ max_resources: options.maxResources,
103
+ max_workers: options.maxWorkers,
104
+ max_storage_keys: options.maxStorageKeys,
105
+ max_script_source_bytes: options.maxScriptSourceBytes,
106
+ max_total_script_source_bytes: options.maxTotalScriptSourceBytes,
107
+ max_network_events: options.maxNetworkEvents,
108
+ max_console_events: options.maxConsoleEvents,
109
+ max_websocket_events: options.maxWebsocketEvents,
110
+ },
111
+ });
112
+ if (!parsed.success)
113
+ return cliError(new AnalysisInputError("inspect_web_page"));
114
+ const result = await inspectWebPage(context.provider, context.authority, parsed.data);
115
+ return result.ok ? result.value : cliError(result.error);
116
+ }),
117
+ });
118
+ };
119
+ const browserContext = async () => {
120
+ const config = parseConfig(process.env);
121
+ if (!config.ok)
122
+ return { ok: false, error: cliError(config.error) };
123
+ const authority = await loadConfiguredPermissionAuthority(config.value);
124
+ if (!authority.ok)
125
+ return { ok: false, error: cliError(authority.error) };
126
+ return {
127
+ ok: true,
128
+ authority: authority.value,
129
+ provider: new CdpBrowserProvider(),
130
+ allowedBrowserOrigins: config.value.browserAllowedOrigins,
131
+ };
132
+ };
133
+ const cliError = (error) => ({
134
+ error: "Browser observation failed",
135
+ ...projectAnalysisError(error),
136
+ });
@@ -2,7 +2,8 @@ import { Cli, z } from "incur";
2
2
  import { compareEvidenceBundlesCommand, exportEvidenceBundleCommand, importEvidenceBundleCommand, } from "./application/EvidenceBundleCommands.js";
3
3
  import { parseConfig } from "./config.js";
4
4
  import { logCliCommand } from "./cliLogging.js";
5
- import { projectAnalysisError } from "./domain/errors.js";
5
+ import { AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "./domain/errors.js";
6
+ import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
6
7
  /** Register filesystem-gated Evidence v2 commands. */
7
8
  export const registerEvidenceCommands = (cli, logger) => {
8
9
  cli.command("evidence-import", {
@@ -14,6 +15,11 @@ export const registerEvidenceCommands = (cli, logger) => {
14
15
  const config = parseConfig(process.env);
15
16
  if (!config.ok)
16
17
  return cliError(config.error);
18
+ const denied = await authorizeEvidence(config.value, [
19
+ { capability: "evidence_read", path: args.path, access: "read" },
20
+ ]);
21
+ if (denied !== undefined)
22
+ return cliError(denied);
17
23
  const imported = await importEvidenceBundleCommand(args.path, config.value.evidenceFilePolicy);
18
24
  return imported.ok ? imported.value : cliError(imported.error);
19
25
  }),
@@ -31,6 +37,12 @@ export const registerEvidenceCommands = (cli, logger) => {
31
37
  const config = parseConfig(process.env);
32
38
  if (!config.ok)
33
39
  return cliError(config.error);
40
+ const denied = await authorizeEvidence(config.value, [
41
+ { capability: "evidence_read", path: args.source, access: "read" },
42
+ { capability: "evidence_write", path: args.output, access: "write" },
43
+ ]);
44
+ if (denied !== undefined)
45
+ return cliError(denied);
34
46
  const exported = await exportEvidenceBundleCommand(args.source, args.output, options.overwrite, config.value.evidenceFilePolicy);
35
47
  return exported.ok ? exported.value : cliError(exported.error);
36
48
  }),
@@ -50,6 +62,12 @@ export const registerEvidenceCommands = (cli, logger) => {
50
62
  const config = parseConfig(process.env);
51
63
  if (!config.ok)
52
64
  return cliError(config.error);
65
+ const denied = await authorizeEvidence(config.value, [
66
+ { capability: "evidence_read", path: args.left, access: "read" },
67
+ { capability: "evidence_read", path: args.right, access: "read" },
68
+ ]);
69
+ if (denied !== undefined)
70
+ return cliError(denied);
53
71
  const compared = await compareEvidenceBundlesCommand({
54
72
  leftPath: args.left,
55
73
  rightPath: args.right,
@@ -61,6 +79,29 @@ export const registerEvidenceCommands = (cli, logger) => {
61
79
  }),
62
80
  });
63
81
  };
82
+ const authorizeEvidence = async (config, requests) => {
83
+ const authority = await loadConfiguredPermissionAuthority(config);
84
+ if (!authority.ok)
85
+ return authority.error;
86
+ for (const request of requests) {
87
+ const result = await authority.value.authorize({
88
+ capability: request.capability,
89
+ roots: [request.path],
90
+ executables: [],
91
+ environment_names: [],
92
+ network: "none",
93
+ mount: false,
94
+ operation_identity: `cli:${request.capability}:${request.path}`,
95
+ }, request.access);
96
+ if (!result.ok)
97
+ return result.error instanceof PermissionRequiredError
98
+ ? result.error
99
+ : new AnalysisProtocolError(result.error.message, {
100
+ cause: result.error,
101
+ });
102
+ }
103
+ return undefined;
104
+ };
64
105
  const cliError = (error) => ({
65
106
  error: "Analysis failed",
66
107
  ...projectAnalysisError(error),
@@ -2,6 +2,9 @@ import { Cli, z } from "incur";
2
2
  import { runCrossVersionInvestigation } from "./application/CrossVersionInvestigation.js";
3
3
  import { logCliCommand } from "./cliLogging.js";
4
4
  import { parseConfig } from "./config.js";
5
+ import { projectAnalysisError } from "./domain/errors.js";
6
+ import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
7
+ import { authorizeFileReadWithDeferredWrite, authorizeRootPermission, } from "./application/DeferredFileAuthorization.js";
5
8
  const investigationOptionsSchema = z.object({
6
9
  yes: z
7
10
  .boolean()
@@ -24,6 +27,9 @@ const investigationOptionsSchema = z.object({
24
27
  .default(268_435_456),
25
28
  pageSize: z.number().int().min(1).max(500).default(500),
26
29
  changeLimit: z.number().int().min(1).max(500).default(500),
30
+ integrityPolicy: z.enum(["fail", "record-and-continue"]).default("fail"),
31
+ integrityContinueApproved: z.boolean().default(false),
32
+ maxIntegrityMismatches: z.number().int().min(1).max(100).default(10),
27
33
  });
28
34
  /** Register the persistent cross-version CLI workflow. */
29
35
  export const registerInvestigationCommands = (cli, logger) => {
@@ -43,6 +49,9 @@ export const registerInvestigationCommands = (cli, logger) => {
43
49
  maxEntryBytes: "max-entry-bytes",
44
50
  pageSize: "page-size",
45
51
  changeLimit: "change-limit",
52
+ integrityPolicy: "integrity-policy",
53
+ integrityContinueApproved: "integrity-continue-approved",
54
+ maxIntegrityMismatches: "max-integrity-mismatches",
46
55
  yes: "y",
47
56
  },
48
57
  run: ({ args, options }) => logCliCommand(logger, "investigate-versions", () => runInvestigation(args, options)),
@@ -56,7 +65,26 @@ const runInvestigation = async (args, options) => {
56
65
  };
57
66
  const config = parseConfig(process.env);
58
67
  if (!config.ok)
59
- return { error: config.error._tag, message: config.error.message };
68
+ return cliError(config.error);
69
+ const authority = await loadConfiguredPermissionAuthority(config.value);
70
+ if (!authority.ok)
71
+ return cliError(authority.error);
72
+ const workspaceAuthorization = await authorizeFileReadWithDeferredWrite(authority.value, {
73
+ path: args.workspacePath,
74
+ readCapability: "investigation_workspace_read",
75
+ writeCapability: "investigation_workspace_write",
76
+ operation: "investigate_versions",
77
+ });
78
+ if (!workspaceAuthorization.ok)
79
+ return cliError(workspaceAuthorization.error);
80
+ const inputAuthorization = await authorizeRootPermission(authority.value, {
81
+ capability: "investigation_input",
82
+ roots: [args.leftPath, args.rightPath],
83
+ access: "read",
84
+ operation: "investigate_versions",
85
+ });
86
+ if (!inputAuthorization.ok)
87
+ return cliError(inputAuthorization.error);
60
88
  const investigated = await runCrossVersionInvestigation({
61
89
  approved: true,
62
90
  workspace_path: args.workspacePath,
@@ -66,6 +94,9 @@ const runInvestigation = async (args, options) => {
66
94
  : { expected_workspace_revision: options.expectedRevision }),
67
95
  left_path: args.leftPath,
68
96
  right_path: args.rightPath,
97
+ integrity_policy: options.integrityPolicy,
98
+ integrity_continue_approved: options.integrityContinueApproved,
99
+ max_integrity_mismatches: options.maxIntegrityMismatches,
69
100
  options: {
70
101
  max_entries: options.maxEntries,
71
102
  max_total_bytes: options.maxTotalBytes,
@@ -76,11 +107,16 @@ const runInvestigation = async (args, options) => {
76
107
  page_size: options.pageSize,
77
108
  change_limit: options.changeLimit,
78
109
  },
79
- }, config.value.evidenceFilePolicy, { inputRoots: config.value.investigationInputRoots });
110
+ }, config.value.evidenceFilePolicy, {
111
+ inputRoots: config.value.investigationInputRoots,
112
+ integrityContinueEnabled: config.value.artifactIntegrityContinueEnabled,
113
+ authorizeWorkspaceWrite: workspaceAuthorization.value.authorizeWrite,
114
+ });
80
115
  return investigated.ok
81
116
  ? investigated.value.evidence
82
- : {
83
- error: investigated.error._tag,
84
- message: investigated.error.message,
85
- };
117
+ : cliError(investigated.error);
86
118
  };
119
+ const cliError = (error) => ({
120
+ error: "Analysis failed",
121
+ ...projectAnalysisError(error),
122
+ });
@@ -1,13 +1,30 @@
1
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2
+ /** Identify caller-visible CLI outcomes that mean the requested operation failed. */
3
+ export const isCliOperationFailure = (value) => {
4
+ if (!isRecord(value))
5
+ return false;
6
+ if (typeof value.error === "string")
7
+ return true;
8
+ if (value.healthy === false)
9
+ return true;
10
+ return (value.status === "failed" ||
11
+ value.status === "needs_confirmation" ||
12
+ value.status === "needs_human" ||
13
+ value.status === "planned");
14
+ };
1
15
  /** Log one CLI command with duration and a stable success or failure status. */
2
16
  export const logCliCommand = async (logger, command, execute) => {
3
17
  const startedAt = performance.now();
4
18
  try {
5
19
  const value = await execute();
6
- logger.info({
20
+ const failed = isCliOperationFailure(value);
21
+ logger[failed ? "error" : "info"]({
7
22
  command,
8
23
  durationMs: Math.round((performance.now() - startedAt) * 100) / 100,
9
- status: "ok",
10
- }, "CLI command completed");
24
+ status: failed ? "error" : "ok",
25
+ }, failed ? "CLI command failed" : "CLI command completed");
26
+ if (failed)
27
+ process.exitCode = 1;
11
28
  return value;
12
29
  }
13
30
  catch (cause) {
@@ -0,0 +1,118 @@
1
+ import { Cli, z } from "incur";
2
+ import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
3
+ import { readProjectPermissionStore, revokeProjectPermissionGrant, } from "./application/ProjectPermissionStore.js";
4
+ import { logCliCommand } from "./cliLogging.js";
5
+ import { parseConfig } from "./config.js";
6
+ import { PermissionRequiredError, projectAnalysisError, } from "./domain/errors.js";
7
+ const capabilitySchema = z.enum([
8
+ "process_capture",
9
+ "browser_observe",
10
+ "evidence_read",
11
+ "evidence_write",
12
+ "investigation_input",
13
+ "investigation_workspace_read",
14
+ "investigation_workspace_write",
15
+ "snapshot_read",
16
+ "snapshot_write",
17
+ "artifact_extract",
18
+ "native_mount",
19
+ "reference_read",
20
+ ]);
21
+ /** Register inspectable, dry-run, and revocation policy UX for the CLI. */
22
+ export const registerPolicyCommands = (cli, logger) => {
23
+ cli.command("policy", {
24
+ description: "Inspect, explain, or revoke unified local permission grants",
25
+ args: z.object({
26
+ action: z.enum(["status", "list", "explain", "revoke"]),
27
+ value: z.string().optional(),
28
+ }),
29
+ options: z.object({
30
+ root: z.string().optional(),
31
+ executable: z.string().optional(),
32
+ environmentNames: z.array(z.string()).default([]),
33
+ origins: z.array(z.string()).default([]),
34
+ network: z.enum(["none", "loopback", "external"]).default("none"),
35
+ mount: z.boolean().default(false),
36
+ write: z.boolean().default(false),
37
+ }),
38
+ alias: { environmentNames: "environment-names" },
39
+ run: ({ args, options }) => logCliCommand(logger, "policy", async () => {
40
+ const config = parseConfig(process.env);
41
+ if (!config.ok)
42
+ return failure(config.error);
43
+ if (args.action === "status")
44
+ return {
45
+ reload: "SIGHUP",
46
+ restart_required: false,
47
+ administrator_ceilings: config.value.permissionCeilings,
48
+ project_store: config.value.permissionProjectStore ?? null,
49
+ };
50
+ if (args.action === "list")
51
+ return readProjectGrants(config.value).then((result) => result.ok ? { grants: result.grants } : result.error);
52
+ if (args.action === "revoke") {
53
+ if (args.value === undefined)
54
+ return { error: "Grant ID is required for policy revoke" };
55
+ const { permissionProjectRoot, permissionProjectStore } = config.value;
56
+ if (permissionProjectRoot === undefined ||
57
+ permissionProjectStore === undefined)
58
+ return { error: "Project policy is not configured" };
59
+ const revoked = await revokeProjectPermissionGrant(permissionProjectStore, permissionProjectRoot, args.value);
60
+ return revoked.ok && revoked.value
61
+ ? {
62
+ revoked: args.value,
63
+ reload: "Send SIGHUP to the REA MCP process",
64
+ }
65
+ : revoked.ok
66
+ ? { error: "Grant was not found", grant_id: args.value }
67
+ : { error: revoked.error.message };
68
+ }
69
+ const capability = capabilitySchema.safeParse(args.value);
70
+ if (!capability.success)
71
+ return {
72
+ error: "A registered capability is required for policy explain",
73
+ };
74
+ return explain(config.value, capability.data, options);
75
+ }),
76
+ });
77
+ };
78
+ const explain = async (config, capability, options) => {
79
+ const authority = await loadConfiguredPermissionAuthority(config);
80
+ if (!authority.ok)
81
+ return failure(authority.error);
82
+ const evaluated = await authority.value.explain({
83
+ capability,
84
+ roots: options.root === undefined ? [] : [options.root],
85
+ executables: options.executable === undefined ? [] : [options.executable],
86
+ environment_names: options.environmentNames,
87
+ origins: options.origins,
88
+ network: options.network,
89
+ mount: options.mount,
90
+ operation_identity: `policy_explain:${capability}`,
91
+ }, options.write ? "write" : "read");
92
+ return evaluated.ok
93
+ ? { allowed: true, grant_id: evaluated.value.grant_id }
94
+ : evaluated.error instanceof PermissionRequiredError
95
+ ? { allowed: false, error: projectAnalysisError(evaluated.error) }
96
+ : { allowed: false, error: evaluated.error.message };
97
+ };
98
+ const readProjectGrants = async (config) => {
99
+ if (config.permissionProjectRoot === undefined ||
100
+ config.permissionProjectStore === undefined)
101
+ return {
102
+ ok: false,
103
+ error: { error: "Project policy is not configured" },
104
+ };
105
+ const store = await readProjectPermissionStore(config.permissionProjectStore, config.permissionProjectRoot);
106
+ return store.ok
107
+ ? {
108
+ ok: true,
109
+ grants: store.value?.grants ?? [],
110
+ path: config.permissionProjectStore,
111
+ root: config.permissionProjectRoot,
112
+ }
113
+ : { ok: false, error: { error: store.error.message } };
114
+ };
115
+ const failure = (error) => ({
116
+ error: "Policy command failed",
117
+ ...projectAnalysisError(error),
118
+ });