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
package/dist/cli.js CHANGED
@@ -14,8 +14,10 @@ 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";
19
21
  /**
20
22
  * Build the one-shot Incur CLI without starting Hopper at import time.
21
23
  * Analysis commands acquire and close their own sessions; bare `mcp` and
@@ -26,7 +28,7 @@ export const createCli = () => {
26
28
  ? "silent"
27
29
  : parseLogLevel(process.env.REA_LOG_LEVEL));
28
30
  const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, {
29
- version: process.env.REA_PACKAGE_VERSION ?? "0.0.0-development",
31
+ version: PRODUCT_IDENTITY.packageVersion,
30
32
  description: "Reverse engineer anything from your terminal or agent.",
31
33
  mcp: {
32
34
  command: PRODUCT_IDENTITY.mcpCommand,
@@ -55,6 +57,7 @@ export const createCli = () => {
55
57
  registerEvidenceCommands(cli, logger);
56
58
  registerReferenceSourceCommand(cli, logger);
57
59
  registerProcessCommands(cli, logger);
60
+ registerPolicyCommands(cli, logger);
58
61
  return cli;
59
62
  };
60
63
  const registerCoreCommands = (cli, logger) => {
@@ -135,7 +138,7 @@ const registerSetupCommands = (cli, logger) => {
135
138
  });
136
139
  cli.command("upgrade", {
137
140
  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")),
141
+ run: ({ formatExplicit }) => logCliCommand(logger, "upgrade", () => runUpgrade(PRODUCT_IDENTITY.packageVersion, systemUpgradeHost(), formatExplicit ? "structured" : "human")),
139
142
  });
140
143
  };
141
144
  const confirmSetup = async (actions) => {
@@ -270,6 +273,32 @@ const registerReferenceSourceCommand = (cli, logger) => {
270
273
  error: "Import failed",
271
274
  ...projectAnalysisError(config.error),
272
275
  };
276
+ const authority = await loadConfiguredPermissionAuthority(config.value);
277
+ if (!authority.ok)
278
+ return {
279
+ error: "Import failed",
280
+ ...projectAnalysisError(authority.error),
281
+ };
282
+ const authorized = await authority.value.authorize({
283
+ capability: "reference_read",
284
+ roots: [args.root],
285
+ executables: [],
286
+ environment_names: [],
287
+ network: "none",
288
+ mount: false,
289
+ operation_identity: `import_reference_source:${args.root}`,
290
+ }, "read");
291
+ if (!authorized.ok) {
292
+ const error = authorized.error instanceof PermissionRequiredError
293
+ ? authorized.error
294
+ : new AnalysisProtocolError(authorized.error.message, {
295
+ cause: authorized.error,
296
+ });
297
+ return {
298
+ error: "Import failed",
299
+ ...projectAnalysisError(error),
300
+ };
301
+ }
273
302
  const imported = await importReferenceSource({
274
303
  root: args.root,
275
304
  caller: "rea-cli",
@@ -295,7 +324,17 @@ const registerArtifactCommands = (cli, logger) => {
295
324
  options: z.object({
296
325
  offset: z.number().int().min(0).default(0),
297
326
  limit: z.number().int().min(1).max(500).default(100),
327
+ integrityPolicy: z.enum(["fail", "record-and-continue"]).default("fail"),
328
+ integrityContinueApproved: z.boolean().default(false),
329
+ maxIntegrityMismatches: z.number().int().min(1).max(100).default(10),
330
+ nativeMountApproved: z.boolean().default(false),
298
331
  }),
332
+ alias: {
333
+ integrityPolicy: "integrity-policy",
334
+ integrityContinueApproved: "integrity-continue-approved",
335
+ maxIntegrityMismatches: "max-integrity-mismatches",
336
+ nativeMountApproved: "native-mount-approved",
337
+ },
299
338
  run: ({ args, options }) => logCliCommand(logger, "inventory-artifact", () => runProviderAnalysis(args.path, "inventory_artifact", {
300
339
  node_offset: options.offset,
301
340
  node_limit: options.limit,
@@ -303,6 +342,10 @@ const registerArtifactCommands = (cli, logger) => {
303
342
  occurrence_limit: options.limit,
304
343
  edge_offset: options.offset,
305
344
  edge_limit: options.limit,
345
+ integrity_policy: options.integrityPolicy,
346
+ integrity_continue_approved: options.integrityContinueApproved,
347
+ max_integrity_mismatches: options.maxIntegrityMismatches,
348
+ native_mount_approved: options.nativeMountApproved,
306
349
  }, logger)),
307
350
  });
308
351
  cli.command("extract-artifact", {
@@ -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,8 @@ 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 { AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "./domain/errors.js";
6
+ import { loadConfiguredPermissionAuthority } from "./application/PermissionConfiguration.js";
5
7
  const investigationOptionsSchema = z.object({
6
8
  yes: z
7
9
  .boolean()
@@ -24,6 +26,9 @@ const investigationOptionsSchema = z.object({
24
26
  .default(268_435_456),
25
27
  pageSize: z.number().int().min(1).max(500).default(500),
26
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),
27
32
  });
28
33
  /** Register the persistent cross-version CLI workflow. */
