rea-agents 1.2.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 (107) hide show
  1. package/README.md +95 -27
  2. package/bridge/hopper_bridge.py +172 -14
  3. package/dist/application/AnalysisSnapshotCache.js +131 -0
  4. package/dist/application/AnalysisSnapshotFiles.js +31 -0
  5. package/dist/application/ArtifactGraphConstruction.js +1 -1
  6. package/dist/application/ArtifactInventory.js +100 -24
  7. package/dist/application/AuthorizedArtifactInventory.js +16 -0
  8. package/dist/application/BinarySession.js +149 -36
  9. package/dist/application/BinarySessionPort.js +1 -0
  10. package/dist/application/BoundedJsonFiles.js +109 -0
  11. package/dist/application/CapabilityInventory.js +127 -0
  12. package/dist/application/ClientRegistrationStatus.js +69 -0
  13. package/dist/application/CrossVersionInventory.js +109 -0
  14. package/dist/application/CrossVersionInvestigation.js +358 -0
  15. package/dist/application/DirectAnalysis.js +205 -24
  16. package/dist/application/Doctor.js +104 -3
  17. package/dist/application/EvidenceBundleFiles.js +15 -108
  18. package/dist/application/EvidenceLedger.js +3 -2
  19. package/dist/application/InvestigationProviders.js +48 -0
  20. package/dist/application/InvestigationWorkspaceStore.js +214 -0
  21. package/dist/application/LinuxHopper.js +72 -48
  22. package/dist/application/PermissionAuthority.js +183 -0
  23. package/dist/application/PermissionConfiguration.js +26 -0
  24. package/dist/application/ProcessCaptureAuthority.js +2 -2
  25. package/dist/application/ProcessCaptureError.js +25 -0
  26. package/dist/application/ProcessCaptureLifecycle.js +22 -2
  27. package/dist/application/ProcessCli.js +111 -31
  28. package/dist/application/ProcessHarness.js +6 -3
  29. package/dist/application/ProgressReporter.js +30 -0
  30. package/dist/application/ProjectPermissionStore.js +110 -0
  31. package/dist/application/ReferenceSourceImportEntries.js +18 -3
  32. package/dist/application/ReferenceSourceImportTypes.js +32 -0
  33. package/dist/application/Setup.js +35 -101
  34. package/dist/application/SetupClients.js +19 -0
  35. package/dist/application/SetupInstallFailure.js +53 -0
  36. package/dist/application/SetupPlan.js +31 -0
  37. package/dist/application/SupportedClients.js +41 -0
  38. package/dist/application/Uninstall.js +34 -24
  39. package/dist/application/UnknownEvidence.js +33 -0
  40. package/dist/application/Upgrade.js +3 -1
  41. package/dist/application/runtime.js +2 -2
  42. package/dist/artifacts/ArtifactProvider.js +15 -8
  43. package/dist/catalogIdentity.js +121 -0
  44. package/dist/cli.js +76 -13
  45. package/dist/cliEvidenceCommands.js +52 -12
  46. package/dist/cliInvestigationCommands.js +137 -0
  47. package/dist/cliLogging.js +20 -3
  48. package/dist/cliOutput.js +41 -0
  49. package/dist/cliPolicyCommands.js +115 -0
  50. package/dist/config.js +102 -27
  51. package/dist/contracts/artifactToolContracts.js +14 -1
  52. package/dist/contracts/errorSchemas.js +98 -0
  53. package/dist/contracts/promptContracts.js +256 -0
  54. package/dist/contracts/sessionLifecycleInputs.js +11 -0
  55. package/dist/contracts/toolContractTypes.js +1 -0
  56. package/dist/contracts/toolContracts.js +20 -7
  57. package/dist/contracts/toolOutputSchemas.js +70 -9
  58. package/dist/domain/analysisSnapshot.js +149 -0
  59. package/dist/domain/artifactComparison.js +38 -11
  60. package/dist/domain/artifactGraph.js +25 -1
  61. package/dist/domain/artifactInventoryEvidence.js +18 -0
  62. package/dist/domain/changedBehavior.js +35 -10
  63. package/dist/domain/errors.js +384 -32
  64. package/dist/domain/evidence.js +4 -2
  65. package/dist/domain/evidenceBundle.js +28 -0
  66. package/dist/domain/hopperStartupFailure.js +55 -0
  67. package/dist/domain/investigationWorkspace.js +341 -0
  68. package/dist/domain/jsonValue.js +2 -0
  69. package/dist/domain/nativeInspection.js +3 -2
  70. package/dist/domain/permissionPolicy.js +157 -0
  71. package/dist/domain/processCapture.js +5 -4
  72. package/dist/domain/processComparison.js +6 -4
  73. package/dist/domain/reconstructionVerification.js +1 -1
  74. package/dist/domain/reconstructionVerificationSchemas.js +1 -0
  75. package/dist/domain/staticRuntimeCorrelation.js +5 -1
  76. package/dist/generatedPackageMetadata.js +9 -0
  77. package/dist/hopper/BridgeLauncher.js +51 -20
  78. package/dist/hopper/HopperClient.js +45 -3
  79. package/dist/hopper/HopperProvider.js +1 -0
  80. package/dist/identity.js +10 -3
  81. package/dist/logger.js +2 -2
  82. package/dist/main.js +139 -28
  83. package/dist/server/createServer.js +54 -5
  84. package/dist/server/mcpProgress.js +23 -0
  85. package/dist/server/promptCompletion.js +144 -0
  86. package/dist/server/registerArtifactComparisonTool.js +6 -2
  87. package/dist/server/registerBundleComparisonTool.js +6 -2
  88. package/dist/server/registerEnhancedTools.js +20 -7
  89. package/dist/server/registerEvidenceResources.js +302 -0
  90. package/dist/server/registerEvidenceTools.js +59 -8
  91. package/dist/server/registerFunctionComparisonTool.js +7 -3
  92. package/dist/server/registerInvestigationTools.js +111 -18
  93. package/dist/server/registerOfficialTools.js +25 -14
  94. package/dist/server/registerProcessComparisonTool.js +8 -10
  95. package/dist/server/registerPrompts.js +67 -0
  96. package/dist/server/registerSessionStatusTool.js +52 -0
  97. package/dist/server/registerSessionTools.js +178 -22
  98. package/dist/server/runDerivedOperation.js +35 -0
  99. package/dist/server/sessionEvidence.js +2 -8
  100. package/dist/server/sessionToolPolicies.js +1 -42
  101. package/dist/server/toolResult.js +34 -6
  102. package/dist/serverIdentity.js +70 -0
  103. package/install.sh +11 -11
  104. package/package.json +10 -6
  105. package/scripts/hopper-demo-x11.py +349 -0
  106. package/scripts/rea.mjs +6 -3
  107. package/skills/rea-analysis/SKILL.md +9 -2
