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
|
@@ -8,13 +8,19 @@ import { scanAuthorizedArtifactInventory } from "./AuthorizedArtifactInventory.j
|
|
|
8
8
|
import { ARTIFACT_GRAPH_PROVIDER } from "./InvestigationProviders.js";
|
|
9
9
|
export const AUTOMATIC_RUN_LIMITATION = "Automatic cross-version runs compare shipped artifact structure only; they do not execute either version.";
|
|
10
10
|
/** Authorize and scan both inputs once under the same bounded traversal policy. */
|
|
11
|
-
export const scanVersions = async (input, inputRoots, signal) => {
|
|
11
|
+
export const scanVersions = async (input, inputRoots, signal, integrityContinueEnabled = false) => {
|
|
12
12
|
try {
|
|
13
13
|
if (signal?.aborted === true)
|
|
14
14
|
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
15
15
|
const limits = artifactLimits(input.options);
|
|
16
|
-
const
|
|
17
|
-
|
|
16
|
+
const integrity = {
|
|
17
|
+
mode: input.integrity_policy,
|
|
18
|
+
approved: input.integrity_continue_approved,
|
|
19
|
+
enabled: integrityContinueEnabled,
|
|
20
|
+
maxMismatches: input.max_integrity_mismatches,
|
|
21
|
+
};
|
|
22
|
+
const left = await scanAuthorizedArtifactInventory(input.left_path, inputRoots, limits, signal, integrity);
|
|
23
|
+
const right = await scanAuthorizedArtifactInventory(input.right_path, inputRoots, limits, signal, integrity);
|
|
18
24
|
return ok({ left, right });
|
|
19
25
|
}
|
|
20
26
|
catch (cause) {
|
|
@@ -25,8 +31,8 @@ export const scanVersions = async (input, inputRoots, signal) => {
|
|
|
25
31
|
};
|
|
26
32
|
/** Project complete snapshots into bounded, comparable Evidence page sets. */
|
|
27
33
|
export const createInventoryEvidencePages = (input, snapshots) => {
|
|
28
|
-
const left = inventoryPages(input.left_path, snapshots.left, input
|
|
29
|
-
const right = inventoryPages(input.right_path, snapshots.right, input
|
|
34
|
+
const left = inventoryPages(input.left_path, snapshots.left, input);
|
|
35
|
+
const right = inventoryPages(input.right_path, snapshots.right, input);
|
|
30
36
|
if (left.length > 100 || right.length > 100)
|
|
31
37
|
return err(new ArtifactOperationError("inventory_artifact", "limit"));
|
|
32
38
|
return ok({ left, right });
|
|
@@ -46,11 +52,13 @@ export const runLimitations = (snapshots) => [
|
|
|
46
52
|
AUTOMATIC_RUN_LIMITATION,
|
|
47
53
|
]),
|
|
48
54
|
].sort((left, right) => left.localeCompare(right));
|
|
49
|
-
const inventoryPages = (path, snapshot,
|
|
55
|
+
const inventoryPages = (path, snapshot, input) => {
|
|
56
|
+
const options = input.options;
|
|
50
57
|
const count = Math.max(1, Math.ceil(snapshot.nodes.length / options.page_size), Math.ceil(snapshot.occurrences.length / options.page_size), Math.ceil(snapshot.edges.length / options.page_size));
|
|
51
|
-
return Array.from({ length: count }, (_, index) => inventoryPage(path, snapshot,
|
|
58
|
+
return Array.from({ length: count }, (_, index) => inventoryPage(path, snapshot, input, index * options.page_size));
|
|
52
59
|
};
|
|
53
|
-
const inventoryPage = (path, snapshot,
|
|
60
|
+
const inventoryPage = (path, snapshot, input, offset) => {
|
|
61
|
+
const options = input.options;
|
|
54
62
|
const result = paginateArtifactInventory(snapshot, {
|
|
55
63
|
nodeOffset: offset,
|
|
56
64
|
nodeLimit: options.page_size,
|
|
@@ -78,6 +86,9 @@ const inventoryPage = (path, snapshot, options, offset) => {
|
|
|
78
86
|
max_compression_ratio: options.max_compression_ratio,
|
|
79
87
|
max_depth: options.max_depth,
|
|
80
88
|
max_path_bytes: options.max_path_bytes,
|
|
89
|
+
integrity_policy: input.integrity_policy,
|
|
90
|
+
integrity_continue_approved: input.integrity_continue_approved,
|
|
91
|
+
max_integrity_mismatches: input.max_integrity_mismatches,
|
|
81
92
|
},
|
|
82
93
|
result: jsonValueSchema.parse(result),
|
|
83
94
|
rawResult: null,
|
|
@@ -2,8 +2,8 @@ import { compareArtifacts } from "../domain/artifactComparison.js";
|
|
|
2
2
|
import { findChangedBehavior } from "../domain/changedBehavior.js";
|
|
3
3
|
import { createEvidence } from "../domain/evidence.js";
|
|
4
4
|
import { createEvidenceBundle, } from "../domain/evidenceBundle.js";
|
|
5
|
-
import { AnalysisInputError, EvidenceIntegrityError, InvestigationWorkspaceError, } from "../domain/errors.js";
|
|
6
|
-
import { createInvestigationRunIdentity, createInvestigationWorkspace, crossVersionInvestigationInputSchema, investigationRunSchema, investigationRunSummarySchema, reviseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
|
|
5
|
+
import { AnalysisCancelledError, AnalysisInputError, EvidenceIntegrityError, InvestigationWorkspaceError, } from "../domain/errors.js";
|
|
6
|
+
import { createLegacyInvestigationRunIdentity, createInvestigationRunIdentity, createInvestigationWorkspace, crossVersionInvestigationInputSchema, investigationRunSchema, investigationRunSummarySchema, reviseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
|
|
7
7
|
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
8
8
|
import { err, ok } from "../domain/result.js";
|
|
9
9
|
import { AUTOMATIC_RUN_LIMITATION, createInventoryEvidencePages, runLimitations, scanVersions, targetFor, } from "./CrossVersionInventory.js";
|
|
@@ -22,15 +22,31 @@ export const runCrossVersionInvestigation = async (input, policy, execution) =>
|
|
|
22
22
|
const preflight = validateWorkspaceRequest(initial.value, parsed.data);
|
|
23
23
|
if (!preflight.ok)
|
|
24
24
|
return preflight;
|
|
25
|
-
|
|
25
|
+
if (isCancelled(execution.signal))
|
|
26
|
+
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
27
|
+
await execution.progress?.report({
|
|
28
|
+
phase: "scan_versions",
|
|
29
|
+
completed: 0,
|
|
30
|
+
total: 4,
|
|
31
|
+
message: "Scanning both version inputs",
|
|
32
|
+
});
|
|
33
|
+
const snapshots = await scanVersions(parsed.data, execution.inputRoots, execution.signal, execution.integrityContinueEnabled);
|
|
26
34
|
if (!snapshots.ok)
|
|
27
35
|
return snapshots;
|
|
36
|
+
await execution.progress?.report({
|
|
37
|
+
phase: "scan_versions",
|
|
38
|
+
completed: 1,
|
|
39
|
+
total: 4,
|
|
40
|
+
message: "Version inputs scanned",
|
|
41
|
+
});
|
|
28
42
|
return continueInvestigation({
|
|
29
43
|
input: parsed.data,
|
|
30
44
|
initial: initial.value,
|
|
31
45
|
snapshots: snapshots.value,
|
|
32
46
|
policy,
|
|
33
47
|
session: execution.session,
|
|
48
|
+
signal: execution.signal,
|
|
49
|
+
progress: execution.progress,
|
|
34
50
|
});
|
|
35
51
|
};
|
|
36
52
|
const continueInvestigation = async (context) => {
|
|
@@ -40,13 +56,29 @@ const continueInvestigation = async (context) => {
|
|
|
40
56
|
left,
|
|
41
57
|
right,
|
|
42
58
|
options: context.input.options,
|
|
59
|
+
integrity_policy: context.input.integrity_policy,
|
|
60
|
+
integrity_continue_approved: context.input.integrity_continue_approved,
|
|
61
|
+
max_integrity_mismatches: context.input.max_integrity_mismatches,
|
|
43
62
|
});
|
|
44
|
-
const
|
|
63
|
+
const legacyIdentity = createLegacyInvestigationRunIdentity({
|
|
64
|
+
left,
|
|
65
|
+
right,
|
|
66
|
+
options: context.input.options,
|
|
67
|
+
});
|
|
68
|
+
const existing = context.initial?.runs.find((run) => run.run_id === identity.runId ||
|
|
69
|
+
(context.input.integrity_policy === "fail" &&
|
|
70
|
+
!context.input.integrity_continue_approved &&
|
|
71
|
+
context.input.max_integrity_mismatches === 10 &&
|
|
72
|
+
"legacy_request_identity" in run &&
|
|
73
|
+
run.max_integrity_mismatches === 10 &&
|
|
74
|
+
run.run_id === legacyIdentity.runId));
|
|
45
75
|
if (context.initial !== null && existing?.status === "complete")
|
|
46
76
|
return completeOutcome(context.initial, existing, true, context.session);
|
|
47
77
|
const pages = createInventoryEvidencePages(context.input, context.snapshots);
|
|
48
78
|
if (!pages.ok)
|
|
49
79
|
return pages;
|
|
80
|
+
if (isCancelled(context.signal))
|
|
81
|
+
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
50
82
|
const inventoried = await ensureInventoryCheckpoint({
|
|
51
83
|
input: context.input,
|
|
52
84
|
current: context.initial,
|
|
@@ -57,11 +89,36 @@ const continueInvestigation = async (context) => {
|
|
|
57
89
|
});
|
|
58
90
|
if (!inventoried.ok)
|
|
59
91
|
return inventoried;
|
|
92
|
+
await context.progress?.report({
|
|
93
|
+
phase: "inventory_versions",
|
|
94
|
+
completed: 2,
|
|
95
|
+
total: 4,
|
|
96
|
+
message: "Both artifact inventories checkpointed",
|
|
97
|
+
});
|
|
98
|
+
if (isCancelled(context.signal))
|
|
99
|
+
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
60
100
|
const compared = await ensureComparisonCheckpoint(context.input, inventoried.value, context.policy);
|
|
61
101
|
if (!compared.ok)
|
|
62
102
|
return compared;
|
|
63
|
-
|
|
103
|
+
await context.progress?.report({
|
|
104
|
+
phase: "compare_artifacts",
|
|
105
|
+
completed: 3,
|
|
106
|
+
total: 4,
|
|
107
|
+
message: "Artifact comparison checkpointed",
|
|
108
|
+
});
|
|
109
|
+
if (isCancelled(context.signal))
|
|
110
|
+
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
111
|
+
const finalized = await finalizeInvestigation(context.input, compared.value, context.policy, context.session);
|
|
112
|
+
await context.progress?.report({
|
|
113
|
+
phase: "changed_behavior",
|
|
114
|
+
completed: 4,
|
|
115
|
+
total: 4,
|
|
116
|
+
message: finalized.ok ? "Investigation completed" : "Investigation failed",
|
|
117
|
+
terminal: true,
|
|
118
|
+
});
|
|
119
|
+
return finalized;
|
|
64
120
|
};
|
|
121
|
+
const isCancelled = (signal) => signal?.aborted === true;
|
|
65
122
|
const ensureInventoryCheckpoint = async (input) => {
|
|
66
123
|
if (input.existing !== undefined) {
|
|
67
124
|
const consistent = validateInventoryCheckpoint(input.existing, input.pages);
|
|
@@ -77,6 +134,9 @@ const ensureInventoryCheckpoint = async (input) => {
|
|
|
77
134
|
left,
|
|
78
135
|
right,
|
|
79
136
|
options: input.input.options,
|
|
137
|
+
integrity_policy: input.input.integrity_policy,
|
|
138
|
+
integrity_continue_approved: input.input.integrity_continue_approved,
|
|
139
|
+
max_integrity_mismatches: input.input.max_integrity_mismatches,
|
|
80
140
|
});
|
|
81
141
|
const run = investigationRunSchema.parse({
|
|
82
142
|
schema_version: 1,
|
|
@@ -85,6 +145,9 @@ const ensureInventoryCheckpoint = async (input) => {
|
|
|
85
145
|
left,
|
|
86
146
|
right,
|
|
87
147
|
options: input.input.options,
|
|
148
|
+
integrity_policy: input.input.integrity_policy,
|
|
149
|
+
integrity_continue_approved: input.input.integrity_continue_approved,
|
|
150
|
+
max_integrity_mismatches: input.input.max_integrity_mismatches,
|
|
88
151
|
status: "running",
|
|
89
152
|
completed_stages: ["inventory_left", "inventory_right"],
|
|
90
153
|
left_inventory_evidence_ids: evidenceIds(input.pages.left),
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { parseConfig } from "../config.js";
|
|
2
|
+
import { jsonObjectSchema } from "../domain/jsonValue.js";
|
|
3
|
+
import { createServerIdentity } from "../serverIdentity.js";
|
|
2
4
|
import { EnhancedTools } from "./EnhancedTools.js";
|
|
3
5
|
import { createBinarySession } from "./runtime.js";
|
|
4
6
|
import { silentLogger } from "../logger.js";
|
|
5
7
|
import { createEvidence } from "../domain/evidence.js";
|
|
6
|
-
import { EvidenceIntegrityError, projectAnalysisError, } from "../domain/errors.js";
|
|
8
|
+
import { EvidenceIntegrityError, AnalysisProtocolError, PermissionRequiredError, projectAnalysisError, } from "../domain/errors.js";
|
|
7
9
|
import { access } from "node:fs/promises";
|
|
8
10
|
import { readAnalysisSnapshot, writeAnalysisSnapshot, } from "./AnalysisSnapshotFiles.js";
|
|
9
11
|
import { parseBinaryTarget } from "../domain/binaryTarget.js";
|
|
10
12
|
import { snapshotEvidenceForQuery, snapshotMatchesTarget, } from "../domain/analysisSnapshot.js";
|
|
11
13
|
import { err, ok } from "../domain/result.js";
|
|
14
|
+
import { loadConfiguredPermissionAuthority } from "./PermissionConfiguration.js";
|
|
12
15
|
const WORKFLOW_PROVIDER = {
|
|
13
16
|
id: "rea-workflow",
|
|
14
17
|
name: "REA composed investigation workflow",
|
|
@@ -19,12 +22,17 @@ const WORKFLOW_PROVIDER = {
|
|
|
19
22
|
* Unlike MCP mode, every CLI invocation is intentionally isolated and does not
|
|
20
23
|
* retain a target or bridge resources for a subsequent command.
|
|
21
24
|
*/
|
|
22
|
-
export const runDirectAnalysis = async (path, tool, arguments_, options = {}) => runAnalysis(path, tool, arguments_, {
|
|
25
|
+
export const runDirectAnalysis = async (path, tool, arguments_, options = {}) => withProcessCancellation(options.signal, (signal) => runAnalysis(path, tool, arguments_, {
|
|
23
26
|
logger: options.logger ?? silentLogger,
|
|
24
27
|
snapshotPath: options.snapshotPath,
|
|
25
|
-
|
|
28
|
+
signal,
|
|
29
|
+
}));
|
|
26
30
|
/** Execute one provider-native semantic operation with atomic provenance. */
|
|
27
|
-
export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger) => runAnalysis(path, tool, arguments_, {
|
|
31
|
+
export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger, signal) => withProcessCancellation(signal, (operationSignal) => runAnalysis(path, tool, arguments_, {
|
|
32
|
+
logger,
|
|
33
|
+
snapshotPath: undefined,
|
|
34
|
+
signal: operationSignal,
|
|
35
|
+
}));
|
|
28
36
|
/** Describe configured providers without opening a target or launching Hopper. */
|
|
29
37
|
export const runSessionStatus = async (logger = silentLogger) => {
|
|
30
38
|
const config = parseConfig(process.env);
|
|
@@ -32,17 +40,84 @@ export const runSessionStatus = async (logger = silentLogger) => {
|
|
|
32
40
|
return cliError(config.error);
|
|
33
41
|
const session = createBinarySession(config.value, logger);
|
|
34
42
|
try {
|
|
35
|
-
return
|
|
43
|
+
return {
|
|
44
|
+
...jsonObjectSchema.parse(session.status()),
|
|
45
|
+
server_identity: createServerIdentity({
|
|
46
|
+
startedAt: new Date().toISOString(),
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
36
49
|
}
|
|
37
50
|
finally {
|
|
38
51
|
await session.close();
|
|
39
52
|
}
|
|
40
53
|
};
|
|
54
|
+
const authorizeAnalysis = async (authority, tool, arguments_, snapshotPath) => {
|
|
55
|
+
const requests = [];
|
|
56
|
+
if (snapshotPath !== undefined) {
|
|
57
|
+
requests.push({
|
|
58
|
+
capability: "snapshot_read",
|
|
59
|
+
path: snapshotPath,
|
|
60
|
+
access: "write",
|
|
61
|
+
}, {
|
|
62
|
+
capability: "snapshot_write",
|
|
63
|
+
path: snapshotPath,
|
|
64
|
+
access: "write",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (tool === "extract_artifact" && typeof arguments_.output_root === "string")
|
|
68
|
+
requests.push({
|
|
69
|
+
capability: "artifact_extract",
|
|
70
|
+
path: arguments_.output_root,
|
|
71
|
+
access: "write",
|
|
72
|
+
});
|
|
73
|
+
for (const request of requests) {
|
|
74
|
+
const result = await authority.authorize({
|
|
75
|
+
capability: request.capability,
|
|
76
|
+
roots: [request.path],
|
|
77
|
+
executables: [],
|
|
78
|
+
environment_names: [],
|
|
79
|
+
network: "none",
|
|
80
|
+
mount: false,
|
|
81
|
+
operation_identity: `${tool}:${request.capability}:${request.path}`,
|
|
82
|
+
}, request.access);
|
|
83
|
+
if (!result.ok)
|
|
84
|
+
return err(result.error instanceof PermissionRequiredError
|
|
85
|
+
? result.error
|
|
86
|
+
: new AnalysisProtocolError(result.error.message, {
|
|
87
|
+
cause: result.error,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
if (tool === "inventory_artifact" &&
|
|
91
|
+
arguments_.native_mount_approved === true) {
|
|
92
|
+
const result = await authority.authorize({
|
|
93
|
+
capability: "native_mount",
|
|
94
|
+
roots: [],
|
|
95
|
+
executables: [],
|
|
96
|
+
environment_names: [],
|
|
97
|
+
network: "none",
|
|
98
|
+
mount: true,
|
|
99
|
+
operation_identity: "inventory_artifact:native_mount",
|
|
100
|
+
}, "read");
|
|
101
|
+
if (!result.ok)
|
|
102
|
+
return err(result.error instanceof PermissionRequiredError
|
|
103
|
+
? result.error
|
|
104
|
+
: new AnalysisProtocolError(result.error.message, {
|
|
105
|
+
cause: result.error,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
return ok(null);
|
|
109
|
+
};
|
|
41
110
|
const runAnalysis = async (path, tool, arguments_, options) => {
|
|
42
|
-
const { logger, snapshotPath } = options;
|
|
111
|
+
const { logger, signal, snapshotPath } = options;
|
|
43
112
|
const config = parseConfig(process.env);
|
|
44
113
|
if (!config.ok)
|
|
45
114
|
return cliError(config.error);
|
|
115
|
+
const permissionAuthority = await loadConfiguredPermissionAuthority(config.value);
|
|
116
|
+
if (!permissionAuthority.ok)
|
|
117
|
+
return cliError(permissionAuthority.error);
|
|
118
|
+
const authorized = await authorizeAnalysis(permissionAuthority.value, tool, arguments_, snapshotPath);
|
|
119
|
+
if (!authorized.ok)
|
|
120
|
+
return cliError(authorized.error);
|
|
46
121
|
const unavailable = snapshotUnavailable(snapshotPath, config.value.hopperLoaderArgs);
|
|
47
122
|
if (unavailable !== undefined)
|
|
48
123
|
return cliError(unavailable);
|
|
@@ -61,7 +136,7 @@ const runAnalysis = async (path, tool, arguments_, options) => {
|
|
|
61
136
|
if (prepared.value.evidence !== undefined)
|
|
62
137
|
return prepared.value.evidence;
|
|
63
138
|
const { snapshot } = prepared.value;
|
|
64
|
-
const opened = await session.open(path, snapshot === undefined ? {} : { snapshot });
|
|
139
|
+
const opened = await session.open(path, snapshot === undefined ? { signal } : { signal, snapshot });
|
|
65
140
|
if (!opened.ok)
|
|
66
141
|
return cliError(opened.error);
|
|
67
142
|
let output;
|
|
@@ -69,7 +144,7 @@ const runAnalysis = async (path, tool, arguments_, options) => {
|
|
|
69
144
|
if (tool === "binary_overview" ||
|
|
70
145
|
tool === "analyze_function" ||
|
|
71
146
|
tool === "trace_feature") {
|
|
72
|
-
const result = await new EnhancedTools(session).execute(tool, arguments_);
|
|
147
|
+
const result = await new EnhancedTools(session).execute(tool, arguments_, signal);
|
|
73
148
|
if (!result.ok)
|
|
74
149
|
output = cliError(result.error);
|
|
75
150
|
else {
|
|
@@ -86,7 +161,7 @@ const runAnalysis = async (path, tool, arguments_, options) => {
|
|
|
86
161
|
}
|
|
87
162
|
}
|
|
88
163
|
else {
|
|
89
|
-
const result = await session.execute(tool, arguments_);
|
|
164
|
+
const result = await session.execute(tool, arguments_, { signal });
|
|
90
165
|
if (!result.ok)
|
|
91
166
|
output = cliError(result.error);
|
|
92
167
|
else {
|
|
@@ -117,6 +192,19 @@ const runAnalysis = async (path, tool, arguments_, options) => {
|
|
|
117
192
|
await session.close();
|
|
118
193
|
}
|
|
119
194
|
};
|
|
195
|
+
const withProcessCancellation = async (suppliedSignal, operation) => {
|
|
196
|
+
if (suppliedSignal !== undefined)
|
|
197
|
+
return operation(suppliedSignal);
|
|
198
|
+
const controller = new AbortController();
|
|
199
|
+
const cancel = () => controller.abort();
|
|
200
|
+
process.once("SIGINT", cancel);
|
|
201
|
+
try {
|
|
202
|
+
return await operation(controller.signal);
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
process.off("SIGINT", cancel);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
120
208
|
const prepareSnapshot = async (options) => {
|
|
121
209
|
const { path, tool, arguments: arguments_, snapshotPath, policy, provider, } = options;
|
|
122
210
|
if (snapshotPath === undefined || !(await fileExists(snapshotPath)))
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { access } from "node:fs/promises";
|
|
3
|
-
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { access, readFile, readdir } from "node:fs/promises";
|
|
4
3
|
import { homedir } from "node:os";
|
|
5
4
|
import { join } from "node:path";
|
|
6
5
|
import { execFile } from "node:child_process";
|
|
@@ -9,6 +8,9 @@ import { parseBinaryTarget } from "../domain/binaryTarget.js";
|
|
|
9
8
|
import { supportsNodeVersion } from "../domain/runtimeVersion.js";
|
|
10
9
|
import { probeHomebrew } from "./homebrew.js";
|
|
11
10
|
import { linuxHopperBinarySupported, linuxHopperLauncherPath, linuxSharedLibrariesAvailable, readLinuxDistribution, } from "./LinuxHopper.js";
|
|
11
|
+
import { CATALOG_IDENTITY } from "../catalogIdentity.js";
|
|
12
|
+
import { PRODUCT_IDENTITY, SDK_IDENTITY } from "../identity.js";
|
|
13
|
+
import { readClientRegistrationStatuses, } from "./ClientRegistrationStatus.js";
|
|
12
14
|
const execFileAsync = promisify(execFile);
|
|
13
15
|
const DEFAULT_HOPPER = "/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
|
|
14
16
|
const SYSTEM_LINUX_HOPPER = "/opt/hopper/bin/Hopper";
|
|
@@ -68,10 +70,54 @@ export const runDoctor = async (target, host = systemDoctorHost()) => {
|
|
|
68
70
|
remediation: "Supply a readable local app or program path.",
|
|
69
71
|
classification: "config_drift",
|
|
70
72
|
}));
|
|
73
|
+
const installationPaths = (await host.installationPaths?.()) ?? [];
|
|
74
|
+
const installedSkillVersion = await host.installedSkillVersion?.();
|
|
75
|
+
const registrations = (await host.clientRegistrations?.()) ?? [];
|
|
76
|
+
for (const registration of registrations)
|
|
77
|
+
if (registration.state !== "aligned")
|
|
78
|
+
checks.push({
|
|
79
|
+
name: `registration:${registration.client}`,
|
|
80
|
+
ok: false,
|
|
81
|
+
classification: "config_drift",
|
|
82
|
+
detail: registration.config_path,
|
|
83
|
+
remediation: registration.remediation ??
|
|
84
|
+
"Run rea setup, then restart the affected client.",
|
|
85
|
+
});
|
|
71
86
|
return {
|
|
72
87
|
healthy: checks.every(({ ok }) => ok),
|
|
73
88
|
...(hopperPath === undefined ? {} : { hopperPath }),
|
|
74
89
|
checks,
|
|
90
|
+
identity: {
|
|
91
|
+
cli_package_version: PRODUCT_IDENTITY.packageVersion,
|
|
92
|
+
expected_skill_version: PRODUCT_IDENTITY.skillVersion,
|
|
93
|
+
sdk: SDK_IDENTITY,
|
|
94
|
+
catalog: CATALOG_IDENTITY,
|
|
95
|
+
live_server: {
|
|
96
|
+
state: "unknown",
|
|
97
|
+
remediation: "Compare rea://server/identity or binary_session from the active client; an on-disk registration cannot prove the running server version.",
|
|
98
|
+
},
|
|
99
|
+
installations: {
|
|
100
|
+
paths: installationPaths,
|
|
101
|
+
state: installationPaths.length === 0
|
|
102
|
+
? "unknown"
|
|
103
|
+
: installationPaths.length === 1
|
|
104
|
+
? "single"
|
|
105
|
+
: "multiple",
|
|
106
|
+
},
|
|
107
|
+
skill: {
|
|
108
|
+
installed_version: installedSkillVersion ?? null,
|
|
109
|
+
state: installedSkillVersion === undefined
|
|
110
|
+
? "unknown"
|
|
111
|
+
: installedSkillVersion === PRODUCT_IDENTITY.skillVersion
|
|
112
|
+
? "aligned"
|
|
113
|
+
: "stale",
|
|
114
|
+
remediation: installedSkillVersion === undefined ||
|
|
115
|
+
installedSkillVersion === PRODUCT_IDENTITY.skillVersion
|
|
116
|
+
? null
|
|
117
|
+
: "Run rea setup to update the installed REA skill.",
|
|
118
|
+
},
|
|
119
|
+
registrations,
|
|
120
|
+
},
|
|
75
121
|
};
|
|
76
122
|
};
|
|
77
123
|
const unsupportedHopperRemediation = (configured) => configured
|
|
@@ -157,8 +203,34 @@ export const systemDoctorHost = () => ({
|
|
|
157
203
|
}
|
|
158
204
|
return paths;
|
|
159
205
|
},
|
|
206
|
+
async installationPaths() {
|
|
207
|
+
try {
|
|
208
|
+
const command = process.platform === "win32" ? "where" : "which";
|
|
209
|
+
const arguments_ = process.platform === "win32" ? ["rea"] : ["-a", "rea"];
|
|
210
|
+
return uniqueLines((await execFileAsync(command, arguments_)).stdout);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
async installedSkillVersion() {
|
|
217
|
+
try {
|
|
218
|
+
const content = await readFile(join(homedir(), ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md"), "utf8");
|
|
219
|
+
return /^\s{2}version:\s*"([^"]+)"\s*$/mu.exec(content)?.[1];
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
clientRegistrations: () => readClientRegistrationStatuses(homedir()),
|
|
160
226
|
});
|
|
161
227
|
const parseMajor = (version) => Number.parseInt(version.split(".")[0] ?? "0", 10);
|
|
228
|
+
const uniqueLines = (value) => [
|
|
229
|
+
...new Set(value
|
|
230
|
+
.split(/\r?\n/u)
|
|
231
|
+
.map((line) => line.trim())
|
|
232
|
+
.filter((line) => line.length > 0)),
|
|
233
|
+
];
|
|
162
234
|
const check = (name, ok, detail, failure) => ({
|
|
163
235
|
name,
|
|
164
236
|
ok,
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { consumePermission, clearSessionPermissions, createPermissionPolicy, evaluatePermission, grantPermission, reloadPermissionCeilings, revokePermission, } from "../domain/permissionPolicy.js";
|
|
4
|
+
import { PermissionRequiredError } from "../domain/errors.js";
|
|
5
|
+
import { err, ok } from "../domain/result.js";
|
|
6
|
+
/** Failure to establish a filesystem object's canonical authorization identity. */
|
|
7
|
+
export class PermissionPathError extends Error {
|
|
8
|
+
path;
|
|
9
|
+
reason;
|
|
10
|
+
_tag = "PermissionPathError";
|
|
11
|
+
constructor(path, reason, options) {
|
|
12
|
+
super(`Cannot canonicalize permission path: ${path}`, options);
|
|
13
|
+
this.path = path;
|
|
14
|
+
this.reason = reason;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Canonicalize administrator ceilings before they become authorization state. */
|
|
18
|
+
export const canonicalizePermissionCeilings = async (ceilings) => {
|
|
19
|
+
const normalized = [];
|
|
20
|
+
for (const ceiling of ceilings) {
|
|
21
|
+
const canonical = await canonicalizeScope(ceiling, "read");
|
|
22
|
+
if (!canonical.ok)
|
|
23
|
+
return canonical;
|
|
24
|
+
normalized.push(canonical.value);
|
|
25
|
+
}
|
|
26
|
+
return ok(normalized);
|
|
27
|
+
};
|
|
28
|
+
/** Canonicalize one exact operation request before subset evaluation. */
|
|
29
|
+
export const canonicalizePermissionRequest = async (request, access) => {
|
|
30
|
+
const canonical = await canonicalizeScope(request, access);
|
|
31
|
+
return canonical.ok
|
|
32
|
+
? ok({ ...canonical.value, operation_identity: request.operation_identity })
|
|
33
|
+
: canonical;
|
|
34
|
+
};
|
|
35
|
+
const canonicalizeScope = async (scope, access) => {
|
|
36
|
+
const roots = await canonicalizePaths(scope.roots, access);
|
|
37
|
+
if (!roots.ok)
|
|
38
|
+
return roots;
|
|
39
|
+
const executables = await canonicalizePaths(scope.executables, "read");
|
|
40
|
+
if (!executables.ok)
|
|
41
|
+
return executables;
|
|
42
|
+
return ok({ ...scope, roots: roots.value, executables: executables.value });
|
|
43
|
+
};
|
|
44
|
+
const canonicalizePaths = async (paths, access) => {
|
|
45
|
+
const canonical = [];
|
|
46
|
+
for (const path of paths) {
|
|
47
|
+
const result = await canonicalizePath(path, access);
|
|
48
|
+
if (!result.ok)
|
|
49
|
+
return result;
|
|
50
|
+
canonical.push(result.value);
|
|
51
|
+
}
|
|
52
|
+
return ok(canonical);
|
|
53
|
+
};
|
|
54
|
+
const canonicalizePath = async (path, access) => {
|
|
55
|
+
const requested = resolve(path);
|
|
56
|
+
try {
|
|
57
|
+
return ok(await realpath(requested));
|
|
58
|
+
}
|
|
59
|
+
catch (cause) {
|
|
60
|
+
if (access !== "write" || !isNotFound(cause))
|
|
61
|
+
return err(new PermissionPathError(path, errorReason(cause), { cause }));
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const parent = await realpath(dirname(requested));
|
|
65
|
+
return ok(join(parent, basename(requested)));
|
|
66
|
+
}
|
|
67
|
+
catch (cause) {
|
|
68
|
+
return err(new PermissionPathError(path, errorReason(cause), { cause }));
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const isNotFound = (error) => error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
72
|
+
const errorReason = (error) => isNotFound(error) ? "not_found" : "io";
|
|
73
|
+
/** Stateful owner for permission evaluation, consumption, reload, and revoke. */
|
|
74
|
+
export class PermissionAuthority {
|
|
75
|
+
policy;
|
|
76
|
+
constructor(policy) {
|
|
77
|
+
this.policy = policy;
|
|
78
|
+
}
|
|
79
|
+
/** Canonicalize and authorize one exact operation before its side effect. */
|
|
80
|
+
async authorize(request, access, options = {}) {
|
|
81
|
+
const evaluated = await this.explain(request, access, options);
|
|
82
|
+
if (!evaluated.ok)
|
|
83
|
+
return evaluated;
|
|
84
|
+
const decision = evaluated.value;
|
|
85
|
+
this.policy = consumePermission(this.policy, decision);
|
|
86
|
+
return ok(decision);
|
|
87
|
+
}
|
|
88
|
+
/** Dry-run the exact shared evaluation without consuming a once grant. */
|
|
89
|
+
async explain(request, access, options = {}) {
|
|
90
|
+
const canonical = await canonicalizePermissionRequest(request, access);
|
|
91
|
+
if (!canonical.ok)
|
|
92
|
+
return canonical;
|
|
93
|
+
const decision = evaluatePermission(this.policy, canonical.value);
|
|
94
|
+
if (!decision.allowed)
|
|
95
|
+
return err(new PermissionRequiredError(canonical.value, decision.missing, ceilingFor(this.policy, canonical.value.capability), options.elicitationSupported ?? false, options.restartRequired ?? false));
|
|
96
|
+
return ok(decision);
|
|
97
|
+
}
|
|
98
|
+
/** Add authority beneath the administrator ceiling. */
|
|
99
|
+
grant(grant) {
|
|
100
|
+
const result = grantPermission(this.policy, grant);
|
|
101
|
+
if (result.ok)
|
|
102
|
+
this.policy = result.value;
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
/** Revoke authority immediately for subsequent operations. */
|
|
106
|
+
revoke(grantId) {
|
|
107
|
+
this.policy = revokePermission(this.policy, grantId);
|
|
108
|
+
}
|
|
109
|
+
/** Drop once/session authority when its owning connection ends. */
|
|
110
|
+
clearSessionGrants() {
|
|
111
|
+
this.policy = clearSessionPermissions(this.policy);
|
|
112
|
+
}
|
|
113
|
+
/** Atomically replace optional persisted project grants. */
|
|
114
|
+
async replaceProjectGrants(grants) {
|
|
115
|
+
let candidate = {
|
|
116
|
+
...this.policy,
|
|
117
|
+
grants: this.policy.grants.filter(({ lifetime }) => lifetime !== "project"),
|
|
118
|
+
};
|
|
119
|
+
for (const grant of grants) {
|
|
120
|
+
const canonical = await canonicalizeGrant(grant);
|
|
121
|
+
if (!canonical.ok)
|
|
122
|
+
return canonical;
|
|
123
|
+
const added = grantPermission(candidate, canonical.value);
|
|
124
|
+
if (!added.ok)
|
|
125
|
+
return err(new PermissionPathError(grant.grant_id, "io", {
|
|
126
|
+
cause: added.error,
|
|
127
|
+
}));
|
|
128
|
+
candidate = added.value;
|
|
129
|
+
}
|
|
130
|
+
this.policy = candidate;
|
|
131
|
+
return ok(null);
|
|
132
|
+
}
|
|
133
|
+
/** Replace grants derived from the currently loaded administrator ceiling. */
|
|
134
|
+
async replaceAdministratorGrants(grants) {
|
|
135
|
+
let candidate = {
|
|
136
|
+
...this.policy,
|
|
137
|
+
grants: this.policy.grants.filter(({ lifetime }) => lifetime !== "administrator"),
|
|
138
|
+
};
|
|
139
|
+
for (const grant of grants) {
|
|
140
|
+
const canonical = await canonicalizeGrant(grant);
|
|
141
|
+
if (!canonical.ok)
|
|
142
|
+
return canonical;
|
|
143
|
+
const added = grantPermission(candidate, canonical.value);
|
|
144
|
+
if (!added.ok)
|
|
145
|
+
return err(new PermissionPathError(grant.grant_id, "io", {
|
|
146
|
+
cause: added.error,
|
|
147
|
+
}));
|
|
148
|
+
candidate = added.value;
|
|
149
|
+
}
|
|
150
|
+
this.policy = candidate;
|
|
151
|
+
return ok(null);
|
|
152
|
+
}
|
|
153
|
+
/** Replace ceilings without restarting the MCP process. */
|
|
154
|
+
async reload(ceilings) {
|
|
155
|
+
const canonical = await canonicalizePermissionCeilings(ceilings);
|
|
156
|
+
if (!canonical.ok)
|
|
157
|
+
return canonical;
|
|
158
|
+
this.policy = reloadPermissionCeilings(this.policy, canonical.value);
|
|
159
|
+
return ok(null);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** Create one canonical policy and optional grants at the composition root. */
|
|
163
|
+
export const createPermissionAuthority = async (ceilings, grants = []) => {
|
|
164
|
+
const canonical = await canonicalizePermissionCeilings(ceilings);
|
|
165
|
+
if (!canonical.ok)
|
|
166
|
+
return canonical;
|
|
167
|
+
let policy = createPermissionPolicy(canonical.value);
|
|
168
|
+
for (const grant of grants) {
|
|
169
|
+
const canonicalGrant = await canonicalizeGrant(grant);
|
|
170
|
+
if (!canonicalGrant.ok)
|
|
171
|
+
return canonicalGrant;
|
|
172
|
+
const added = grantPermission(policy, canonicalGrant.value);
|
|
173
|
+
if (!added.ok)
|
|
174
|
+
return err(new PermissionPathError(grant.grant_id, "io", { cause: added.error }));
|
|
175
|
+
policy = added.value;
|
|
176
|
+
}
|
|
177
|
+
return ok(new PermissionAuthority(policy));
|
|
178
|
+
};
|
|
179
|
+
const canonicalizeGrant = async (grant) => {
|
|
180
|
+
const scope = await canonicalizeScope(grant, "read");
|
|
181
|
+
return scope.ok ? ok({ ...grant, ...scope.value }) : scope;
|
|
182
|
+
};
|
|
183
|
+
const ceilingFor = (policy, capability) => policy.ceilings.find((ceiling) => ceiling.capability === capability) ?? null;
|