rea-agents 1.4.0 → 1.6.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 +37 -13
- package/dist/application/AnalysisSnapshotCache.js +8 -6
- package/dist/application/ArtifactGraphConstruction.js +11 -2
- package/dist/application/ArtifactInventory.js +3 -2
- package/dist/application/AuthorizedArtifactInventory.js +2 -1
- package/dist/application/BinarySession.js +26 -11
- package/dist/application/BoundedJsonFiles.js +6 -29
- package/dist/application/BrowserEvidence.js +107 -0
- package/dist/application/BrowserObservationPort.js +1 -0
- package/dist/application/BrowserObservationService.js +116 -0
- package/dist/application/CapabilityInventory.js +14 -0
- package/dist/application/ClientConfigPath.js +33 -0
- package/dist/application/ConfiguredRoots.js +19 -0
- package/dist/application/CrossVersionInventory.js +16 -3
- package/dist/application/CrossVersionInvestigation.js +23 -62
- package/dist/application/CrossVersionInvestigationEvidence.js +64 -0
- package/dist/application/CrossVersionInvestigationReplay.js +170 -0
- package/dist/application/DeferredFileAuthorization.js +61 -0
- package/dist/application/DirectAnalysis.js +38 -18
- package/dist/application/Doctor.js +16 -11
- package/dist/application/ElectronEvidence.js +35 -0
- package/dist/application/ElectronObservationPort.js +1 -0
- package/dist/application/ElectronObservationService.js +46 -0
- package/dist/application/EvidenceLedger.js +11 -7
- package/dist/application/InvestigationWorkspaceStore.js +2 -26
- package/dist/application/PermissionAuthority.js +30 -0
- package/dist/application/ProcessCaptureAuthority.js +3 -2
- package/dist/application/ProcessCaptureLifecycle.js +19 -13
- package/dist/application/ProcessCheckpoints.js +1 -1
- package/dist/application/ProcessCli.js +17 -5
- package/dist/application/ProcessHarness.js +19 -26
- package/dist/application/ProcessNormalization.js +17 -2
- package/dist/application/ProcessOwnership.js +44 -23
- package/dist/application/ProcessSampling.js +111 -25
- package/dist/application/ProjectPermissionStore.js +107 -3
- package/dist/application/ReferenceSourceImport.js +13 -16
- package/dist/application/ReferenceSourceImportPolicy.js +3 -2
- package/dist/application/Setup.js +89 -30
- package/dist/application/SetupClients.js +7 -4
- package/dist/application/SetupInstallFailure.js +24 -0
- package/dist/application/SetupPlan.js +11 -7
- package/dist/application/SetupSkill.js +20 -3
- package/dist/application/TerminalRenderer.js +1 -1
- package/dist/application/Uninstall.js +20 -7
- package/dist/application/Upgrade.js +14 -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 +261 -0
- package/dist/browser/CdpCaptureCompleteness.js +90 -0
- package/dist/browser/CdpCaptureDocuments.js +299 -0
- package/dist/browser/CdpCaptureEvents.js +547 -0
- package/dist/browser/CdpCaptureStorage.js +53 -0
- package/dist/browser/CdpCaptureValues.js +68 -0
- package/dist/browser/CdpConnection.js +241 -0
- package/dist/browser/CdpElectronInspection.js +330 -0
- package/dist/browser/CdpElectronProvider.js +134 -0
- package/dist/browser/CdpEndpoint.js +173 -0
- package/dist/browser/CdpObservationSession.js +301 -0
- package/dist/browser/CdpOptionalCommand.js +15 -0
- package/dist/browser/CdpPageCapture.js +376 -0
- package/dist/browser/CdpSafeMetadata.js +203 -0
- package/dist/browser/CdpScreenshot.js +101 -0
- package/dist/browser/CdpTargetSession.js +47 -0
- package/dist/browser/CdpWebMcpDiscovery.js +242 -0
- package/dist/browser/ElectronFileScope.js +50 -0
- package/dist/browser/PngVisualDiff.js +177 -0
- package/dist/browser/SensitiveTextCapture.js +25 -0
- package/dist/browser/WebSourceMapFetcher.js +357 -0
- package/dist/catalogIdentity.js +10 -0
- package/dist/cli.js +6 -0
- package/dist/cliBrowserAdvancedCommands.js +169 -0
- package/dist/cliBrowserCommands.js +312 -0
- package/dist/cliElectronCommands.js +114 -0
- package/dist/cliInvestigationCommands.js +26 -34
- package/dist/cliPolicyCommands.js +60 -12
- package/dist/config.js +80 -1
- package/dist/contracts/browserToolContracts.js +381 -0
- package/dist/contracts/electronToolContracts.js +73 -0
- package/dist/contracts/errorSchemas.js +2 -0
- package/dist/contracts/toolContracts.js +5 -5
- package/dist/domain/browserCompleteness.js +101 -0
- package/dist/domain/browserObservation.js +587 -0
- package/dist/domain/browserSession.js +63 -0
- package/dist/domain/changedBehavior.js +4 -15
- package/dist/domain/electronObservation.js +162 -0
- package/dist/domain/errors.js +41 -0
- package/dist/domain/investigationWorkspace.js +4 -0
- package/dist/domain/jsonLimits.js +26 -0
- package/dist/domain/jsonShape.js +125 -0
- package/dist/domain/permissionPolicy.js +6 -2
- package/dist/domain/processCapture.js +3 -1
- package/dist/domain/processComparison.js +22 -34
- package/dist/domain/reconstructionVerification.js +2 -15
- package/dist/domain/reconstructionVerificationSchemas.js +2 -0
- package/dist/domain/referenceSourceGraph.js +3 -1
- package/dist/domain/staticRuntimeCorrelation.js +2 -0
- package/dist/domain/symbolAnalysis.js +1 -1
- package/dist/domain/webBundleAnalysis.js +195 -0
- package/dist/domain/webBundleAnalyzer.js +423 -0
- package/dist/domain/webCaptureDiff.js +184 -0
- package/dist/domain/webContentArtifact.js +47 -0
- package/dist/domain/webInventory.js +61 -0
- package/dist/domain/webMcpDiscovery.js +68 -0
- package/dist/domain/webScreenshot.js +114 -0
- package/dist/generatedPackageMetadata.js +2 -2
- package/dist/hopper/BridgeLauncher.js +2 -3
- package/dist/hopper/HopperClient.js +51 -11
- package/dist/hopper/HopperProvider.js +6 -1
- package/dist/main.js +42 -31
- package/dist/native/CommandRunner.js +11 -4
- package/dist/native/NativeMacOSProvider.js +3 -2
- package/dist/native/NativeMachoInspection.js +21 -6
- package/dist/server/createServer.js +14 -0
- package/dist/server/registerBrowserTools.js +95 -0
- package/dist/server/registerElectronTools.js +36 -0
- package/dist/server/registerInvestigationTools.js +31 -37
- package/dist/server/registerProcessComparisonTool.js +2 -0
- package/dist/server/registerSessionTools.js +16 -2
- package/dist/server/runDerivedOperation.js +10 -2
- package/package.json +6 -1
- package/scripts/rea.mjs +2 -2
- package/skills/rea-analysis/SKILL.md +28 -3
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { promisify } from "node:util";
|
|
4
|
+
import { readProcessRunId } from "./ProcessOwnership.js";
|
|
4
5
|
const execFileAsync = promisify(execFile);
|
|
5
6
|
const PROC_READ_CONCURRENCY = 64;
|
|
7
|
+
/** Prove that forkpty completed session setup and installed the run token. */
|
|
8
|
+
export const isInitializedPtyRoot = (options) => options.before.pid === options.rootPid &&
|
|
9
|
+
options.before.process_group_id === options.rootPid &&
|
|
10
|
+
options.before.session_id === options.rootPid &&
|
|
11
|
+
options.after.pid === options.rootPid &&
|
|
12
|
+
options.after.parent_pid === options.before.parent_pid &&
|
|
13
|
+
options.after.process_group_id === options.rootPid &&
|
|
14
|
+
options.after.session_id === options.rootPid &&
|
|
15
|
+
options.after.startTime === options.before.startTime &&
|
|
16
|
+
options.observedRunId === options.expectedRunId;
|
|
6
17
|
const isExpectedProcReadFailure = (cause) => cause instanceof Error &&
|
|
7
18
|
"code" in cause &&
|
|
8
19
|
(cause.code === "ENOENT" ||
|
|
@@ -67,23 +78,58 @@ const readProcessCommand = async (pid, signal) => {
|
|
|
67
78
|
return handleProcReadFailure(cause);
|
|
68
79
|
}
|
|
69
80
|
};
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
signal
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
const systemLinuxChildrenHost = {
|
|
82
|
+
async taskIds(pid, signal) {
|
|
83
|
+
try {
|
|
84
|
+
signal.throwIfAborted();
|
|
85
|
+
const entries = await readdir(`/proc/${String(pid)}/task`);
|
|
86
|
+
signal.throwIfAborted();
|
|
87
|
+
return entries
|
|
88
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
89
|
+
.map(Number)
|
|
90
|
+
.filter((taskId) => Number.isSafeInteger(taskId) && taskId > 0)
|
|
91
|
+
.sort((left, right) => left - right);
|
|
92
|
+
}
|
|
93
|
+
catch (cause) {
|
|
94
|
+
return handleProcReadFailure(cause);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
async children(pid, taskId, signal) {
|
|
98
|
+
try {
|
|
99
|
+
return await readFile(`/proc/${String(pid)}/task/${String(taskId)}/children`, { encoding: "utf8", signal });
|
|
100
|
+
}
|
|
101
|
+
catch (cause) {
|
|
102
|
+
return handleProcReadFailure(cause);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
/** Read the deduplicated children created by every thread in a Linux process. */
|
|
107
|
+
export const readLinuxChildren = async (pid, signal, host = systemLinuxChildrenHost) => {
|
|
108
|
+
const taskIds = await host.taskIds(pid, signal);
|
|
109
|
+
if (taskIds === undefined)
|
|
110
|
+
return undefined;
|
|
111
|
+
const children = new Set();
|
|
112
|
+
let successfulReads = 0;
|
|
113
|
+
for (let offset = 0; offset < taskIds.length; offset += PROC_READ_CONCURRENCY) {
|
|
114
|
+
const texts = await Promise.all(taskIds
|
|
115
|
+
.slice(offset, offset + PROC_READ_CONCURRENCY)
|
|
116
|
+
.map((taskId) => host.children(pid, taskId, signal)));
|
|
117
|
+
for (const text of texts) {
|
|
118
|
+
if (text === undefined)
|
|
119
|
+
continue;
|
|
120
|
+
successfulReads += 1;
|
|
121
|
+
for (const value of text.trim().split(/\s+/)) {
|
|
122
|
+
if (!/^\d+$/.test(value))
|
|
123
|
+
continue;
|
|
124
|
+
const child = Number(value);
|
|
125
|
+
if (Number.isSafeInteger(child) && child > 0)
|
|
126
|
+
children.add(child);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
80
129
|
}
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
.
|
|
84
|
-
.filter((value) => /^\d+$/.test(value))
|
|
85
|
-
.map(Number)
|
|
86
|
-
.filter((child) => Number.isSafeInteger(child) && child > 0);
|
|
130
|
+
return successfulReads === 0
|
|
131
|
+
? undefined
|
|
132
|
+
: [...children].sort((left, right) => left - right);
|
|
87
133
|
};
|
|
88
134
|
const inspectProcess = async (pid, expectedParent, signal, identities) => {
|
|
89
135
|
const before = await readProcessStat(pid, signal);
|
|
@@ -179,6 +225,34 @@ const readPsRows = async (signal) => {
|
|
|
179
225
|
row.parent_pid >= 0)
|
|
180
226
|
.sort((left, right) => left.pid - right.pid);
|
|
181
227
|
};
|
|
228
|
+
const readRootIdentity = async (rootPid, signal) => process.platform === "linux"
|
|
229
|
+
? readProcessStat(rootPid, signal)
|
|
230
|
+
: (await readPsRows(signal)).find(({ pid }) => pid === rootPid);
|
|
231
|
+
const inspectInitializedPtyRoot = async (rootPid, runId, signal) => {
|
|
232
|
+
const before = await readRootIdentity(rootPid, signal);
|
|
233
|
+
if (before === undefined ||
|
|
234
|
+
before.process_group_id !== rootPid ||
|
|
235
|
+
before.session_id !== rootPid)
|
|
236
|
+
return undefined;
|
|
237
|
+
let observedRunId;
|
|
238
|
+
try {
|
|
239
|
+
observedRunId = await readProcessRunId(rootPid);
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
const after = await readRootIdentity(rootPid, signal);
|
|
245
|
+
if (after === undefined ||
|
|
246
|
+
!isInitializedPtyRoot({
|
|
247
|
+
rootPid,
|
|
248
|
+
expectedRunId: runId,
|
|
249
|
+
before,
|
|
250
|
+
observedRunId,
|
|
251
|
+
after,
|
|
252
|
+
}))
|
|
253
|
+
return undefined;
|
|
254
|
+
return { startTime: after.startTime };
|
|
255
|
+
};
|
|
182
256
|
const samplePs = async (context) => {
|
|
183
257
|
const { rootPid, limit, signal } = context;
|
|
184
258
|
if (limit <= 0)
|
|
@@ -242,25 +316,37 @@ const sampleProcesses = async (context) => {
|
|
|
242
316
|
return samples;
|
|
243
317
|
};
|
|
244
318
|
/** Start bounded process-tree sampling and expose an awaited stop. */
|
|
245
|
-
export const startProcessSampler = (
|
|
319
|
+
export const startProcessSampler = (options) => {
|
|
320
|
+
const { rootPid, runId, started, limit, samples } = options;
|
|
246
321
|
const identities = new Map();
|
|
247
322
|
const lastObservations = new Map();
|
|
248
323
|
let pending;
|
|
249
324
|
let stopped = false;
|
|
250
325
|
let abortCurrent;
|
|
251
326
|
let partial = false;
|
|
327
|
+
let rootInitialized = false;
|
|
252
328
|
const sample = () => {
|
|
253
329
|
if (stopped || pending !== undefined)
|
|
254
330
|
return;
|
|
255
331
|
const controller = new AbortController();
|
|
256
332
|
abortCurrent = () => controller.abort();
|
|
257
|
-
pending =
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
333
|
+
pending = (async () => {
|
|
334
|
+
if (!rootInitialized) {
|
|
335
|
+
const identity = await inspectInitializedPtyRoot(rootPid, runId, controller.signal);
|
|
336
|
+
if (identity === undefined)
|
|
337
|
+
return [];
|
|
338
|
+
if (identity.startTime !== undefined)
|
|
339
|
+
identities.set(rootPid, identity.startTime);
|
|
340
|
+
rootInitialized = true;
|
|
341
|
+
}
|
|
342
|
+
return sampleProcesses({
|
|
343
|
+
rootPid,
|
|
344
|
+
elapsedMs: Date.now() - started,
|
|
345
|
+
limit: limit + 1,
|
|
346
|
+
identities,
|
|
347
|
+
signal: controller.signal,
|
|
348
|
+
});
|
|
349
|
+
})()
|
|
264
350
|
.then((values) => {
|
|
265
351
|
for (const value of values) {
|
|
266
352
|
const observation = JSON.stringify({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, open, readFile, realpath, stat, unlink, } from "node:fs/promises";
|
|
3
3
|
import { dirname } from "node:path";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
4
5
|
import writeFileAtomic from "write-file-atomic";
|
|
5
6
|
import { z } from "zod";
|
|
6
7
|
import { err, ok } from "../domain/result.js";
|
|
@@ -8,6 +9,8 @@ const grantSchema = z.object({
|
|
|
8
9
|
grant_id: z.string().min(1),
|
|
9
10
|
capability: z.enum([
|
|
10
11
|
"process_capture",
|
|
12
|
+
"browser_observe",
|
|
13
|
+
"electron_observe",
|
|
11
14
|
"evidence_read",
|
|
12
15
|
"evidence_write",
|
|
13
16
|
"investigation_input",
|
|
@@ -22,6 +25,7 @@ const grantSchema = z.object({
|
|
|
22
25
|
roots: z.array(z.string()),
|
|
23
26
|
executables: z.array(z.string()),
|
|
24
27
|
environment_names: z.array(z.string()),
|
|
28
|
+
origins: z.array(z.string()).optional(),
|
|
25
29
|
network: z.enum(["none", "loopback", "external"]),
|
|
26
30
|
mount: z.boolean(),
|
|
27
31
|
lifetime: z.literal("project"),
|
|
@@ -65,7 +69,15 @@ export const readProjectPermissionStore = async (path, projectRoot) => {
|
|
|
65
69
|
const metadata = await stat(path);
|
|
66
70
|
if ((metadata.mode & 0o077) !== 0)
|
|
67
71
|
return err(new ProjectPermissionStoreError("not_owner_only"));
|
|
68
|
-
const
|
|
72
|
+
const encoded = await readFile(path, "utf8");
|
|
73
|
+
let decoded;
|
|
74
|
+
try {
|
|
75
|
+
decoded = JSON.parse(encoded);
|
|
76
|
+
}
|
|
77
|
+
catch (cause) {
|
|
78
|
+
return err(new ProjectPermissionStoreError("invalid", { cause }));
|
|
79
|
+
}
|
|
80
|
+
const parsed = storeSchema.safeParse(decoded);
|
|
69
81
|
if (!parsed.success ||
|
|
70
82
|
parsed.data.project_id !== project.value.id ||
|
|
71
83
|
parsed.data.project_root !== project.value.root)
|
|
@@ -107,4 +119,96 @@ export const writeProjectPermissionStore = async (path, projectRoot, grants) =>
|
|
|
107
119
|
return err(new ProjectPermissionStoreError("io", { cause }));
|
|
108
120
|
}
|
|
109
121
|
};
|
|
110
|
-
|
|
122
|
+
/** Revoke one grant while serializing the complete read-modify-write cycle. */
|
|
123
|
+
export const revokeProjectPermissionGrant = async (path, projectRoot, grantId) => {
|
|
124
|
+
let lock;
|
|
125
|
+
try {
|
|
126
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
127
|
+
const acquired = await acquirePermissionStoreLock(path);
|
|
128
|
+
if (!acquired.ok)
|
|
129
|
+
return acquired;
|
|
130
|
+
lock = acquired.value;
|
|
131
|
+
const current = await readProjectPermissionStore(path, projectRoot);
|
|
132
|
+
if (!current.ok)
|
|
133
|
+
return current;
|
|
134
|
+
const grants = current.value?.grants ?? [];
|
|
135
|
+
const retained = grants.filter(({ grant_id }) => grant_id !== grantId);
|
|
136
|
+
if (retained.length === grants.length)
|
|
137
|
+
return ok(false);
|
|
138
|
+
const written = await writeProjectPermissionStore(path, projectRoot, retained);
|
|
139
|
+
return written.ok ? ok(true) : written;
|
|
140
|
+
}
|
|
141
|
+
catch (cause) {
|
|
142
|
+
return err(new ProjectPermissionStoreError("io", { cause }));
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
if (lock !== undefined)
|
|
146
|
+
await releasePermissionStoreLock(lock);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const acquirePermissionStoreLock = async (destination) => {
|
|
150
|
+
const path = `${destination}.lock`;
|
|
151
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
152
|
+
try {
|
|
153
|
+
return ok(await createPermissionStoreLock(path));
|
|
154
|
+
}
|
|
155
|
+
catch (cause) {
|
|
156
|
+
if (!isAlreadyExists(cause))
|
|
157
|
+
return err(new ProjectPermissionStoreError("io", { cause }));
|
|
158
|
+
if (await removeStalePermissionStoreLock(path))
|
|
159
|
+
continue;
|
|
160
|
+
if (attempt === 99)
|
|
161
|
+
return err(new ProjectPermissionStoreError("locked", { cause }));
|
|
162
|
+
await delay(10);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return err(new ProjectPermissionStoreError("locked"));
|
|
166
|
+
};
|
|
167
|
+
const createPermissionStoreLock = async (path) => {
|
|
168
|
+
const handle = await open(path, "wx", 0o600);
|
|
169
|
+
try {
|
|
170
|
+
await handle.writeFile(`${String(process.pid)}\n`, "utf8");
|
|
171
|
+
await handle.sync();
|
|
172
|
+
return { path, handle };
|
|
173
|
+
}
|
|
174
|
+
catch (cause) {
|
|
175
|
+
await handle.close().catch(() => undefined);
|
|
176
|
+
await unlink(path).catch(() => undefined);
|
|
177
|
+
throw cause;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const removeStalePermissionStoreLock = async (path) => {
|
|
181
|
+
try {
|
|
182
|
+
const metadata = await lstat(path);
|
|
183
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > 32)
|
|
184
|
+
return false;
|
|
185
|
+
const encoded = (await readFile(path, "utf8")).trim();
|
|
186
|
+
if (!/^[1-9][0-9]{0,9}$/u.test(encoded))
|
|
187
|
+
return false;
|
|
188
|
+
if (processIsAlive(Number(encoded)))
|
|
189
|
+
return false;
|
|
190
|
+
await unlink(path);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const processIsAlive = (pid) => {
|
|
198
|
+
try {
|
|
199
|
+
process.kill(pid, 0);
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
catch (cause) {
|
|
203
|
+
return errorCode(cause) !== "ESRCH";
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const releasePermissionStoreLock = async (lock) => {
|
|
207
|
+
await lock.handle.close().catch(() => undefined);
|
|
208
|
+
await unlink(lock.path).catch(() => undefined);
|
|
209
|
+
};
|
|
210
|
+
const isNotFound = (cause) => errorCode(cause) === "ENOENT";
|
|
211
|
+
const isAlreadyExists = (cause) => errorCode(cause) === "EEXIST";
|
|
212
|
+
const errorCode = (cause) => typeof cause === "object" && cause !== null && "code" in cause
|
|
213
|
+
? cause.code
|
|
214
|
+
: undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isSecretLikePath } from "../domain/referenceSourceClassification.js";
|
|
2
|
-
import { createHistoricalSourceGraph, } from "../domain/referenceSourceGraph.js";
|
|
2
|
+
import { createHistoricalSourceGraph, historicalSourceParseFailureKey, } from "../domain/referenceSourceGraph.js";
|
|
3
3
|
import { err, ok } from "../domain/result.js";
|
|
4
4
|
import { readReferenceSource } from "../reference/ReferenceSourceReader.js";
|
|
5
5
|
import { parseReferenceSourceEntries } from "./ReferenceSourceImportEntries.js";
|
|
@@ -38,6 +38,17 @@ const deduplicateRelationships = (relationships) => {
|
|
|
38
38
|
}
|
|
39
39
|
return result;
|
|
40
40
|
};
|
|
41
|
+
/** Sort parse failures deterministically while retaining distinct reasons. */
|
|
42
|
+
export const normalizeHistoricalSourceParseFailures = (failures) => [...failures]
|
|
43
|
+
.sort((left, right) => byCodePoint(historicalSourceParseFailureKey(left), historicalSourceParseFailureKey(right)))
|
|
44
|
+
.filter((value, index, array) => {
|
|
45
|
+
if (index === 0)
|
|
46
|
+
return true;
|
|
47
|
+
const previous = array[index - 1];
|
|
48
|
+
return (previous === undefined ||
|
|
49
|
+
historicalSourceParseFailureKey(value) !==
|
|
50
|
+
historicalSourceParseFailureKey(previous));
|
|
51
|
+
});
|
|
41
52
|
const buildProvenance = (options) => ({
|
|
42
53
|
importer: options.importer ?? "rea-reference-source-import",
|
|
43
54
|
importer_version: options.importerVersion ?? null,
|
|
@@ -134,21 +145,7 @@ export const importReferenceSource = async (options) => {
|
|
|
134
145
|
if (isAborted(options.signal))
|
|
135
146
|
return err(cancelled());
|
|
136
147
|
const uniqueRelationships = deduplicateRelationships(relationships);
|
|
137
|
-
const uniqueFailures =
|
|
138
|
-
.sort((left, right) => {
|
|
139
|
-
const byPath = byCodePoint(left.path, right.path);
|
|
140
|
-
if (byPath !== 0)
|
|
141
|
-
return byPath;
|
|
142
|
-
return byCodePoint(left.parser, right.parser);
|
|
143
|
-
})
|
|
144
|
-
.filter((value, index, array) => {
|
|
145
|
-
if (index === 0)
|
|
146
|
-
return true;
|
|
147
|
-
const previous = array[index - 1];
|
|
148
|
-
if (previous === undefined)
|
|
149
|
-
return true;
|
|
150
|
-
return value.path !== previous.path || value.parser !== previous.parser;
|
|
151
|
-
});
|
|
148
|
+
const uniqueFailures = normalizeHistoricalSourceParseFailures(parseFailures);
|
|
152
149
|
const sortedEntries = [...entries].sort((left, right) => byCodePoint(left.path, right.path));
|
|
153
150
|
const sortedExclusions = sortAndLimit(exclusions, limits.maxEntries);
|
|
154
151
|
const sortedLimitations = [...limitations].sort(byCodePoint);
|
|
@@ -4,6 +4,7 @@ import ignore from "ignore";
|
|
|
4
4
|
import { err, ok } from "../domain/result.js";
|
|
5
5
|
import { withinRoot } from "../reference/ReferenceSourceReaderPaths.js";
|
|
6
6
|
import { DEFAULT_REFERENCE_SOURCE_IGNORE_PATTERNS, DEFAULT_REFERENCE_SOURCE_LIMITS, } from "./ReferenceSourceImportTypes.js";
|
|
7
|
+
import { canonicalizeConfiguredRoots } from "./ConfiguredRoots.js";
|
|
7
8
|
const failure = (code, message) => ({
|
|
8
9
|
tag: "reference-source-import",
|
|
9
10
|
code,
|
|
@@ -29,8 +30,8 @@ const authorizeRoot = async (requestedRoot, approvedRoots) => {
|
|
|
29
30
|
if (!(await stat(requestedRoot)).isDirectory())
|
|
30
31
|
return err(failure("invalid-root", "Reference source root is not a directory"));
|
|
31
32
|
const canonicalRoot = await realpath(resolve(requestedRoot));
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
const canonicalApprovedRoots = await canonicalizeConfiguredRoots(approvedRoots.map((approvedRoot) => resolve(approvedRoot)));
|
|
34
|
+
for (const canonicalApproved of canonicalApprovedRoots) {
|
|
34
35
|
if (withinRoot(canonicalApproved, canonicalRoot))
|
|
35
36
|
return ok(canonicalRoot);
|
|
36
37
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { access, copyFile, mkdir, readFile,
|
|
1
|
+
import { access, copyFile, mkdir, readFile, rm, writeFile, } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -6,15 +6,16 @@ import writeFileAtomic from "write-file-atomic";
|
|
|
6
6
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
7
7
|
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
8
8
|
import { supportsNodeVersion } from "../domain/runtimeVersion.js";
|
|
9
|
+
import { resolveClientConfigTransactionPath } from "./ClientConfigPath.js";
|
|
9
10
|
import { runDoctor, systemDoctorHost } from "./Doctor.js";
|
|
10
11
|
import { installLinuxHopper, readLinuxDistribution, } from "./LinuxHopper.js";
|
|
11
12
|
import { installMacHopper } from "./MacHopper.js";
|
|
12
13
|
import { configureDetectedClients } from "./SetupClients.js";
|
|
13
14
|
import { supportedClients } from "./SupportedClients.js";
|
|
14
|
-
import { installCanonicalSkill } from "./SetupSkill.js";
|
|
15
|
+
import { canonicalSkillNeedsInstall, installCanonicalSkill, } from "./SetupSkill.js";
|
|
15
16
|
import { setupPlan } from "./SetupPlan.js";
|
|
16
17
|
import { setupInstallFailure, } from "./SetupInstallFailure.js";
|
|
17
|
-
export { installCanonicalSkill } from "./SetupSkill.js";
|
|
18
|
+
export { canonicalSkillNeedsInstall, installCanonicalSkill, } from "./SetupSkill.js";
|
|
18
19
|
const registrationCommand = () => process.env.npm_command === "exec"
|
|
19
20
|
? ["npx", "-y", PRODUCT_IDENTITY.packageName, "mcp"]
|
|
20
21
|
: [resolve(process.argv[1] ?? PRODUCT_IDENTITY.cliBinary), "mcp"];
|
|
@@ -39,14 +40,10 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
|
39
40
|
if (unsupported !== undefined)
|
|
40
41
|
return fail(unsupported);
|
|
41
42
|
let hopperPath = await host.hopperPath();
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const installHopper = hopperPath === undefined || linuxHopperRepairNeeded;
|
|
47
|
-
const detectedClients = await host.detectedClients();
|
|
48
|
-
plannedActions = setupPlan(host.platform, installHopper, detectedClients);
|
|
49
|
-
let approved = options.approved;
|
|
43
|
+
const discovery = await discoverSetupActions(host, hopperPath);
|
|
44
|
+
const { detectedClients, installHopper, installSkill } = discovery;
|
|
45
|
+
plannedActions = discovery.plannedActions;
|
|
46
|
+
let approved = options.approved || plannedActions.length === 0;
|
|
50
47
|
let interactiveApproval = false;
|
|
51
48
|
if (!approved && confirm !== undefined && !options.structured) {
|
|
52
49
|
interactiveApproval = await confirm(plannedActions);
|
|
@@ -86,11 +83,13 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
|
86
83
|
});
|
|
87
84
|
if (clientFailure !== undefined)
|
|
88
85
|
return fail(clientFailure);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
86
|
+
if (installSkill) {
|
|
87
|
+
const skill = await host.installSkill();
|
|
88
|
+
if (skill === "failed")
|
|
89
|
+
return fail("REA analysis skill could not be installed or verified. Check permissions for `~/.agents/skills`, then rerun setup.");
|
|
90
|
+
if (skill === "installed")
|
|
91
|
+
appliedActions.push("installed_skill");
|
|
92
|
+
}
|
|
94
93
|
const doctor = await host.doctor();
|
|
95
94
|
const remediation = finalSetupRemediation(host.platform, appliedActions.includes("installed_hopper"), doctor.healthy, hopperPath);
|
|
96
95
|
return {
|
|
@@ -102,6 +101,32 @@ export const runSetup = async (options, host = systemSetupHost(), confirm) => {
|
|
|
102
101
|
...(remediation === undefined ? {} : { remediation }),
|
|
103
102
|
};
|
|
104
103
|
};
|
|
104
|
+
const discoverSetupActions = async (host, hopperPath) => {
|
|
105
|
+
const initialDoctor = await host.doctor();
|
|
106
|
+
const linuxHopperRepairNeeded = initialDoctor.checks.some(({ name, ok, detail }) => !ok &&
|
|
107
|
+
(name === "hopper-demo-runtime" ||
|
|
108
|
+
(name === "hopper-version" && detail === "/opt/hopper/bin/Hopper")));
|
|
109
|
+
const installHopper = hopperPath === undefined || linuxHopperRepairNeeded;
|
|
110
|
+
const [detectedClients, installSkill] = await Promise.all([
|
|
111
|
+
host.detectedClients(),
|
|
112
|
+
host.skillNeedsInstall(),
|
|
113
|
+
]);
|
|
114
|
+
const command = registrationCommand();
|
|
115
|
+
const clients = installHopper
|
|
116
|
+
? detectedClients
|
|
117
|
+
: await filterClientsNeedingConfigure(host, detectedClients, hopperPath, command);
|
|
118
|
+
const plannedActions = setupPlan(host.platform, installHopper, installSkill, clients);
|
|
119
|
+
return {
|
|
120
|
+
installHopper,
|
|
121
|
+
installSkill,
|
|
122
|
+
detectedClients: clients,
|
|
123
|
+
plannedActions,
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
const filterClientsNeedingConfigure = async (host, detectedClients, hopperPath, command) => {
|
|
127
|
+
const needs = await Promise.all(detectedClients.map((client) => host.clientNeedsConfigure(client, hopperPath, command)));
|
|
128
|
+
return detectedClients.filter((_, index) => needs[index]);
|
|
129
|
+
};
|
|
105
130
|
const finalSetupRemediation = (platform, installedHopper, healthy, hopperPath) => {
|
|
106
131
|
if (platform === "darwin" && installedHopper)
|
|
107
132
|
return "Open Hopper, choose its demo mode or activate a license, then rerun rea doctor --json.";
|
|
@@ -149,6 +174,8 @@ const systemSetupHost = () => {
|
|
|
149
174
|
: client.format === "toml"
|
|
150
175
|
? configureTomlClient(client, hopperPath, command)
|
|
151
176
|
: configureJsonClient(client, hopperPath, command),
|
|
177
|
+
clientNeedsConfigure: (client, hopperPath, command) => clientConfigurationAligned(client, hopperPath, command).then((aligned) => !aligned),
|
|
178
|
+
skillNeedsInstall: () => canonicalSkillNeedsInstall(homedir()),
|
|
152
179
|
installSkill: () => installCanonicalSkill(homedir()),
|
|
153
180
|
doctor: () => runDoctor(undefined, doctorHost),
|
|
154
181
|
};
|
|
@@ -168,10 +195,13 @@ export const configureJsonClient = async (client, hopperPath, command = [
|
|
|
168
195
|
PRODUCT_IDENTITY.packageName,
|
|
169
196
|
"mcp",
|
|
170
197
|
]) => {
|
|
198
|
+
const transactionPath = await resolveClientConfigTransactionPath(client.configPath);
|
|
199
|
+
if (transactionPath === undefined)
|
|
200
|
+
return { status: "failed", reason: "path" };
|
|
171
201
|
let document = {};
|
|
172
202
|
let original;
|
|
173
203
|
try {
|
|
174
|
-
original = await readFile(
|
|
204
|
+
original = await readFile(transactionPath, "utf8");
|
|
175
205
|
document = parseObject(original);
|
|
176
206
|
}
|
|
177
207
|
catch (cause) {
|
|
@@ -199,7 +229,7 @@ export const configureJsonClient = async (client, hopperPath, command = [
|
|
|
199
229
|
if (original !== undefined) {
|
|
200
230
|
backupPath = `${client.configPath}.rea.backup`;
|
|
201
231
|
try {
|
|
202
|
-
await copyFile(
|
|
232
|
+
await copyFile(transactionPath, backupPath);
|
|
203
233
|
}
|
|
204
234
|
catch {
|
|
205
235
|
return { status: "failed", reason: "backup" };
|
|
@@ -210,26 +240,26 @@ export const configureJsonClient = async (client, hopperPath, command = [
|
|
|
210
240
|
[PRODUCT_IDENTITY.mcpServerKey]: desired,
|
|
211
241
|
};
|
|
212
242
|
const encoded = `${JSON.stringify(document, null, 2)}\n`;
|
|
213
|
-
const temporary = `${client.configPath}.rea.tmp`;
|
|
214
243
|
try {
|
|
215
244
|
await mkdir(dirname(client.configPath), { recursive: true });
|
|
216
|
-
await
|
|
217
|
-
|
|
245
|
+
await writeFileAtomic(transactionPath, encoded, {
|
|
246
|
+
encoding: "utf8",
|
|
247
|
+
mode: 0o600,
|
|
248
|
+
});
|
|
218
249
|
}
|
|
219
250
|
catch {
|
|
220
|
-
await rm(temporary, { force: true }).catch(() => undefined);
|
|
221
251
|
return { status: "failed", reason: "write" };
|
|
222
252
|
}
|
|
223
253
|
try {
|
|
224
|
-
const readback = parseObject(await readFile(
|
|
254
|
+
const readback = parseObject(await readFile(transactionPath, "utf8"));
|
|
225
255
|
const value = parseOptionalObject(readback.mcpServers)[PRODUCT_IDENTITY.mcpServerKey];
|
|
226
256
|
if (JSON.stringify(value) !== JSON.stringify(desired)) {
|
|
227
|
-
await restoreConfig(
|
|
257
|
+
await restoreConfig(transactionPath, original);
|
|
228
258
|
return { status: "failed", reason: "readback" };
|
|
229
259
|
}
|
|
230
260
|
}
|
|
231
261
|
catch {
|
|
232
|
-
await restoreConfig(
|
|
262
|
+
await restoreConfig(transactionPath, original);
|
|
233
263
|
return { status: "failed", reason: "readback" };
|
|
234
264
|
}
|
|
235
265
|
return {
|
|
@@ -244,10 +274,13 @@ export const configureTomlClient = async (client, hopperPath, command = [
|
|
|
244
274
|
PRODUCT_IDENTITY.packageName,
|
|
245
275
|
"mcp",
|
|
246
276
|
]) => {
|
|
277
|
+
const transactionPath = await resolveClientConfigTransactionPath(client.configPath);
|
|
278
|
+
if (transactionPath === undefined)
|
|
279
|
+
return { status: "failed", reason: "path" };
|
|
247
280
|
let document = {};
|
|
248
281
|
let original;
|
|
249
282
|
try {
|
|
250
|
-
original = await readFile(
|
|
283
|
+
original = await readFile(transactionPath, "utf8");
|
|
251
284
|
document = objectSchema.parse(parseToml(original));
|
|
252
285
|
}
|
|
253
286
|
catch (cause) {
|
|
@@ -274,7 +307,7 @@ export const configureTomlClient = async (client, hopperPath, command = [
|
|
|
274
307
|
const backupPath = original === undefined ? undefined : `${client.configPath}.rea.backup`;
|
|
275
308
|
try {
|
|
276
309
|
if (backupPath !== undefined)
|
|
277
|
-
await copyFile(
|
|
310
|
+
await copyFile(transactionPath, backupPath);
|
|
278
311
|
}
|
|
279
312
|
catch {
|
|
280
313
|
return { status: "failed", reason: "backup" };
|
|
@@ -285,16 +318,16 @@ export const configureTomlClient = async (client, hopperPath, command = [
|
|
|
285
318
|
};
|
|
286
319
|
try {
|
|
287
320
|
await mkdir(dirname(client.configPath), { recursive: true });
|
|
288
|
-
await writeFileAtomic(
|
|
321
|
+
await writeFileAtomic(transactionPath, stringifyToml(document), {
|
|
289
322
|
encoding: "utf8",
|
|
290
323
|
mode: 0o600,
|
|
291
324
|
});
|
|
292
|
-
const readback = objectSchema.parse(parseToml(await readFile(
|
|
325
|
+
const readback = objectSchema.parse(parseToml(await readFile(transactionPath, "utf8")));
|
|
293
326
|
if (JSON.stringify(parseOptionalObject(readback.mcp_servers)[PRODUCT_IDENTITY.mcpServerKey]) !== JSON.stringify(desired))
|
|
294
327
|
throw new Error("TOML readback mismatch");
|
|
295
328
|
}
|
|
296
329
|
catch {
|
|
297
|
-
await restoreConfig(
|
|
330
|
+
await restoreConfig(transactionPath, original);
|
|
298
331
|
return { status: "failed", reason: "readback" };
|
|
299
332
|
}
|
|
300
333
|
return {
|
|
@@ -327,3 +360,29 @@ const restoreConfig = async (path, original) => {
|
|
|
327
360
|
// The backup remains available for the remediation reported by setup.
|
|
328
361
|
}
|
|
329
362
|
};
|
|
363
|
+
const clientConfigurationDesired = (hopperPath, command) => ({
|
|
364
|
+
command: command[0] ?? PRODUCT_IDENTITY.cliBinary,
|
|
365
|
+
args: command.slice(1),
|
|
366
|
+
...(hopperPath === undefined
|
|
367
|
+
? {}
|
|
368
|
+
: { env: { HOPPER_LAUNCHER_PATH: hopperPath } }),
|
|
369
|
+
});
|
|
370
|
+
const clientConfigurationAligned = async (client, hopperPath, command) => {
|
|
371
|
+
const desired = clientConfigurationDesired(hopperPath, command);
|
|
372
|
+
try {
|
|
373
|
+
const original = await readFile(client.configPath, "utf8");
|
|
374
|
+
if (client.format === "toml") {
|
|
375
|
+
const document = objectSchema.parse(parseToml(original));
|
|
376
|
+
const servers = parseOptionalObject(document.mcp_servers);
|
|
377
|
+
return (JSON.stringify(servers[PRODUCT_IDENTITY.mcpServerKey]) ===
|
|
378
|
+
JSON.stringify(desired));
|
|
379
|
+
}
|
|
380
|
+
const document = parseObject(original);
|
|
381
|
+
const servers = parseOptionalObject(document.mcpServers);
|
|
382
|
+
return (JSON.stringify(servers[PRODUCT_IDENTITY.mcpServerKey]) ===
|
|
383
|
+
JSON.stringify(desired));
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
const failedConfigurationMessage = (reason) => {
|
|
2
|
+
if (reason === "path")
|
|
3
|
+
return "Agent configuration path could not be safely verified. Check its permissions and, if it is a symbolic link, verify that the link resolves to a regular file owned by the current user, then rerun setup.";
|
|
2
4
|
if (reason === "backup")
|
|
3
5
|
return "Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup.";
|
|
4
6
|
if (reason === "write")
|
|
5
7
|
return "Agent configuration could not be updated. Check file permissions, then rerun setup.";
|
|
6
8
|
return "Agent configuration could not be verified after writing. Repair the configuration file or restore its `.rea.backup`, then rerun setup.";
|
|
7
9
|
};
|
|
8
|
-
/** Configure
|
|
10
|
+
/** Configure every detected agent and report the first failed transaction. */
|
|
9
11
|
export const configureDetectedClients = async (options) => {
|
|
12
|
+
let failure;
|
|
10
13
|
for (const client of options.detectedClients) {
|
|
11
14
|
const result = await options.host.configureClient(client, options.hopperPath, options.command);
|
|
12
15
|
options.clients[client.name] = result;
|
|
13
16
|
if (result.status === "failed")
|
|
14
|
-
|
|
15
|
-
if (result.status === "configured")
|
|
17
|
+
failure ??= failedConfigurationMessage(result.reason);
|
|
18
|
+
else if (result.status === "configured")
|
|
16
19
|
options.appliedActions.push(`configured_${client.name}`);
|
|
17
20
|
}
|
|
18
|
-
return
|
|
21
|
+
return failure;
|
|
19
22
|
};
|