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.
- package/README.md +95 -27
- package/bridge/hopper_bridge.py +172 -14
- package/dist/application/AnalysisSnapshotCache.js +131 -0
- package/dist/application/AnalysisSnapshotFiles.js +31 -0
- package/dist/application/ArtifactGraphConstruction.js +1 -1
- package/dist/application/ArtifactInventory.js +100 -24
- package/dist/application/AuthorizedArtifactInventory.js +16 -0
- package/dist/application/BinarySession.js +149 -36
- package/dist/application/BinarySessionPort.js +1 -0
- package/dist/application/BoundedJsonFiles.js +109 -0
- package/dist/application/CapabilityInventory.js +127 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/CrossVersionInventory.js +109 -0
- package/dist/application/CrossVersionInvestigation.js +358 -0
- package/dist/application/DirectAnalysis.js +205 -24
- package/dist/application/Doctor.js +104 -3
- package/dist/application/EvidenceBundleFiles.js +15 -108
- package/dist/application/EvidenceLedger.js +3 -2
- package/dist/application/InvestigationProviders.js +48 -0
- package/dist/application/InvestigationWorkspaceStore.js +214 -0
- package/dist/application/LinuxHopper.js +72 -48
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureAuthority.js +2 -2
- package/dist/application/ProcessCaptureError.js +25 -0
- package/dist/application/ProcessCaptureLifecycle.js +22 -2
- package/dist/application/ProcessCli.js +111 -31
- package/dist/application/ProcessHarness.js +6 -3
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +110 -0
- package/dist/application/ReferenceSourceImportEntries.js +18 -3
- package/dist/application/ReferenceSourceImportTypes.js +32 -0
- package/dist/application/Setup.js +35 -101
- package/dist/application/SetupClients.js +19 -0
- package/dist/application/SetupInstallFailure.js +53 -0
- package/dist/application/SetupPlan.js +31 -0
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/Uninstall.js +34 -24
- package/dist/application/UnknownEvidence.js +33 -0
- package/dist/application/Upgrade.js +3 -1
- package/dist/application/runtime.js +2 -2
- package/dist/artifacts/ArtifactProvider.js +15 -8
- package/dist/catalogIdentity.js +121 -0
- package/dist/cli.js +76 -13
- package/dist/cliEvidenceCommands.js +52 -12
- package/dist/cliInvestigationCommands.js +137 -0
- package/dist/cliLogging.js +20 -3
- package/dist/cliOutput.js +41 -0
- package/dist/cliPolicyCommands.js +115 -0
- package/dist/config.js +102 -27
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/errorSchemas.js +98 -0
- package/dist/contracts/promptContracts.js +256 -0
- package/dist/contracts/sessionLifecycleInputs.js +11 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +20 -7
- package/dist/contracts/toolOutputSchemas.js +70 -9
- package/dist/domain/analysisSnapshot.js +149 -0
- 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 +35 -10
- package/dist/domain/errors.js +384 -32
- package/dist/domain/evidence.js +4 -2
- package/dist/domain/evidenceBundle.js +28 -0
- package/dist/domain/hopperStartupFailure.js +55 -0
- package/dist/domain/investigationWorkspace.js +341 -0
- package/dist/domain/jsonValue.js +2 -0
- package/dist/domain/nativeInspection.js +3 -2
- package/dist/domain/permissionPolicy.js +157 -0
- package/dist/domain/processCapture.js +5 -4
- package/dist/domain/processComparison.js +6 -4
- 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 +51 -20
- package/dist/hopper/HopperClient.js +45 -3
- package/dist/hopper/HopperProvider.js +1 -0
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +139 -28
- package/dist/server/createServer.js +54 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/promptCompletion.js +144 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +20 -7
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +59 -8
- package/dist/server/registerFunctionComparisonTool.js +7 -3
- package/dist/server/registerInvestigationTools.js +111 -18
- package/dist/server/registerOfficialTools.js +25 -14
- package/dist/server/registerProcessComparisonTool.js +8 -10
- package/dist/server/registerPrompts.js +67 -0
- package/dist/server/registerSessionStatusTool.js +52 -0
- package/dist/server/registerSessionTools.js +178 -22
- package/dist/server/runDerivedOperation.js +35 -0
- package/dist/server/sessionEvidence.js +2 -8
- package/dist/server/sessionToolPolicies.js +1 -42
- package/dist/server/toolResult.js +34 -6
- package/dist/serverIdentity.js +70 -0
- package/install.sh +11 -11
- package/package.json +10 -6
- package/scripts/hopper-demo-x11.py +349 -0
- package/scripts/rea.mjs +6 -3
- 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
|
|
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
|
|
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
|
|
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
|
+
});
|
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,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
|
|
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"),
|
|
@@ -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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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({
|