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.
- package/README.md +21 -0
- package/bridge/hopper_bridge.py +12 -3
- package/dist/application/ArtifactGraphConstruction.js +1 -1
- package/dist/application/ArtifactInventory.js +98 -26
- package/dist/application/AuthorizedArtifactInventory.js +2 -2
- package/dist/application/BinarySession.js +83 -0
- package/dist/application/CapabilityInventory.js +127 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/CrossVersionInventory.js +19 -8
- package/dist/application/CrossVersionInvestigation.js +68 -5
- package/dist/application/DirectAnalysis.js +97 -9
- package/dist/application/Doctor.js +74 -2
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureError.js +13 -0
- package/dist/application/ProcessCaptureLifecycle.js +3 -1
- package/dist/application/ProcessCli.js +23 -10
- package/dist/application/ProcessHarness.js +4 -1
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +110 -0
- package/dist/application/Setup.js +2 -41
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/Uninstall.js +1 -1
- package/dist/application/Upgrade.js +2 -0
- package/dist/application/runtime.js +1 -1
- package/dist/artifacts/ArtifactProvider.js +13 -3
- package/dist/catalogIdentity.js +121 -0
- package/dist/cli.js +46 -3
- package/dist/cliEvidenceCommands.js +42 -1
- package/dist/cliInvestigationCommands.js +57 -6
- package/dist/cliLogging.js +20 -3
- package/dist/cliPolicyCommands.js +115 -0
- package/dist/cliProcessCommands.js +4 -14
- package/dist/config.js +63 -3
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/errorSchemas.js +98 -0
- package/dist/contracts/toolContracts.js +10 -1
- package/dist/contracts/toolOutputSchemas.js +62 -30
- package/dist/domain/artifactComparison.js +38 -11
- package/dist/domain/artifactGraph.js +25 -1
- package/dist/domain/artifactInventoryEvidence.js +18 -0
- package/dist/domain/changedBehavior.js +5 -3
- package/dist/domain/errors.js +216 -3
- package/dist/domain/investigationWorkspace.js +131 -4
- package/dist/domain/permissionPolicy.js +157 -0
- package/dist/domain/processComparison.js +1 -0
- package/dist/domain/reconstructionVerification.js +1 -1
- package/dist/domain/reconstructionVerificationSchemas.js +1 -0
- package/dist/domain/staticRuntimeCorrelation.js +5 -1
- package/dist/generatedPackageMetadata.js +9 -0
- package/dist/hopper/BridgeLauncher.js +7 -3
- package/dist/hopper/HopperClient.js +32 -5
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +95 -7
- package/dist/server/createServer.js +52 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +18 -1
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +58 -1
- package/dist/server/registerFunctionComparisonTool.js +6 -2
- package/dist/server/registerInvestigationTools.js +83 -10
- package/dist/server/registerOfficialTools.js +19 -1
- package/dist/server/registerProcessComparisonTool.js +7 -3
- package/dist/server/registerSessionStatusTool.js +44 -3
- package/dist/server/registerSessionTools.js +129 -7
- package/dist/server/runDerivedOperation.js +35 -0
- package/dist/server/toolResult.js +27 -3
- package/dist/serverIdentity.js +70 -0
- 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:
|
|
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(
|
|
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
|
|
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, {
|
|
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
|
+
});
|
package/dist/cliLogging.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
2
|
-
import { captureProcessScenarioFile, compareProcessEvidenceFiles,
|
|
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", () =>
|
|
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", () =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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({
|