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