@@ -2,6 +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 { AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "./domain/errors.js";
6
+ import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
5
7
  /** Register filesystem-gated Evidence v2 commands. */
6
8
  export const registerEvidenceCommands = (cli, logger) => {
7
9
  cli.command("evidence-import", {
@@ -12,11 +14,14 @@ export const registerEvidenceCommands = (cli, logger) => {
12
14
  run: ({ args }) => logCliCommand(logger, "evidence-import", async () => {
13
15
  const config = parseConfig(process.env);
14
16
  if (!config.ok)
15
- return { error: config.error._tag, message: config.error.message };
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);
16
23
  const imported = await importEvidenceBundleCommand(args.path, config.value.evidenceFilePolicy);
17
- return imported.ok
18
- ? imported.value
19
- : { error: imported.error._tag, message: imported.error.message };
24
+ return imported.ok ? imported.value : cliError(imported.error);
20
25
  }),
21
26
  });
22
27
  cli.command("evidence-export", {
@@ -31,11 +36,15 @@ export const registerEvidenceCommands = (cli, logger) => {
31
36
  run: ({ args, options }) => logCliCommand(logger, "evidence-export", async () => {
32
37
  const config = parseConfig(process.env);
33
38
  if (!config.ok)
34
- return { error: config.error._tag, message: config.error.message };
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);
35
46
  const exported = await exportEvidenceBundleCommand(args.source, args.output, options.overwrite, config.value.evidenceFilePolicy);
36
- return exported.ok
37
- ? exported.value
38
- : { error: exported.error._tag, message: exported.error.message };
47
+ return exported.ok ? exported.value : cliError(exported.error);
39
48
  }),
40
49
  });
41
50
  cli.command("compare", {
@@ -52,7 +61,13 @@ export const registerEvidenceCommands = (cli, logger) => {
52
61
  run: ({ args, options }) => logCliCommand(logger, "compare", async () => {
53
62
  const config = parseConfig(process.env);
54
63
  if (!config.ok)
55
- return { error: config.error._tag, message: config.error.message };
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);
56
71
  const compared = await compareEvidenceBundlesCommand({
57
72
  leftPath: args.left,
58
73
  rightPath: args.right,
@@ -60,9 +75,34 @@ export const registerEvidenceCommands = (cli, logger) => {
60
75
  limit: options.limit,
61
76
  policy: config.value.evidenceFilePolicy,
62
77
  });
63
- return compared.ok
64
- ? compared.value
65
- : { error: compared.error._tag, message: compared.error.message };
78
+ return compared.ok ? compared.value : cliError(compared.error);
66
79
  }),
67
80
  });
68
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
+ };
105
+ const cliError = (error) => ({
106
+ error: "Analysis failed",
107
+ ...projectAnalysisError(error),
108
+ });
@@ -0,0 +1,137 @@
1
+ import { Cli, z } from "incur";
2
+ import { runCrossVersionInvestigation } from "./application/CrossVersionInvestigation.js";
3
+ import { logCliCommand } from "./cliLogging.js";
4
+ import { parseConfig } from "./config.js";
5
+ import { AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "./domain/errors.js";
6
+ import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
7
+ const investigationOptionsSchema = z.object({
8
+ yes: z
9
+ .boolean()
10
+ .default(false)
11
+ .describe("Approve persistent workspace writes"),
12
+ workspaceName: z.string().trim().min(1).max(200).default("default"),
13
+ expectedRevision: z.number().int().min(1).optional(),
14
+ maxEntries: z.number().int().min(1).max(50_000).default(10_000),
15
+ maxTotalBytes: z
16
+ .number()
17
+ .int()
18
+ .min(1)
19
+ .max(Number.MAX_SAFE_INTEGER)
20
+ .default(1_073_741_824),
21
+ maxEntryBytes: z
22
+ .number()
23
+ .int()
24
+ .min(1)
25
+ .max(Number.MAX_SAFE_INTEGER)
26
+ .default(268_435_456),
27
+ pageSize: z.number().int().min(1).max(500).default(500),
28
+ changeLimit: z.number().int().min(1).max(500).default(500),
29
+ integrityPolicy: z.enum(["fail", "record-and-continue"]).default("fail"),
30
+ integrityContinueApproved: z.boolean().default(false),
31
+ maxIntegrityMismatches: z.number().int().min(1).max(100).default(10),
32
+ });
33
+ /** Register the persistent cross-version CLI workflow. */
34
+ export const registerInvestigationCommands = (cli, logger) => {
35
+ cli.command("investigate-versions", {
36
+ description: "Run or resume a persistent cross-version artifact investigation",
37
+ args: z.object({
38
+ leftPath: z.string().describe("Earlier artifact or directory path"),
39
+ rightPath: z.string().describe("Later artifact or directory path"),
40
+ workspacePath: z.string().describe("Persistent workspace JSON path"),
41
+ }),
42
+ options: investigationOptionsSchema,
43
+ alias: {
44
+ workspaceName: "workspace-name",
45
+ expectedRevision: "expected-revision",
46
+ maxEntries: "max-entries",
47
+ maxTotalBytes: "max-total-bytes",
48
+ maxEntryBytes: "max-entry-bytes",
49
+ pageSize: "page-size",
50
+ changeLimit: "change-limit",
51
+ integrityPolicy: "integrity-policy",
52
+ integrityContinueApproved: "integrity-continue-approved",
53
+ maxIntegrityMismatches: "max-integrity-mismatches",
54
+ yes: "y",
55
+ },
56
+ run: ({ args, options }) => logCliCommand(logger, "investigate-versions", () => runInvestigation(args, options)),
57
+ });
58
+ };
59
+ const runInvestigation = async (args, options) => {
60
+ if (!options.yes)
61
+ return {
62
+ error: "ApprovalRequired",
63
+ message: "Persistent workspace writes require explicit --yes approval.",
64
+ };
65
+ const config = parseConfig(process.env);
66
+ if (!config.ok)
67
+ return cliError(config.error);
68
+ const authority = await loadConfiguredPermissionAuthority(config.value);
69
+ if (!authority.ok)
70
+ return cliError(authority.error);
71
+ for (const scope of [
72
+ {
73
+ capability: "investigation_workspace_read",
74
+ roots: [args.workspacePath],
75
+ access: "write",
76
+ },
77
+ {
78
+ capability: "investigation_workspace_write",
79
+ roots: [args.workspacePath],
80
+ access: "write",
81
+ },
82
+ {
83
+ capability: "investigation_input",
84
+ roots: [args.leftPath, args.rightPath],
85
+ access: "read",
86
+ },
87
+ ]) {
88
+ const authorized = await authority.value.authorize({
89
+ capability: scope.capability,
90
+ roots: scope.roots,
91
+ executables: [],
92
+ environment_names: [],
93
+ network: "none",
94
+ mount: false,
95
+ operation_identity: `investigate_versions:${scope.capability}:${scope.roots.join(":")}`,
96
+ }, scope.access);
97
+ if (!authorized.ok)
98
+ return cliError(authorized.error instanceof PermissionRequiredError
99
+ ? authorized.error
100
+ : new AnalysisProtocolError(authorized.error.message, {
101
+ cause: authorized.error,
102
+ }));
103
+ }
104
+ const investigated = await runCrossVersionInvestigation({
105
+ approved: true,
106
+ workspace_path: args.workspacePath,
107
+ workspace_name: options.workspaceName,
108
+ ...(options.expectedRevision === undefined
109
+ ? {}
110
+ : { expected_workspace_revision: options.expectedRevision }),
111
+ left_path: args.leftPath,
112
+ right_path: args.rightPath,
113
+ integrity_policy: options.integrityPolicy,
114
+ integrity_continue_approved: options.integrityContinueApproved,
115
+ max_integrity_mismatches: options.maxIntegrityMismatches,
116
+ options: {
117
+ max_entries: options.maxEntries,
118
+ max_total_bytes: options.maxTotalBytes,
119
+ max_entry_bytes: options.maxEntryBytes,
120
+ max_compression_ratio: 1_000,
121
+ max_depth: 20,
122
+ max_path_bytes: 4_096,
123
+ page_size: options.pageSize,
124
+ change_limit: options.changeLimit,
125
+ },
126
+ }, config.value.evidenceFilePolicy, {
127
+ inputRoots: config.value.investigationInputRoots,
128
+ integrityContinueEnabled: config.value.artifactIntegrityContinueEnabled,
129
+ });
130
+ return investigated.ok
131
+ ? investigated.value.evidence
132
+ : cliError(investigated.error);
133
+ };
134
+ const cliError = (error) => ({
135
+ error: "Analysis failed",
136
+ ...projectAnalysisError(error),
137
+ });
@@ -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,41 @@
1
+ const SAFE_VALIDATION_MESSAGE = "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again.";
2
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3
+ const validationError = (value) => {
4
+ if (!isRecord(value))
5
+ return undefined;
6
+ if (value.code === "VALIDATION_ERROR")
7
+ return value;
8
+ if (value.ok === false && isRecord(value.error))
9
+ return value.error.code === "VALIDATION_ERROR" ? value.error : undefined;
10
+ return undefined;
11
+ };
12
+ /** Remove validator internals from Incur's caller-visible CLI output. */
13
+ export const sanitizeCliOutput = (output) => {
14
+ const trimmed = output.trimStart();
15
+ if (trimmed.startsWith("{")) {
16
+ try {
17
+ const parsed = JSON.parse(trimmed);
18
+ const error = validationError(parsed);
19
+ if (error !== undefined) {
20
+ const safeError = {
21
+ code: "VALIDATION_ERROR",
22
+ message: SAFE_VALIDATION_MESSAGE,
23
+ };
24
+ if (error === parsed)
25
+ return `${JSON.stringify(safeError)}\n`;
26
+ if (!isRecord(parsed))
27
+ return output;
28
+ return `${JSON.stringify({ ...parsed, error: safeError })}\n`;
29
+ }
30
+ }
31
+ catch {
32
+ return output;
33
+ }
34
+ return output;
35
+ }
36
+ if (/^ok: false\r?\nerror:\r?\n code: VALIDATION_ERROR(?:\r?\n|$)/u.test(trimmed))
37
+ return `ok: false\nerror:\n code: VALIDATION_ERROR\n message: "${SAFE_VALIDATION_MESSAGE}"\n`;
38
+ if (!/^code: VALIDATION_ERROR(?:\r?\n|$)/u.test(trimmed))
39
+ return output;
40
+ return `code: VALIDATION_ERROR\nmessage: "${SAFE_VALIDATION_MESSAGE}"\n`;
41
+ };
@@ -0,0 +1,115 @@
1
+ import { Cli, z } from "incur";
2
+ import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
3
+ import { readProjectPermissionStore, writeProjectPermissionStore, } 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
+ "evidence_read",
10
+ "evidence_write",
11
+ "investigation_input",
12
+ "investigation_workspace_read",
13
+ "investigation_workspace_write",
14
+ "snapshot_read",
15
+ "snapshot_write",
16
+ "artifact_extract",
17
+ "native_mount",
18
+ "reference_read",
19
+ ]);
20
+ /** Register inspectable, dry-run, and revocation policy UX for the CLI. */
21
+ export const registerPolicyCommands = (cli, logger) => {
22
+ cli.command("policy", {
23
+ description: "Inspect, explain, or revoke unified local permission grants",
24
+ args: z.object({
25
+ action: z.enum(["status", "list", "explain", "revoke"]),
26
+ value: z.string().optional(),
27
+ }),
28
+ options: z.object({
29
+ root: z.string().optional(),
30
+ executable: z.string().optional(),
31
+ environmentNames: z.array(z.string()).default([]),
32
+ network: z.enum(["none", "loopback", "external"]).default("none"),
33
+ mount: z.boolean().default(false),
34
+ write: z.boolean().default(false),
35
+ }),
36
+ alias: { environmentNames: "environment-names" },
37
+ run: ({ args, options }) => logCliCommand(logger, "policy", async () => {
38
+ const config = parseConfig(process.env);
39
+ if (!config.ok)
40
+ return failure(config.error);
41
+ if (args.action === "status")
42
+ return {
43
+ reload: "SIGHUP",
44
+ restart_required: false,
45
+ administrator_ceilings: config.value.permissionCeilings,
46
+ project_store: config.value.permissionProjectStore ?? null,
47
+ };
48
+ if (args.action === "list")
49
+ return readProjectGrants(config.value).then((result) => result.ok ? { grants: result.grants } : result.error);
50
+ if (args.action === "revoke") {
51
+ if (args.value === undefined)
52
+ return { error: "Grant ID is required for policy revoke" };
53
+ const result = await readProjectGrants(config.value);
54
+ if (!result.ok)
55
+ return result.error;
56
+ const retained = result.grants.filter(({ grant_id }) => grant_id !== args.value);
57
+ if (retained.length === result.grants.length)
58
+ return { error: "Grant was not found", grant_id: args.value };
59
+ const written = await writeProjectPermissionStore(result.path, result.root, retained);
60
+ return written.ok
61
+ ? {
62
+ revoked: args.value,
63
+ reload: "Send SIGHUP to the REA MCP process",
64
+ }
65
+ : { error: written.error.message };
66
+ }
67
+ const capability = capabilitySchema.safeParse(args.value);
68
+ if (!capability.success)
69
+ return {
70
+ error: "A registered capability is required for policy explain",
71
+ };
72
+ return explain(config.value, capability.data, options);
73
+ }),
74
+ });
75
+ };
76
+ const explain = async (config, capability, options) => {
77
+ const authority = await loadConfiguredPermissionAuthority(config);
78
+ if (!authority.ok)
79
+ return failure(authority.error);
80
+ const evaluated = await authority.value.explain({
81
+ capability,
82
+ roots: options.root === undefined ? [] : [options.root],
83
+ executables: options.executable === undefined ? [] : [options.executable],
84
+ environment_names: options.environmentNames,
85
+ network: options.network,
86
+ mount: options.mount,
87
+ operation_identity: `policy_explain:${capability}`,
88
+ }, options.write ? "write" : "read");
89
+ return evaluated.ok
90
+ ? { allowed: true, grant_id: evaluated.value.grant_id }
91
+ : evaluated.error instanceof PermissionRequiredError
92
+ ? { allowed: false, error: projectAnalysisError(evaluated.error) }
93
+ : { allowed: false, error: evaluated.error.message };
94
+ };
95
+ const readProjectGrants = async (config) => {
96
+ if (config.permissionProjectRoot === undefined ||
97
+ config.permissionProjectStore === undefined)
98
+ return {
99
+ ok: false,
100
+ error: { error: "Project policy is not configured" },
101
+ };
102
+ const store = await readProjectPermissionStore(config.permissionProjectStore, config.permissionProjectRoot);
103
+ return store.ok
104
+ ? {
105
+ ok: true,
106
+ grants: store.value?.grants ?? [],
107
+ path: config.permissionProjectStore,
108
+ root: config.permissionProjectRoot,
109
+ }
110
+ : { ok: false, error: { error: store.error.message } };
111
+ };
112
+ const failure = (error) => ({
113
+ error: "Policy command failed",
114
+ ...projectAnalysisError(error),
115
+ });
package/dist/config.js CHANGED
@@ -5,16 +5,24 @@ const DEFAULT_HOPPER_LAUNCHER_PATH = "/Applications/Hopper Disassembler.app/Cont
5
5
  const defaultHopperLauncherPath = () => process.platform === "linux"
6
6
  ? "/opt/hopper/bin/Hopper"
7
7
  : DEFAULT_HOPPER_LAUNCHER_PATH;
8
- const environmentSchema = z.object({
8
+ const environmentSchema = z
9
+ .object({
9
10
  HOPPER_LAUNCHER_PATH: z.string().min(1).optional(),
10
11
  HOPPER_TARGET_PATH: z.string().min(1).optional(),
11
- HOPPER_TARGET_KIND: z.enum(["executable", "database"]).default("executable"),
12
+ HOPPER_TARGET_KIND: z
13
+ .enum(["executable", "database"])
14
+ .default("executable"),
12
15
  HOPPER_LOADER_ARGS_JSON: z.string().optional(),
13
16
  REA_LOG_LEVEL: z
14
17
  .enum(["trace", "debug", "info", "warn", "error", "fatal", "silent"])
15
18
  .default("info"),
16
19
  REA_PROCESS_CAPTURE_ENABLED: z.enum(["true", "false"]).default("false"),
17
- REA_ARTIFACT_NATIVE_MOUNT_ENABLED: z.enum(["true", "false"]).default("false"),
20
+ REA_ARTIFACT_NATIVE_MOUNT_ENABLED: z
21
+ .enum(["true", "false"])
22
+ .default("false"),
23
+ REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED: z
24
+ .enum(["true", "false"])
25
+ .default("false"),
18
26
  REA_PROCESS_ALLOW_EXTERNAL_NETWORK: z
19
27
  .enum(["true", "false"])
20
28
  .default("false"),
@@ -22,8 +30,16 @@ const environmentSchema = z.object({
22
30
  REA_PROCESS_WORKING_ROOTS_JSON: z.string().default("[]"),
23
31
  REA_PROCESS_ALLOWED_ENV_JSON: z.string().default("[]"),
24
32
  REA_EVIDENCE_ROOTS_JSON: z.string().default("[]"),
33
+ REA_INVESTIGATION_INPUT_ROOTS_JSON: z.string().default("[]"),
34
+ REA_ANALYSIS_SNAPSHOT_ROOTS_JSON: z.string().default("[]"),
25
35
  REA_REFERENCE_ROOTS_JSON: z.string().default("[]"),
26
36
  REA_REFERENCE_SECRET_PATTERNS_JSON: z.string().default("[]"),
37
+ REA_PERMISSION_PROJECT_ROOT: z.string().min(1).optional(),
38
+ REA_PERMISSION_PROJECT_STORE: z.string().min(1).optional(),
39
+ })
40
+ .refine((value) => (value.REA_PERMISSION_PROJECT_ROOT === undefined) ===
41
+ (value.REA_PERMISSION_PROJECT_STORE === undefined), {
42
+ message: "REA_PERMISSION_PROJECT_ROOT and REA_PERMISSION_PROJECT_STORE must be configured together",
27
43
  });
28
44
  const parseStringArray = (encoded, name) => {
29
45
  try {
@@ -41,6 +57,45 @@ const parseStringArray = (encoded, name) => {
41
57
  return err(new ConfigurationError(`${name} must be valid JSON`, { cause }));
42
58
  }
43
59
  };
60
+ const filePolicy = (roots) => ({
61
+ roots,
62
+ maxBytes: 64 * 1024 * 1024,
63
+ maxDepth: 64,
64
+ maxStringLength: 1024 * 1024,
65
+ maxNodes: 1_000_000,
66
+ });
67
+ const permissionScope = (capability, roots, options = {}) => ({
68
+ capability,
69
+ roots,
70
+ executables: options.executables ?? [],
71
+ environment_names: options.environment_names ?? [],
72
+ network: options.network ?? "none",
73
+ mount: options.mount ?? false,
74
+ });
75
+ const administratorGrants = (ceilings) => ceilings.map((ceiling) => ({
76
+ ...ceiling,
77
+ grant_id: `administrator:${ceiling.capability}`,
78
+ lifetime: "administrator",
79
+ operation_identity: null,
80
+ expires_at: null,
81
+ }));
82
+ const parseLoaderArgs = (encoded) => {
83
+ if (encoded === undefined)
84
+ return ok([]);
85
+ let decoded;
86
+ try {
87
+ decoded = JSON.parse(encoded);
88
+ }
89
+ catch (cause) {
90
+ return err(new ConfigurationError("HOPPER_LOADER_ARGS_JSON must be valid JSON", {
91
+ cause,
92
+ }));
93
+ }
94
+ const parsed = z.array(z.string()).safeParse(decoded);
95
+ return parsed.success
96
+ ? ok(parsed.data)
97
+ : err(new ConfigurationError("HOPPER_LOADER_ARGS_JSON must encode an array of strings", { cause: parsed.error }));
98
+ };
44
99
  /**
45
100
  * Parse Hopper launcher configuration once at the composition root.
46
101
  * Explicit loader arguments override REA's header-derived defaults for a
@@ -54,22 +109,9 @@ export const parseConfig = (environment) => {
54
109
  cause: parsedEnvironment.error,
55
110
  }));
56
111
  }
57
- let loaderArgs = [];
58
- const encoded = parsedEnvironment.data.HOPPER_LOADER_ARGS_JSON;
59
- if (encoded !== undefined) {
60
- try {
61
- loaderArgs = JSON.parse(encoded);
62
- }
63
- catch (cause) {
64
- return err(new ConfigurationError("HOPPER_LOADER_ARGS_JSON must be valid JSON", {
65
- cause,
66
- }));
67
- }
68
- }
69
- const parsedArgs = z.array(z.string()).safeParse(loaderArgs);
70
- if (!parsedArgs.success) {
71
- return err(new ConfigurationError("HOPPER_LOADER_ARGS_JSON must encode an array of strings", { cause: parsedArgs.error }));
72
- }
112
+ const loaderArgs = parseLoaderArgs(parsedEnvironment.data.HOPPER_LOADER_ARGS_JSON);
113
+ if (!loaderArgs.ok)
114
+ return loaderArgs;
73
115
  const executableRoots = parseStringArray(parsedEnvironment.data.REA_PROCESS_EXECUTABLE_ROOTS_JSON, "REA_PROCESS_EXECUTABLE_ROOTS_JSON");
74
116
  if (!executableRoots.ok)
75
117
  return executableRoots;
@@ -82,18 +124,50 @@ export const parseConfig = (environment) => {
82
124
  const evidenceRoots = parseStringArray(parsedEnvironment.data.REA_EVIDENCE_ROOTS_JSON, "REA_EVIDENCE_ROOTS_JSON");
83
125
  if (!evidenceRoots.ok)
84
126
  return evidenceRoots;
127
+ const investigationInputRoots = parseStringArray(parsedEnvironment.data.REA_INVESTIGATION_INPUT_ROOTS_JSON, "REA_INVESTIGATION_INPUT_ROOTS_JSON");
128
+ if (!investigationInputRoots.ok)
129
+ return investigationInputRoots;
130
+ const analysisSnapshotRoots = parseStringArray(parsedEnvironment.data.REA_ANALYSIS_SNAPSHOT_ROOTS_JSON, "REA_ANALYSIS_SNAPSHOT_ROOTS_JSON");
131
+ if (!analysisSnapshotRoots.ok)
132
+ return analysisSnapshotRoots;
85
133
  const referenceRoots = parseStringArray(parsedEnvironment.data.REA_REFERENCE_ROOTS_JSON, "REA_REFERENCE_ROOTS_JSON");
86
134
  if (!referenceRoots.ok)
87
135
  return referenceRoots;
88
136
  const secretPatterns = parseStringArray(parsedEnvironment.data.REA_REFERENCE_SECRET_PATTERNS_JSON, "REA_REFERENCE_SECRET_PATTERNS_JSON");
89
137
  if (!secretPatterns.ok)
90
138
  return secretPatterns;
139
+ const permissionCeilings = [
140
+ ...(parsedEnvironment.data.REA_PROCESS_CAPTURE_ENABLED === "true"
141
+ ? [
142
+ permissionScope("process_capture", workingRoots.value, {
143
+ executables: executableRoots.value,
144
+ environment_names: allowedEnvironment.value,
145
+ network: parsedEnvironment.data.REA_PROCESS_ALLOW_EXTERNAL_NETWORK ===
146
+ "true"
147
+ ? "external"
148
+ : "none",
149
+ }),
150
+ ]
151
+ : []),
152
+ permissionScope("evidence_read", evidenceRoots.value),
153
+ permissionScope("evidence_write", evidenceRoots.value),
154
+ permissionScope("investigation_input", investigationInputRoots.value),
155
+ permissionScope("investigation_workspace_read", evidenceRoots.value),
156
+ permissionScope("investigation_workspace_write", evidenceRoots.value),
157
+ permissionScope("snapshot_read", analysisSnapshotRoots.value),
158
+ permissionScope("snapshot_write", analysisSnapshotRoots.value),
159
+ permissionScope("artifact_extract", ["/"]),
160
+ permissionScope("reference_read", referenceRoots.value),
161
+ ...(parsedEnvironment.data.REA_ARTIFACT_NATIVE_MOUNT_ENABLED === "true"
162
+ ? [permissionScope("native_mount", [], { mount: true })]
163
+ : []),
164
+ ];
91
165
  return ok({
92
166
  hopperLauncherPath: parsedEnvironment.data.HOPPER_LAUNCHER_PATH ??
93
167
  defaultHopperLauncherPath(),
94
168
  hopperTargetPath: parsedEnvironment.data.HOPPER_TARGET_PATH,
95
169
  hopperTargetKind: parsedEnvironment.data.HOPPER_TARGET_KIND,
96
- hopperLoaderArgs: parsedArgs.data,
170
+ hopperLoaderArgs: loaderArgs.value,
97
171
  logLevel: parsedEnvironment.data.REA_LOG_LEVEL,
98
172
  processExecutionPolicy: {
99
173
  enabled: parsedEnvironment.data.REA_PROCESS_CAPTURE_ENABLED === "true",
@@ -103,13 +177,10 @@ export const parseConfig = (environment) => {
103
177
  allowExternalNetwork: parsedEnvironment.data.REA_PROCESS_ALLOW_EXTERNAL_NETWORK === "true",
104
178
  },
105
179
  artifactNativeMountEnabled: parsedEnvironment.data.REA_ARTIFACT_NATIVE_MOUNT_ENABLED === "true",
106
- evidenceFilePolicy: {
107
- roots: evidenceRoots.value,
108
- maxBytes: 64 * 1024 * 1024,
109
- maxDepth: 64,
110
- maxStringLength: 1024 * 1024,
111
- maxNodes: 1_000_000,
112
- },
180
+ artifactIntegrityContinueEnabled: parsedEnvironment.data.REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED === "true",
181
+ evidenceFilePolicy: filePolicy(evidenceRoots.value),
182
+ investigationInputRoots: investigationInputRoots.value,
183
+ analysisSnapshotFilePolicy: filePolicy(analysisSnapshotRoots.value),
113
184
  referenceSourcePolicy: {
114
185
  roots: referenceRoots.value,
115
186
  secretPatterns: secretPatterns.value,
@@ -118,5 +189,9 @@ export const parseConfig = (environment) => {
118
189
  maxDepth: 32,
119
190
  maxPathBytes: 4_096,
120
191
  },
192
+ permissionCeilings,
193
+ administratorPermissionGrants: administratorGrants(permissionCeilings),
194
+ permissionProjectRoot: parsedEnvironment.data.REA_PERMISSION_PROJECT_ROOT,
195
+ permissionProjectStore: parsedEnvironment.data.REA_PERMISSION_PROJECT_STORE,
121
196
  });
122
197
  };
@@ -28,10 +28,23 @@ const traversalLimitsInput = {
28
28
  max_path_bytes: z.number().int().min(1).max(65_535).default(4_096),
29
29
  };
30
30
  /** Exact caller boundary for deterministic artifact inventory. */
31
- export const artifactInventoryInputSchema = z.object({
31
+ export const artifactInventoryInputSchema = z
32
+ .object({
32
33
  native_mount_approved: z.boolean().default(false),
34
+ integrity_policy: z.enum(["fail", "record-and-continue"]).default("fail"),
35
+ integrity_continue_approved: z.boolean().default(false),
36
+ max_integrity_mismatches: z.number().int().min(1).max(100).default(10),
33
37
  ...pageInput,
34
38
  ...traversalLimitsInput,
39
+ })
40
+ .superRefine((input, context) => {
41
+ if (input.integrity_policy === "record-and-continue" &&
42
+ input.integrity_continue_approved !== true)
43
+ context.addIssue({
44
+ code: "custom",
45
+ path: ["integrity_continue_approved"],
46
+ message: "record-and-continue requires explicit approval",
47
+ });
35
48
  });
36
49
  /** Exact caller boundary for approved artifact extraction. */
37
50
  export const artifactExtractionInputSchema = z.object({