29
34
  export const registerInvestigationCommands = (cli, logger) => {
@@ -43,6 +48,9 @@ export const registerInvestigationCommands = (cli, logger) => {
43
48
  maxEntryBytes: "max-entry-bytes",
44
49
  pageSize: "page-size",
45
50
  changeLimit: "change-limit",
51
+ integrityPolicy: "integrity-policy",
52
+ integrityContinueApproved: "integrity-continue-approved",
53
+ maxIntegrityMismatches: "max-integrity-mismatches",
46
54
  yes: "y",
47
55
  },
48
56
  run: ({ args, options }) => logCliCommand(logger, "investigate-versions", () => runInvestigation(args, options)),
@@ -56,7 +64,43 @@ const runInvestigation = async (args, options) => {
56
64
  };
57
65
  const config = parseConfig(process.env);
58
66
  if (!config.ok)
59
- return { error: config.error._tag, message: config.error.message };
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
+ }
60
104
  const investigated = await runCrossVersionInvestigation({
61
105
  approved: true,
62
106
  workspace_path: args.workspacePath,
@@ -66,6 +110,9 @@ const runInvestigation = async (args, options) => {
66
110
  : { expected_workspace_revision: options.expectedRevision }),
67
111
  left_path: args.leftPath,
68
112
  right_path: args.rightPath,
113
+ integrity_policy: options.integrityPolicy,
114
+ integrity_continue_approved: options.integrityContinueApproved,
115
+ max_integrity_mismatches: options.maxIntegrityMismatches,
69
116
  options: {
70
117
  max_entries: options.maxEntries,
71
118
  max_total_bytes: options.maxTotalBytes,
@@ -76,11 +123,15 @@ const runInvestigation = async (args, options) => {
76
123
  page_size: options.pageSize,
77
124
  change_limit: options.changeLimit,
78
125
  },
79
- }, config.value.evidenceFilePolicy, { inputRoots: config.value.investigationInputRoots });
126
+ }, config.value.evidenceFilePolicy, {
127
+ inputRoots: config.value.investigationInputRoots,
128
+ integrityContinueEnabled: config.value.artifactIntegrityContinueEnabled,
129
+ });
80
130
  return investigated.ok
81
131
  ? investigated.value.evidence
82
- : {
83
- error: investigated.error._tag,
84
- message: investigated.error.message,
85
- };
132
+ : cliError(investigated.error);
86
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,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
+ });
@@ -1,22 +1,12 @@
1
- import { Cli, Errors, z } from "incur";
2
- import { captureProcessScenarioFile, compareProcessEvidenceFiles, isProcessCliErrorOutput, } from "./application/ProcessCli.js";
1
+ import { Cli, z } from "incur";
2
+ import { captureProcessScenarioFile, compareProcessEvidenceFiles, } from "./application/ProcessCli.js";
3
3
  import { logCliCommand } from "./cliLogging.js";
