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
|
@@ -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,32 @@ 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,
|
|
50
|
+
authorizeWorkspaceWrite: execution.authorizeWorkspaceWrite,
|
|
34
51
|
});
|
|
35
52
|
};
|
|
36
53
|
const continueInvestigation = async (context) => {
|
|
@@ -40,13 +57,34 @@ const continueInvestigation = async (context) => {
|
|
|
40
57
|
left,
|
|
41
58
|
right,
|
|
42
59
|
options: context.input.options,
|
|
60
|
+
integrity_policy: context.input.integrity_policy,
|
|
61
|
+
integrity_continue_approved: context.input.integrity_continue_approved,
|
|
62
|
+
max_integrity_mismatches: context.input.max_integrity_mismatches,
|
|
63
|
+
});
|
|
64
|
+
const legacyIdentity = createLegacyInvestigationRunIdentity({
|
|
65
|
+
left,
|
|
66
|
+
right,
|
|
67
|
+
options: context.input.options,
|
|
43
68
|
});
|
|
44
|
-
const existing = context.initial?.runs.find((
|
|
69
|
+
const existing = context.initial?.runs.find((run) => run.run_id === identity.runId ||
|
|
70
|
+
(context.input.integrity_policy === "fail" &&
|
|
71
|
+
!context.input.integrity_continue_approved &&
|
|
72
|
+
context.input.max_integrity_mismatches === 10 &&
|
|
73
|
+
"legacy_request_identity" in run &&
|
|
74
|
+
run.max_integrity_mismatches === 10 &&
|
|
75
|
+
run.run_id === legacyIdentity.runId));
|
|
45
76
|
if (context.initial !== null && existing?.status === "complete")
|
|
46
77
|
return completeOutcome(context.initial, existing, true, context.session);
|
|
78
|
+
if (context.authorizeWorkspaceWrite !== undefined) {
|
|
79
|
+
const authorized = await context.authorizeWorkspaceWrite();
|
|
80
|
+
if (!authorized.ok)
|
|
81
|
+
return authorized;
|
|
82
|
+
}
|
|
47
83
|
const pages = createInventoryEvidencePages(context.input, context.snapshots);
|
|
48
84
|
if (!pages.ok)
|
|
49
85
|
return pages;
|
|
86
|
+
if (isCancelled(context.signal))
|
|
87
|
+
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
50
88
|
const inventoried = await ensureInventoryCheckpoint({
|
|
51
89
|
input: context.input,
|
|
52
90
|
current: context.initial,
|
|
@@ -57,11 +95,36 @@ const continueInvestigation = async (context) => {
|
|
|
57
95
|
});
|
|
58
96
|
if (!inventoried.ok)
|
|
59
97
|
return inventoried;
|
|
98
|
+
await context.progress?.report({
|
|
99
|
+
phase: "inventory_versions",
|
|
100
|
+
completed: 2,
|
|
101
|
+
total: 4,
|
|
102
|
+
message: "Both artifact inventories checkpointed",
|
|
103
|
+
});
|
|
104
|
+
if (isCancelled(context.signal))
|
|
105
|
+
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
60
106
|
const compared = await ensureComparisonCheckpoint(context.input, inventoried.value, context.policy);
|
|
61
107
|
if (!compared.ok)
|
|
62
108
|
return compared;
|
|
63
|
-
|
|
109
|
+
await context.progress?.report({
|
|
110
|
+
phase: "compare_artifacts",
|
|
111
|
+
completed: 3,
|
|
112
|
+
total: 4,
|
|
113
|
+
message: "Artifact comparison checkpointed",
|
|
114
|
+
});
|
|
115
|
+
if (isCancelled(context.signal))
|
|
116
|
+
return err(new AnalysisCancelledError("find_changed_behavior"));
|
|
117
|
+
const finalized = await finalizeInvestigation(context.input, compared.value, context.policy, context.session);
|
|
118
|
+
await context.progress?.report({
|
|
119
|
+
phase: "changed_behavior",
|
|
120
|
+
completed: 4,
|
|
121
|
+
total: 4,
|
|
122
|
+
message: finalized.ok ? "Investigation completed" : "Investigation failed",
|
|
123
|
+
terminal: true,
|
|
124
|
+
});
|
|
125
|
+
return finalized;
|
|
64
126
|
};
|
|
127
|
+
const isCancelled = (signal) => signal?.aborted === true;
|
|
65
128
|
const ensureInventoryCheckpoint = async (input) => {
|
|
66
129
|
if (input.existing !== undefined) {
|
|
67
130
|
const consistent = validateInventoryCheckpoint(input.existing, input.pages);
|
|
@@ -77,6 +140,9 @@ const ensureInventoryCheckpoint = async (input) => {
|
|
|
77
140
|
left,
|
|
78
141
|
right,
|
|
79
142
|
options: input.input.options,
|
|
143
|
+
integrity_policy: input.input.integrity_policy,
|
|
144
|
+
integrity_continue_approved: input.input.integrity_continue_approved,
|
|
145
|
+
max_integrity_mismatches: input.input.max_integrity_mismatches,
|
|
80
146
|
});
|
|
81
147
|
const run = investigationRunSchema.parse({
|
|
82
148
|
schema_version: 1,
|
|
@@ -85,6 +151,9 @@ const ensureInventoryCheckpoint = async (input) => {
|
|
|
85
151
|
left,
|
|
86
152
|
right,
|
|
87
153
|
options: input.input.options,
|
|
154
|
+
integrity_policy: input.input.integrity_policy,
|
|
155
|
+
integrity_continue_approved: input.input.integrity_continue_approved,
|
|
156
|
+
max_integrity_mismatches: input.input.max_integrity_mismatches,
|
|
88
157
|
status: "running",
|
|
89
158
|
completed_stages: ["inventory_left", "inventory_right"],
|
|
90
159
|
left_inventory_evidence_ids: evidenceIds(input.pages.left),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AnalysisProtocolError, PermissionRequiredError, } from "../domain/errors.js";
|
|
2
|
+
import { err, ok } from "../domain/result.js";
|
|
3
|
+
import { PermissionPathError, } from "./PermissionAuthority.js";
|
|
4
|
+
/** Authorize one root-scoped operation and project policy path failures. */
|
|
5
|
+
export const authorizeRootPermission = async (authority, request) => {
|
|
6
|
+
const authorized = await authority.authorize({
|
|
7
|
+
capability: request.capability,
|
|
8
|
+
roots: request.roots,
|
|
9
|
+
executables: [],
|
|
10
|
+
environment_names: [],
|
|
11
|
+
network: "none",
|
|
12
|
+
mount: false,
|
|
13
|
+
operation_identity: `${request.operation}:${request.capability}:${request.roots.join(":")}`,
|
|
14
|
+
}, request.access);
|
|
15
|
+
return authorized.ok
|
|
16
|
+
? ok(null)
|
|
17
|
+
: err(authorized.error instanceof PermissionRequiredError
|
|
18
|
+
? authorized.error
|
|
19
|
+
: new AnalysisProtocolError(authorized.error.message, {
|
|
20
|
+
cause: authorized.error,
|
|
21
|
+
}));
|
|
22
|
+
};
|
|
23
|
+
/** Authorize an existing file read and defer write authority until requested. */
|
|
24
|
+
export const authorizeFileReadWithDeferredWrite = async (authority, input) => {
|
|
25
|
+
const read = await authority.authorize({
|
|
26
|
+
capability: input.readCapability,
|
|
27
|
+
roots: [input.path],
|
|
28
|
+
executables: [],
|
|
29
|
+
environment_names: [],
|
|
30
|
+
network: "none",
|
|
31
|
+
mount: false,
|
|
32
|
+
operation_identity: `${input.operation}:${input.readCapability}:${input.path}`,
|
|
33
|
+
}, "read");
|
|
34
|
+
let writeAuthorization;
|
|
35
|
+
if (!read.ok) {
|
|
36
|
+
if (!(read.error instanceof PermissionPathError) ||
|
|
37
|
+
read.error.reason !== "not_found")
|
|
38
|
+
return err(read.error instanceof PermissionRequiredError
|
|
39
|
+
? read.error
|
|
40
|
+
: new AnalysisProtocolError(read.error.message, {
|
|
41
|
+
cause: read.error,
|
|
42
|
+
}));
|
|
43
|
+
writeAuthorization = authorizeRootPermission(authority, {
|
|
44
|
+
capability: input.writeCapability,
|
|
45
|
+
roots: [input.path],
|
|
46
|
+
access: "write",
|
|
47
|
+
operation: input.operation,
|
|
48
|
+
});
|
|
49
|
+
const authorized = await writeAuthorization;
|
|
50
|
+
if (!authorized.ok)
|
|
51
|
+
return authorized;
|
|
52
|
+
}
|
|
53
|
+
return ok({
|
|
54
|
+
authorizeWrite: () => (writeAuthorization ??= authorizeRootPermission(authority, {
|
|
55
|
+
capability: input.writeCapability,
|
|
56
|
+
roots: [input.path],
|
|
57
|
+
access: "write",
|
|
58
|
+
operation: input.operation,
|
|
59
|
+
})),
|
|
60
|
+
});
|
|
61
|
+
};
|
|
@@ -1,14 +1,18 @@
|
|
|
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";
|
|
15
|
+
import { authorizeFileReadWithDeferredWrite, } from "./DeferredFileAuthorization.js";
|
|
12
16
|
const WORKFLOW_PROVIDER = {
|
|
13
17
|
id: "rea-workflow",
|
|
14
18
|
name: "REA composed investigation workflow",
|
|
@@ -19,12 +23,20 @@ const WORKFLOW_PROVIDER = {
|
|
|
19
23
|
* Unlike MCP mode, every CLI invocation is intentionally isolated and does not
|
|
20
24
|
* retain a target or bridge resources for a subsequent command.
|
|
21
25
|
*/
|
|
22
|
-
export const runDirectAnalysis = async (path, tool, arguments_, options = {}) => runAnalysis(path, tool, arguments_, {
|
|
26
|
+
export const runDirectAnalysis = async (path, tool, arguments_, options = {}) => withProcessCancellation(options.signal, (signal) => runAnalysis(path, tool, arguments_, {
|
|
23
27
|
logger: options.logger ?? silentLogger,
|
|
24
28
|
snapshotPath: options.snapshotPath,
|
|
25
|
-
|
|
29
|
+
signal,
|
|
30
|
+
...(options.permissionAuthority === undefined
|
|
31
|
+
? {}
|
|
32
|
+
: { permissionAuthority: options.permissionAuthority }),
|
|
33
|
+
}));
|
|
26
34
|
/** Execute one provider-native semantic operation with atomic provenance. */
|
|
27
|
-
export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger) => runAnalysis(path, tool, arguments_, {
|
|
35
|
+
export const runProviderAnalysis = async (path, tool, arguments_, logger = silentLogger, signal) => withProcessCancellation(signal, (operationSignal) => runAnalysis(path, tool, arguments_, {
|
|
36
|
+
logger,
|
|
37
|
+
snapshotPath: undefined,
|
|
38
|
+
signal: operationSignal,
|
|
39
|
+
}));
|
|
28
40
|
/** Describe configured providers without opening a target or launching Hopper. */
|
|
29
41
|
export const runSessionStatus = async (logger = silentLogger) => {
|
|
30
42
|
const config = parseConfig(process.env);
|
|
@@ -32,17 +44,97 @@ export const runSessionStatus = async (logger = silentLogger) => {
|
|
|
32
44
|
return cliError(config.error);
|
|
33
45
|
const session = createBinarySession(config.value, logger);
|
|
34
46
|
try {
|
|
35
|
-
return
|
|
47
|
+
return {
|
|
48
|
+
...jsonObjectSchema.parse(session.status()),
|
|
49
|
+
server_identity: createServerIdentity({
|
|
50
|
+
startedAt: new Date().toISOString(),
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
36
53
|
}
|
|
37
54
|
finally {
|
|
38
55
|
await session.close();
|
|
39
56
|
}
|
|
40
57
|
};
|
|
58
|
+
const authorizeAnalysis = async (authority, tool, arguments_) => {
|
|
59
|
+
const requests = [];
|
|
60
|
+
if (tool === "extract_artifact" && typeof arguments_.output_root === "string")
|
|
61
|
+
requests.push({
|
|
62
|
+
capability: "artifact_extract",
|
|
63
|
+
path: arguments_.output_root,
|
|
64
|
+
access: "write",
|
|
65
|
+
});
|
|
66
|
+
for (const request of requests) {
|
|
67
|
+
const result = await authority.authorize({
|
|
68
|
+
capability: request.capability,
|
|
69
|
+
roots: [request.path],
|
|
70
|
+
executables: [],
|
|
71
|
+
environment_names: [],
|
|
72
|
+
network: "none",
|
|
73
|
+
mount: false,
|
|
74
|
+
operation_identity: `${tool}:${request.capability}:${request.path}`,
|
|
75
|
+
}, request.access);
|
|
76
|
+
if (!result.ok)
|
|
77
|
+
return err(result.error instanceof PermissionRequiredError
|
|
78
|
+
? result.error
|
|
79
|
+
: new AnalysisProtocolError(result.error.message, {
|
|
80
|
+
cause: result.error,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
if (tool === "inventory_artifact" &&
|
|
84
|
+
arguments_.native_mount_approved === true) {
|
|
85
|
+
const result = await authority.authorize({
|
|
86
|
+
capability: "native_mount",
|
|
87
|
+
roots: [],
|
|
88
|
+
executables: [],
|
|
89
|
+
environment_names: [],
|
|
90
|
+
network: "none",
|
|
91
|
+
mount: true,
|
|
92
|
+
operation_identity: "inventory_artifact:native_mount",
|
|
93
|
+
}, "read");
|
|
94
|
+
if (!result.ok)
|
|
95
|
+
return err(result.error instanceof PermissionRequiredError
|
|
96
|
+
? result.error
|
|
97
|
+
: new AnalysisProtocolError(result.error.message, {
|
|
98
|
+
cause: result.error,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
return ok(null);
|
|
102
|
+
};
|
|
103
|
+
const permissionAuthorityFor = (config, supplied) => supplied === undefined
|
|
104
|
+
? loadConfiguredPermissionAuthority(config)
|
|
105
|
+
: Promise.resolve(ok(supplied));
|
|
106
|
+
const authorizeSnapshotAccess = async (authority, tool, snapshotPath) => snapshotPath === undefined
|
|
107
|
+
? ok(undefined)
|
|
108
|
+
: authorizeFileReadWithDeferredWrite(authority, {
|
|
109
|
+
path: snapshotPath,
|
|
110
|
+
readCapability: "snapshot_read",
|
|
111
|
+
writeCapability: "snapshot_write",
|
|
112
|
+
operation: tool,
|
|
113
|
+
});
|
|
114
|
+
const authorizeDeferredWrite = (authorization) => authorization?.authorizeWrite() ?? Promise.resolve(ok(null));
|
|
115
|
+
const authorizeAnalysisRun = async (input) => {
|
|
116
|
+
const authority = await permissionAuthorityFor(input.config, input.suppliedAuthority);
|
|
117
|
+
if (!authority.ok)
|
|
118
|
+
return authority;
|
|
119
|
+
const operation = await authorizeAnalysis(authority.value, input.tool, input.arguments);
|
|
120
|
+
if (!operation.ok)
|
|
121
|
+
return operation;
|
|
122
|
+
return authorizeSnapshotAccess(authority.value, input.tool, input.snapshotPath);
|
|
123
|
+
};
|
|
41
124
|
const runAnalysis = async (path, tool, arguments_, options) => {
|
|
42
|
-
const { logger, snapshotPath } = options;
|
|
125
|
+
const { logger, signal, snapshotPath } = options;
|
|
43
126
|
const config = parseConfig(process.env);
|
|
44
127
|
if (!config.ok)
|
|
45
128
|
return cliError(config.error);
|
|
129
|
+
const authorization = await authorizeAnalysisRun({
|
|
130
|
+
config: config.value,
|
|
131
|
+
suppliedAuthority: options.permissionAuthority,
|
|
132
|
+
tool,
|
|
133
|
+
arguments: arguments_,
|
|
134
|
+
snapshotPath,
|
|
135
|
+
});
|
|
136
|
+
if (!authorization.ok)
|
|
137
|
+
return cliError(authorization.error);
|
|
46
138
|
const unavailable = snapshotUnavailable(snapshotPath, config.value.hopperLoaderArgs);
|
|
47
139
|
if (unavailable !== undefined)
|
|
48
140
|
return cliError(unavailable);
|
|
@@ -60,8 +152,11 @@ const runAnalysis = async (path, tool, arguments_, options) => {
|
|
|
60
152
|
return cliError(prepared.error);
|
|
61
153
|
if (prepared.value.evidence !== undefined)
|
|
62
154
|
return prepared.value.evidence;
|
|
155
|
+
const writable = await authorizeDeferredWrite(authorization.value);
|
|
156
|
+
if (!writable.ok)
|
|
157
|
+
return cliError(writable.error);
|
|
63
158
|
const { snapshot } = prepared.value;
|
|
64
|
-
const opened = await session.open(path, snapshot === undefined ? {} : { snapshot });
|
|
159
|
+
const opened = await session.open(path, snapshot === undefined ? { signal } : { signal, snapshot });
|
|
65
160
|
if (!opened.ok)
|
|
66
161
|
return cliError(opened.error);
|
|
67
162
|
let output;
|
|
@@ -69,7 +164,7 @@ const runAnalysis = async (path, tool, arguments_, options) => {
|
|
|
69
164
|
if (tool === "binary_overview" ||
|
|
70
165
|
tool === "analyze_function" ||
|
|
71
166
|
tool === "trace_feature") {
|
|
72
|
-
const result = await new EnhancedTools(session).execute(tool, arguments_);
|
|
167
|
+
const result = await new EnhancedTools(session).execute(tool, arguments_, signal);
|
|
73
168
|
if (!result.ok)
|
|
74
169
|
output = cliError(result.error);
|
|
75
170
|
else {
|
|
@@ -86,7 +181,7 @@ const runAnalysis = async (path, tool, arguments_, options) => {
|
|
|
86
181
|
}
|
|
87
182
|
}
|
|
88
183
|
else {
|
|
89
|
-
const result = await session.execute(tool, arguments_);
|
|
184
|
+
const result = await session.execute(tool, arguments_, { signal });
|
|
90
185
|
if (!result.ok)
|
|
91
186
|
output = cliError(result.error);
|
|
92
187
|
else {
|
|
@@ -117,6 +212,19 @@ const runAnalysis = async (path, tool, arguments_, options) => {
|
|
|
117
212
|
await session.close();
|
|
118
213
|
}
|
|
119
214
|
};
|
|
215
|
+
const withProcessCancellation = async (suppliedSignal, operation) => {
|
|
216
|
+
if (suppliedSignal !== undefined)
|
|
217
|
+
return operation(suppliedSignal);
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const cancel = () => controller.abort();
|
|
220
|
+
process.once("SIGINT", cancel);
|
|
221
|
+
try {
|
|
222
|
+
return await operation(controller.signal);
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
process.off("SIGINT", cancel);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
120
228
|
const prepareSnapshot = async (options) => {
|
|
121
229
|
const { path, tool, arguments: arguments_, snapshotPath, policy, provider, } = options;
|
|
122
230
|
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,
|
|
@@ -85,7 +85,7 @@ export class EvidenceLedger {
|
|
|
85
85
|
}
|
|
86
86
|
/** Export records in deterministic, semantically irrelevant ID order. */
|
|
87
87
|
export() {
|
|
88
|
-
return createEvidenceBundle([...this.#records.values()], [...this.#unknownRevisions.values()]);
|
|
88
|
+
return structuredClone(createEvidenceBundle([...this.#records.values()], [...this.#unknownRevisions.values()]));
|
|
89
89
|
}
|
|
90
90
|
/** Check whether one immutable Evidence ID is present in this ledger. */
|
|
91
91
|
has(evidenceId) {
|
|
@@ -162,7 +162,7 @@ export class EvidenceLedger {
|
|
|
162
162
|
if (!checked.ok)
|
|
163
163
|
return checked;
|
|
164
164
|
this.#commit(pendingRecords, pendingUnknowns, checked.value);
|
|
165
|
-
return ok(unknown);
|
|
165
|
+
return ok(structuredClone(unknown));
|
|
166
166
|
}
|
|
167
167
|
/** Apply one approved full-state update using optimistic revision matching. */
|
|
168
168
|
updateUnknown(input, mutationEvidence) {
|
|
@@ -187,7 +187,8 @@ export class EvidenceLedger {
|
|
|
187
187
|
(filters.severity === undefined ||
|
|
188
188
|
unknown.severity === filters.severity) &&
|
|
189
189
|
(filters.domain === undefined || unknown.domain === filters.domain))
|
|
190
|
-
.sort((left, right) => left.unknown_id.localeCompare(right.unknown_id))
|
|
190
|
+
.sort((left, right) => left.unknown_id.localeCompare(right.unknown_id))
|
|
191
|
+
.map((unknown) => structuredClone(unknown));
|
|
191
192
|
}
|
|
192
193
|
/** Return current resolution validity; imported invalid states are rejected. */
|
|
193
194
|
verifyUnknownResolution(unknownId) {
|
|
@@ -197,7 +198,7 @@ export class EvidenceLedger {
|
|
|
197
198
|
return ok({
|
|
198
199
|
valid: unknown.status === "resolved",
|
|
199
200
|
truthVerified: unknown.resolution?.disposition === "verified",
|
|
200
|
-
unknown,
|
|
201
|
+
unknown: structuredClone(unknown),
|
|
201
202
|
});
|
|
202
203
|
}
|
|
203
204
|
/** Clear records when the owning session closes. */
|
|
@@ -225,7 +226,7 @@ export class EvidenceLedger {
|
|
|
225
226
|
if (!checked.ok)
|
|
226
227
|
return checked;
|
|
227
228
|
this.#commit(pendingRecords, pendingUnknowns, checked.value);
|
|
228
|
-
return ok(unknown);
|
|
229
|
+
return ok(structuredClone(unknown));
|
|
229
230
|
}
|
|
230
231
|
#validateCandidate(records, unknowns) {
|
|
231
232
|
if (records.size + unknowns.size > this.limits.maxRecords)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lstat, open, readFile, realpath, unlink, } from "node:fs/promises";
|
|
2
2
|
import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
3
3
|
import writeFileAtomic from "write-file-atomic";
|
|
4
|
+
import { isJsonWithinLimits } from "../domain/jsonLimits.js";
|
|
4
5
|
import { InvestigationWorkspaceError } from "../domain/errors.js";
|
|
5
6
|
import { parseInvestigationWorkspace, serializeInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
|
|
6
7
|
import { err, ok } from "../domain/result.js";
|
|
@@ -97,7 +98,7 @@ const readWorkspaceFile = async (destination, policy) => {
|
|
|
97
98
|
catch (cause) {
|
|
98
99
|
return err(new InvestigationWorkspaceError("read", "invalid-json", { cause }));
|
|
99
100
|
}
|
|
100
|
-
if (!
|
|
101
|
+
if (!isJsonWithinLimits(decoded, policy))
|
|
101
102
|
return err(new InvestigationWorkspaceError("read", "too-large"));
|
|
102
103
|
try {
|
|
103
104
|
return ok(parseInvestigationWorkspace(decoded));
|
|
@@ -182,31 +183,6 @@ const validateNextRevision = (current, next, expectedRevision) => {
|
|
|
182
183
|
? ok(null)
|
|
183
184
|
: err(new InvestigationWorkspaceError("update", "revision-conflict"));
|
|
184
185
|
};
|
|
185
|
-
const withinJsonLimits = (root, policy) => {
|
|
186
|
-
const pending = [
|
|
187
|
-
{ value: root, depth: 0 },
|
|
188
|
-
];
|
|
189
|
-
let nodes = 0;
|
|
190
|
-
while (pending.length > 0) {
|
|
191
|
-
const current = pending.pop();
|
|
192
|
-
if (current === undefined)
|
|
193
|
-
break;
|
|
194
|
-
nodes += 1;
|
|
195
|
-
if (nodes > policy.maxNodes || current.depth > policy.maxDepth)
|
|
196
|
-
return false;
|
|
197
|
-
if (typeof current.value === "string" &&
|
|
198
|
-
current.value.length > policy.maxStringLength)
|
|
199
|
-
return false;
|
|
200
|
-
if (typeof current.value !== "object" || current.value === null)
|
|
201
|
-
continue;
|
|
202
|
-
for (const [key, value] of Object.entries(current.value)) {
|
|
203
|
-
if (key.length > policy.maxStringLength)
|
|
204
|
-
return false;
|
|
205
|
-
pending.push({ value, depth: current.depth + 1 });
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return true;
|
|
209
|
-
};
|
|
210
186
|
const isFileNotFound = (cause) => errorCode(cause) === "ENOENT";
|
|
211
187
|
const isAlreadyExists = (cause) => errorCode(cause) === "EEXIST";
|
|
212
188
|
const errorCode = (cause) => typeof cause === "object" && cause !== null && "code" in cause
|