4
- const requireProcessCommandSuccess = async (execute) => {
5
- const value = await execute();
6
- if (isProcessCliErrorOutput(value))
7
- throw new Errors.IncurError({
8
- code: "PROCESS_COMMAND_FAILED",
9
- message: value.message,
10
- retryable: false,
11
- });
12
- return value;
13
- };
14
4
  /** Register Process Capture v4 one-shot commands through shared application services. */
15
5
  export const registerProcessCommands = (cli, logger) => {
16
6
  cli.command("capture-process", {
17
7
  description: "Capture one approved Process Capture v4 JSON scenario",
18
8
  args: z.object({ scenario: z.string().describe("Scenario JSON path") }),
19
- run: ({ args }) => logCliCommand(logger, "capture-process", () => requireProcessCommandSuccess(() => captureProcessScenarioFile(args.scenario))),
9
+ run: ({ args }) => logCliCommand(logger, "capture-process", () => captureProcessScenarioFile(args.scenario)),
20
10
  });
21
11
  cli.command("compare-process-captures", {
22
12
  description: "Compare two Process Capture v4 Evidence JSON files",
@@ -24,6 +14,6 @@ export const registerProcessCommands = (cli, logger) => {
24
14
  left: z.string().describe("Left capture Evidence JSON path"),
25
15
  right: z.string().describe("Right capture Evidence JSON path"),
26
16
  }),
27
- run: ({ args }) => logCliCommand(logger, "compare-process-captures", () => requireProcessCommandSuccess(() => compareProcessEvidenceFiles(args.left, args.right))),
17
+ run: ({ args }) => logCliCommand(logger, "compare-process-captures", () => compareProcessEvidenceFiles(args.left, args.right)),
28
18
  });
29
19
  };
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"),
@@ -26,6 +34,12 @@ const environmentSchema = z.object({
26
34
  REA_ANALYSIS_SNAPSHOT_ROOTS_JSON: z.string().default("[]"),
27
35
  REA_REFERENCE_ROOTS_JSON: z.string().default("[]"),
28
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",
29
43
  });
30
44
  const parseStringArray = (encoded, name) => {
31
45
  try {
@@ -50,6 +64,21 @@ const filePolicy = (roots) => ({
50
64
  maxStringLength: 1024 * 1024,
51
65
  maxNodes: 1_000_000,
52
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
+ }));
53
82
  const parseLoaderArgs = (encoded) => {
54
83
  if (encoded === undefined)
55
84
  return ok([]);
@@ -107,6 +136,32 @@ export const parseConfig = (environment) => {
107
136
  const secretPatterns = parseStringArray(parsedEnvironment.data.REA_REFERENCE_SECRET_PATTERNS_JSON, "REA_REFERENCE_SECRET_PATTERNS_JSON");
108
137
  if (!secretPatterns.ok)
109
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
+ ];
110
165
  return ok({
111
166
  hopperLauncherPath: parsedEnvironment.data.HOPPER_LAUNCHER_PATH ??
112
167
  defaultHopperLauncherPath(),
@@ -122,6 +177,7 @@ export const parseConfig = (environment) => {
122
177
  allowExternalNetwork: parsedEnvironment.data.REA_PROCESS_ALLOW_EXTERNAL_NETWORK === "true",
123
178
  },
124
179
  artifactNativeMountEnabled: parsedEnvironment.data.REA_ARTIFACT_NATIVE_MOUNT_ENABLED === "true",
180
+ artifactIntegrityContinueEnabled: parsedEnvironment.data.REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED === "true",
125
181
  evidenceFilePolicy: filePolicy(evidenceRoots.value),
126
182
  investigationInputRoots: investigationInputRoots.value,
127
183
  analysisSnapshotFilePolicy: filePolicy(analysisSnapshotRoots.value),
@@ -133,5 +189,9 @@ export const parseConfig = (environment) => {
133
189
  maxDepth: 32,
134
190
  maxPathBytes: 4_096,
135
191
  },
192
+ permissionCeilings,
193
+ administratorPermissionGrants: administratorGrants(permissionCeilings),
194
+ permissionProjectRoot: parsedEnvironment.data.REA_PERMISSION_PROJECT_ROOT,
195
+ permissionProjectStore: parsedEnvironment.data.REA_PERMISSION_PROJECT_STORE,
136
196
  });
137
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({