rea-agents 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -12
- package/bridge/hopper_bridge.py +12 -3
- package/dist/application/AnalysisSnapshotCache.js +8 -6
- package/dist/application/ArtifactGraphConstruction.js +12 -3
- package/dist/application/ArtifactInventory.js +100 -27
- package/dist/application/AuthorizedArtifactInventory.js +4 -3
- package/dist/application/BinarySession.js +92 -4
- package/dist/application/BoundedJsonFiles.js +6 -29
- package/dist/application/BrowserEvidence.js +35 -0
- package/dist/application/BrowserObservationPort.js +1 -0
- package/dist/application/BrowserObservationService.js +52 -0
- package/dist/application/CapabilityInventory.js +134 -0
- package/dist/application/ClientRegistrationStatus.js +69 -0
- package/dist/application/ConfiguredRoots.js +19 -0
- package/dist/application/CrossVersionInventory.js +19 -8
- package/dist/application/CrossVersionInvestigation.js +74 -5
- package/dist/application/DeferredFileAuthorization.js +61 -0
- package/dist/application/DirectAnalysis.js +117 -9
- package/dist/application/Doctor.js +74 -2
- package/dist/application/EvidenceLedger.js +6 -5
- package/dist/application/InvestigationWorkspaceStore.js +2 -26
- package/dist/application/PermissionAuthority.js +183 -0
- package/dist/application/PermissionConfiguration.js +26 -0
- package/dist/application/ProcessCaptureAuthority.js +3 -2
- package/dist/application/ProcessCaptureError.js +13 -0
- package/dist/application/ProcessCaptureLifecycle.js +21 -9
- package/dist/application/ProcessCheckpoints.js +1 -1
- package/dist/application/ProcessCli.js +40 -15
- package/dist/application/ProcessHarness.js +10 -20
- package/dist/application/ProcessNormalization.js +17 -2
- package/dist/application/ProcessOwnership.js +4 -3
- package/dist/application/ProcessSampling.js +52 -17
- package/dist/application/ProgressReporter.js +30 -0
- package/dist/application/ProjectPermissionStore.js +213 -0
- package/dist/application/ReferenceSourceImportPolicy.js +3 -2
- package/dist/application/Setup.js +2 -41
- package/dist/application/SupportedClients.js +41 -0
- package/dist/application/TerminalRenderer.js +1 -1
- package/dist/application/Uninstall.js +1 -1
- package/dist/application/Upgrade.js +2 -0
- package/dist/application/runtime.js +1 -1
- package/dist/artifacts/ArtifactProvider.js +13 -3
- package/dist/artifacts/MachOSliceArtifactReader.js +1 -1
- package/dist/artifacts/NativeDmgArtifactReader.js +1 -1
- package/dist/artifacts/SafeOutputTree.js +3 -3
- package/dist/browser/CdpBrowserProvider.js +158 -0
- package/dist/browser/CdpCaptureDocuments.js +140 -0
- package/dist/browser/CdpCaptureEvents.js +248 -0
- package/dist/browser/CdpCaptureStorage.js +53 -0
- package/dist/browser/CdpCaptureValues.js +70 -0
- package/dist/browser/CdpConnection.js +218 -0
- package/dist/browser/CdpEndpoint.js +114 -0
- package/dist/browser/CdpOptionalCommand.js +15 -0
- package/dist/browser/CdpPageCapture.js +273 -0
- package/dist/catalogIdentity.js +123 -0
- package/dist/cli.js +48 -3
- package/dist/cliBrowserCommands.js +136 -0
- package/dist/cliEvidenceCommands.js +42 -1
- package/dist/cliInvestigationCommands.js +42 -6
- package/dist/cliLogging.js +20 -3
- package/dist/cliPolicyCommands.js +118 -0
- package/dist/cliProcessCommands.js +4 -14
- package/dist/config.js +107 -3
- package/dist/contracts/artifactToolContracts.js +14 -1
- package/dist/contracts/browserToolContracts.js +80 -0
- package/dist/contracts/errorSchemas.js +100 -0
- package/dist/contracts/toolContracts.js +13 -6
- package/dist/contracts/toolOutputSchemas.js +62 -30
- package/dist/domain/artifactComparison.js +38 -11
- package/dist/domain/artifactGraph.js +25 -1
- package/dist/domain/artifactInventoryEvidence.js +18 -0
- package/dist/domain/browserObservation.js +361 -0
- package/dist/domain/changedBehavior.js +9 -18
- package/dist/domain/errors.js +257 -3
- package/dist/domain/investigationWorkspace.js +131 -4
- package/dist/domain/jsonLimits.js +26 -0
- package/dist/domain/permissionPolicy.js +161 -0
- package/dist/domain/processCapture.js +3 -1
- package/dist/domain/processComparison.js +23 -34
- package/dist/domain/reconstructionVerification.js +3 -16
- package/dist/domain/reconstructionVerificationSchemas.js +3 -0
- package/dist/domain/staticRuntimeCorrelation.js +7 -1
- package/dist/domain/symbolAnalysis.js +1 -1
- package/dist/generatedPackageMetadata.js +9 -0
- package/dist/hopper/BridgeLauncher.js +7 -3
- package/dist/hopper/HopperClient.js +32 -5
- package/dist/identity.js +10 -3
- package/dist/logger.js +2 -2
- package/dist/main.js +101 -7
- package/dist/native/CommandRunner.js +11 -4
- package/dist/server/createServer.js +59 -5
- package/dist/server/mcpProgress.js +23 -0
- package/dist/server/registerArtifactComparisonTool.js +6 -2
- package/dist/server/registerBrowserTools.js +36 -0
- package/dist/server/registerBundleComparisonTool.js +6 -2
- package/dist/server/registerEnhancedTools.js +18 -1
- package/dist/server/registerEvidenceResources.js +302 -0
- package/dist/server/registerEvidenceTools.js +58 -1
- package/dist/server/registerFunctionComparisonTool.js +6 -2
- package/dist/server/registerInvestigationTools.js +78 -10
- package/dist/server/registerOfficialTools.js +19 -1
- package/dist/server/registerProcessComparisonTool.js +9 -3
- package/dist/server/registerSessionStatusTool.js +44 -3
- package/dist/server/registerSessionTools.js +144 -9
- package/dist/server/runDerivedOperation.js +43 -0
- package/dist/server/toolResult.js +27 -3
- package/dist/serverIdentity.js +70 -0
- package/package.json +7 -4
- package/scripts/rea.mjs +1 -1
- package/skills/rea-analysis/SKILL.md +18 -3
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, open, readFile, realpath, stat, unlink, } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
|
+
import writeFileAtomic from "write-file-atomic";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { err, ok } from "../domain/result.js";
|
|
8
|
+
const grantSchema = z.object({
|
|
9
|
+
grant_id: z.string().min(1),
|
|
10
|
+
capability: z.enum([
|
|
11
|
+
"process_capture",
|
|
12
|
+
"browser_observe",
|
|
13
|
+
"evidence_read",
|
|
14
|
+
"evidence_write",
|
|
15
|
+
"investigation_input",
|
|
16
|
+
"investigation_workspace_read",
|
|
17
|
+
"investigation_workspace_write",
|
|
18
|
+
"snapshot_read",
|
|
19
|
+
"snapshot_write",
|
|
20
|
+
"artifact_extract",
|
|
21
|
+
"native_mount",
|
|
22
|
+
"reference_read",
|
|
23
|
+
]),
|
|
24
|
+
roots: z.array(z.string()),
|
|
25
|
+
executables: z.array(z.string()),
|
|
26
|
+
environment_names: z.array(z.string()),
|
|
27
|
+
origins: z.array(z.string()).optional(),
|
|
28
|
+
network: z.enum(["none", "loopback", "external"]),
|
|
29
|
+
mount: z.boolean(),
|
|
30
|
+
lifetime: z.literal("project"),
|
|
31
|
+
operation_identity: z.string().nullable(),
|
|
32
|
+
expires_at: z.iso.datetime().nullable(),
|
|
33
|
+
});
|
|
34
|
+
const storeSchema = z.object({
|
|
35
|
+
schema_version: z.literal(1),
|
|
36
|
+
project_id: z.string().regex(/^project_[a-f0-9]{64}$/u),
|
|
37
|
+
project_root: z.string(),
|
|
38
|
+
grants: z.array(grantSchema).max(1_000),
|
|
39
|
+
});
|
|
40
|
+
/** Owner-only project policy persistence failure. */
|
|
41
|
+
export class ProjectPermissionStoreError extends Error {
|
|
42
|
+
reason;
|
|
43
|
+
_tag = "ProjectPermissionStoreError";
|
|
44
|
+
constructor(reason, options) {
|
|
45
|
+
super(`Project permission store failed: ${reason}`, options);
|
|
46
|
+
this.reason = reason;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Derive relocation-explicit project identity from its canonical root. */
|
|
50
|
+
const identifyPermissionProject = async (projectRoot) => {
|
|
51
|
+
try {
|
|
52
|
+
const root = await realpath(projectRoot);
|
|
53
|
+
return ok({
|
|
54
|
+
id: `project_${createHash("sha256").update(root).digest("hex")}`,
|
|
55
|
+
root,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (cause) {
|
|
59
|
+
return err(new ProjectPermissionStoreError("project_not_found", { cause }));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
/** Read and validate an owner-only store bound to the requested project. */
|
|
63
|
+
export const readProjectPermissionStore = async (path, projectRoot) => {
|
|
64
|
+
const project = await identifyPermissionProject(projectRoot);
|
|
65
|
+
if (!project.ok)
|
|
66
|
+
return project;
|
|
67
|
+
try {
|
|
68
|
+
const metadata = await stat(path);
|
|
69
|
+
if ((metadata.mode & 0o077) !== 0)
|
|
70
|
+
return err(new ProjectPermissionStoreError("not_owner_only"));
|
|
71
|
+
const encoded = await readFile(path, "utf8");
|
|
72
|
+
let decoded;
|
|
73
|
+
try {
|
|
74
|
+
decoded = JSON.parse(encoded);
|
|
75
|
+
}
|
|
76
|
+
catch (cause) {
|
|
77
|
+
return err(new ProjectPermissionStoreError("invalid", { cause }));
|
|
78
|
+
}
|
|
79
|
+
const parsed = storeSchema.safeParse(decoded);
|
|
80
|
+
if (!parsed.success ||
|
|
81
|
+
parsed.data.project_id !== project.value.id ||
|
|
82
|
+
parsed.data.project_root !== project.value.root)
|
|
83
|
+
return err(new ProjectPermissionStoreError("invalid"));
|
|
84
|
+
return ok(parsed.data);
|
|
85
|
+
}
|
|
86
|
+
catch (cause) {
|
|
87
|
+
if (isNotFound(cause))
|
|
88
|
+
return ok(null);
|
|
89
|
+
return err(new ProjectPermissionStoreError("io", { cause }));
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
/** Atomically replace explicit project grants with owner-only permissions. */
|
|
93
|
+
export const writeProjectPermissionStore = async (path, projectRoot, grants) => {
|
|
94
|
+
const project = await identifyPermissionProject(projectRoot);
|
|
95
|
+
if (!project.ok)
|
|
96
|
+
return project;
|
|
97
|
+
const candidate = storeSchema.safeParse({
|
|
98
|
+
schema_version: 1,
|
|
99
|
+
project_id: project.value.id,
|
|
100
|
+
project_root: project.value.root,
|
|
101
|
+
grants,
|
|
102
|
+
});
|
|
103
|
+
if (!candidate.success)
|
|
104
|
+
return err(new ProjectPermissionStoreError("invalid", { cause: candidate.error }));
|
|
105
|
+
try {
|
|
106
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
107
|
+
await writeFileAtomic(path, `${JSON.stringify(candidate.data, null, 2)}\n`, {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
mode: 0o600,
|
|
110
|
+
});
|
|
111
|
+
const metadata = await stat(path);
|
|
112
|
+
if ((metadata.mode & 0o077) !== 0) {
|
|
113
|
+
return err(new ProjectPermissionStoreError("not_owner_only"));
|
|
114
|
+
}
|
|
115
|
+
return ok(candidate.data);
|
|
116
|
+
}
|
|
117
|
+
catch (cause) {
|
|
118
|
+
return err(new ProjectPermissionStoreError("io", { cause }));
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/** Revoke one grant while serializing the complete read-modify-write cycle. */
|
|
122
|
+
export const revokeProjectPermissionGrant = async (path, projectRoot, grantId) => {
|
|
123
|
+
let lock;
|
|
124
|
+
try {
|
|
125
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
126
|
+
const acquired = await acquirePermissionStoreLock(path);
|
|
127
|
+
if (!acquired.ok)
|
|
128
|
+
return acquired;
|
|
129
|
+
lock = acquired.value;
|
|
130
|
+
const current = await readProjectPermissionStore(path, projectRoot);
|
|
131
|
+
if (!current.ok)
|
|
132
|
+
return current;
|
|
133
|
+
const grants = current.value?.grants ?? [];
|
|
134
|
+
const retained = grants.filter(({ grant_id }) => grant_id !== grantId);
|
|
135
|
+
if (retained.length === grants.length)
|
|
136
|
+
return ok(false);
|
|
137
|
+
const written = await writeProjectPermissionStore(path, projectRoot, retained);
|
|
138
|
+
return written.ok ? ok(true) : written;
|
|
139
|
+
}
|
|
140
|
+
catch (cause) {
|
|
141
|
+
return err(new ProjectPermissionStoreError("io", { cause }));
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
if (lock !== undefined)
|
|
145
|
+
await releasePermissionStoreLock(lock);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const acquirePermissionStoreLock = async (destination) => {
|
|
149
|
+
const path = `${destination}.lock`;
|
|
150
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
151
|
+
try {
|
|
152
|
+
return ok(await createPermissionStoreLock(path));
|
|
153
|
+
}
|
|
154
|
+
catch (cause) {
|
|
155
|
+
if (!isAlreadyExists(cause))
|
|
156
|
+
return err(new ProjectPermissionStoreError("io", { cause }));
|
|
157
|
+
if (await removeStalePermissionStoreLock(path))
|
|
158
|
+
continue;
|
|
159
|
+
if (attempt === 99)
|
|
160
|
+
return err(new ProjectPermissionStoreError("locked", { cause }));
|
|
161
|
+
await delay(10);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return err(new ProjectPermissionStoreError("locked"));
|
|
165
|
+
};
|
|
166
|
+
const createPermissionStoreLock = async (path) => {
|
|
167
|
+
const handle = await open(path, "wx", 0o600);
|
|
168
|
+
try {
|
|
169
|
+
await handle.writeFile(`${String(process.pid)}\n`, "utf8");
|
|
170
|
+
await handle.sync();
|
|
171
|
+
return { path, handle };
|
|
172
|
+
}
|
|
173
|
+
catch (cause) {
|
|
174
|
+
await handle.close().catch(() => undefined);
|
|
175
|
+
await unlink(path).catch(() => undefined);
|
|
176
|
+
throw cause;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
const removeStalePermissionStoreLock = async (path) => {
|
|
180
|
+
try {
|
|
181
|
+
const metadata = await lstat(path);
|
|
182
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > 32)
|
|
183
|
+
return false;
|
|
184
|
+
const encoded = (await readFile(path, "utf8")).trim();
|
|
185
|
+
if (!/^[1-9][0-9]{0,9}$/u.test(encoded))
|
|
186
|
+
return false;
|
|
187
|
+
if (processIsAlive(Number(encoded)))
|
|
188
|
+
return false;
|
|
189
|
+
await unlink(path);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
const processIsAlive = (pid) => {
|
|
197
|
+
try {
|
|
198
|
+
process.kill(pid, 0);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
catch (cause) {
|
|
202
|
+
return errorCode(cause) !== "ESRCH";
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const releasePermissionStoreLock = async (lock) => {
|
|
206
|
+
await lock.handle.close().catch(() => undefined);
|
|
207
|
+
await unlink(lock.path).catch(() => undefined);
|
|
208
|
+
};
|
|
209
|
+
const isNotFound = (cause) => errorCode(cause) === "ENOENT";
|
|
210
|
+
const isAlreadyExists = (cause) => errorCode(cause) === "EEXIST";
|
|
211
|
+
const errorCode = (cause) => typeof cause === "object" && cause !== null && "code" in cause
|
|
212
|
+
? cause.code
|
|
213
|
+
: undefined;
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
import { access, copyFile, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname,
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import writeFileAtomic from "write-file-atomic";
|
|
6
6
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
@@ -10,6 +10,7 @@ import { runDoctor, systemDoctorHost } from "./Doctor.js";
|
|
|
10
10
|
import { installLinuxHopper, readLinuxDistribution, } from "./LinuxHopper.js";
|
|
11
11
|
import { installMacHopper } from "./MacHopper.js";
|
|
12
12
|
import { configureDetectedClients } from "./SetupClients.js";
|
|
13
|
+
import { supportedClients } from "./SupportedClients.js";
|
|
13
14
|
import { installCanonicalSkill } from "./SetupSkill.js";
|
|
14
15
|
import { setupPlan } from "./SetupPlan.js";
|
|
15
16
|
import { setupInstallFailure, } from "./SetupInstallFailure.js";
|
|
@@ -160,46 +161,6 @@ export const detectClients = async (home) => {
|
|
|
160
161
|
detected.push(candidate);
|
|
161
162
|
return detected;
|
|
162
163
|
};
|
|
163
|
-
/** Describe every client location that setup or uninstall may own. */
|
|
164
|
-
export const supportedClients = (home) => [
|
|
165
|
-
{
|
|
166
|
-
name: "claude_code",
|
|
167
|
-
configPath: join(home, ".claude.json"),
|
|
168
|
-
markerPath: join(home, ".claude"),
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
name: "claude_desktop",
|
|
172
|
-
configPath: join(home, "Library/Application Support/Claude/claude_desktop_config.json"),
|
|
173
|
-
markerPath: join(home, "Library/Application Support/Claude"),
|
|
174
|
-
},
|
|
175
|
-
{
|
|
176
|
-
name: "codex",
|
|
177
|
-
configPath: join(home, ".codex/config.toml"),
|
|
178
|
-
markerPath: join(home, ".codex"),
|
|
179
|
-
format: "toml",
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
name: "cursor",
|
|
183
|
-
configPath: join(home, ".cursor/mcp.json"),
|
|
184
|
-
markerPath: join(home, ".cursor"),
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
name: "gemini_cli",
|
|
188
|
-
configPath: join(home, ".gemini/settings.json"),
|
|
189
|
-
markerPath: join(home, ".gemini"),
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
name: "windsurf",
|
|
193
|
-
configPath: join(home, ".codeium/windsurf/mcp_config.json"),
|
|
194
|
-
markerPath: join(home, ".codeium/windsurf"),
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
name: "devin",
|
|
198
|
-
configPath: join(home, ".devin"),
|
|
199
|
-
markerPath: join(home, ".devin"),
|
|
200
|
-
format: "unsupported",
|
|
201
|
-
},
|
|
202
|
-
];
|
|
203
164
|
/** Back up, atomically update, and semantically read back one JSON MCP configuration. */
|
|
204
165
|
export const configureJsonClient = async (client, hopperPath, command = [
|
|
205
166
|
"npx",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
/** Describe every client location that setup, doctor, or uninstall may inspect. */
|
|
3
|
+
export const supportedClients = (home) => [
|
|
4
|
+
{
|
|
5
|
+
name: "claude_code",
|
|
6
|
+
configPath: join(home, ".claude.json"),
|
|
7
|
+
markerPath: join(home, ".claude"),
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: "claude_desktop",
|
|
11
|
+
configPath: join(home, "Library/Application Support/Claude/claude_desktop_config.json"),
|
|
12
|
+
markerPath: join(home, "Library/Application Support/Claude"),
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "codex",
|
|
16
|
+
configPath: join(home, ".codex/config.toml"),
|
|
17
|
+
markerPath: join(home, ".codex"),
|
|
18
|
+
format: "toml",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "cursor",
|
|
22
|
+
configPath: join(home, ".cursor/mcp.json"),
|
|
23
|
+
markerPath: join(home, ".cursor"),
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "gemini_cli",
|
|
27
|
+
configPath: join(home, ".gemini/settings.json"),
|
|
28
|
+
markerPath: join(home, ".gemini"),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: "windsurf",
|
|
32
|
+
configPath: join(home, ".codeium/windsurf/mcp_config.json"),
|
|
33
|
+
markerPath: join(home, ".codeium/windsurf"),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "devin",
|
|
37
|
+
configPath: join(home, ".devin"),
|
|
38
|
+
markerPath: join(home, ".devin"),
|
|
39
|
+
format: "unsupported",
|
|
40
|
+
},
|
|
41
|
+
];
|
|
@@ -49,7 +49,7 @@ export class TerminalRenderer {
|
|
|
49
49
|
/** Await all queued parsing and return immutable rendered observations. */
|
|
50
50
|
async frames() {
|
|
51
51
|
await this.#pending;
|
|
52
|
-
return this.#frames;
|
|
52
|
+
return structuredClone(this.#frames);
|
|
53
53
|
}
|
|
54
54
|
/** Whether a rendered observation exceeded its independent capture budget. */
|
|
55
55
|
truncated() {
|
|
@@ -5,7 +5,7 @@ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
|
5
5
|
import writeFileAtomic from "write-file-atomic";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
8
|
-
import { supportedClients } from "./
|
|
8
|
+
import { supportedClients } from "./SupportedClients.js";
|
|
9
9
|
const systemFileSystem = {
|
|
10
10
|
readText: (path) => readFile(path, "utf8"),
|
|
11
11
|
copy: (source, destination) => copyFile(source, destination),
|
|
@@ -42,6 +42,8 @@ export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), out
|
|
|
42
42
|
versionCheck: latestVersion === undefined ? "unavailable" : "available",
|
|
43
43
|
installMethod: "npm",
|
|
44
44
|
command: UPGRADE_COMMAND,
|
|
45
|
+
clientRestartRequired: true,
|
|
46
|
+
remediation: "Rerun rea setup to refresh registrations and the skill, then restart clients that may retain an older MCP server.",
|
|
45
47
|
};
|
|
46
48
|
};
|
|
47
49
|
/** Create the npm registry and subprocess effects for a production upgrade. */
|
|
@@ -11,7 +11,7 @@ import { silentLogger } from "../logger.js";
|
|
|
11
11
|
*/
|
|
12
12
|
export const createBinarySession = (config, logger = silentLogger) => {
|
|
13
13
|
return new BinarySession(new CompositeProvider([
|
|
14
|
-
new ArtifactProvider(config.artifactNativeMountEnabled),
|
|
14
|
+
new ArtifactProvider(config.artifactNativeMountEnabled, config.artifactIntegrityContinueEnabled),
|
|
15
15
|
new NativeMacOSProvider(),
|
|
16
16
|
new HopperProvider(config, logger),
|
|
17
17
|
]), { snapshotsEnabled: config.hopperLoaderArgs.length === 0 });
|
|
@@ -10,8 +10,10 @@ const IDENTITY = Object.freeze(ARTIFACT_GRAPH_PROVIDER);
|
|
|
10
10
|
/** Read-only inventory and exclusively owned extraction provider. */
|
|
11
11
|
export class ArtifactProvider {
|
|
12
12
|
nativeMountEnabled;
|
|
13
|
-
|
|
13
|
+
integrityContinueEnabled;
|
|
14
|
+
constructor(nativeMountEnabled = false, integrityContinueEnabled = false) {
|
|
14
15
|
this.nativeMountEnabled = nativeMountEnabled;
|
|
16
|
+
this.integrityContinueEnabled = integrityContinueEnabled;
|
|
15
17
|
}
|
|
16
18
|
#capabilities = Object.freeze(ARTIFACT_TOOL_CONTRACTS.map((contract) => Object.freeze({
|
|
17
19
|
provider: IDENTITY,
|
|
@@ -48,15 +50,17 @@ export class ArtifactProvider {
|
|
|
48
50
|
return this.#capabilities;
|
|
49
51
|
}
|
|
50
52
|
createClient(target) {
|
|
51
|
-
return new ArtifactClient(target, this.nativeMountEnabled);
|
|
53
|
+
return new ArtifactClient(target, this.nativeMountEnabled, this.integrityContinueEnabled);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
class ArtifactClient {
|
|
55
57
|
target;
|
|
56
58
|
nativeMountEnabled;
|
|
57
|
-
|
|
59
|
+
integrityContinueEnabled;
|
|
60
|
+
constructor(target, nativeMountEnabled, integrityContinueEnabled) {
|
|
58
61
|
this.target = target;
|
|
59
62
|
this.nativeMountEnabled = nativeMountEnabled;
|
|
63
|
+
this.integrityContinueEnabled = integrityContinueEnabled;
|
|
60
64
|
}
|
|
61
65
|
async execute(operation, parameters, options) {
|
|
62
66
|
if (operation === "health")
|
|
@@ -99,6 +103,12 @@ class ArtifactClient {
|
|
|
99
103
|
nativeMountApproved: parsed.native_mount_approved === true,
|
|
100
104
|
nativeMountEnabled: this.nativeMountEnabled,
|
|
101
105
|
},
|
|
106
|
+
integrity: {
|
|
107
|
+
mode: parsed.integrity_policy,
|
|
108
|
+
approved: parsed.integrity_continue_approved === true,
|
|
109
|
+
enabled: this.integrityContinueEnabled,
|
|
110
|
+
maxMismatches: parsed.max_integrity_mismatches,
|
|
111
|
+
},
|
|
102
112
|
});
|
|
103
113
|
return ok(createAnalysisExecution(result, IDENTITY, {
|
|
104
114
|
rawResult: null,
|
|
@@ -59,7 +59,7 @@ export class MachOSliceArtifactReader {
|
|
|
59
59
|
return Promise.resolve(createReadStream(this.path, { start: offset, end: offset + size - 1 }));
|
|
60
60
|
}
|
|
61
61
|
provenance() {
|
|
62
|
-
return this.#command === undefined ? [] : [this.#command];
|
|
62
|
+
return this.#command === undefined ? [] : [structuredClone(this.#command)];
|
|
63
63
|
}
|
|
64
64
|
close() {
|
|
65
65
|
return Promise.resolve();
|
|
@@ -59,7 +59,7 @@ export class SafeOutputTree {
|
|
|
59
59
|
return this.#outputRoot;
|
|
60
60
|
}
|
|
61
61
|
get cleanup() {
|
|
62
|
-
return this.#cleanup;
|
|
62
|
+
return structuredClone(this.#cleanup);
|
|
63
63
|
}
|
|
64
64
|
/** Stream one selected regular file with exact byte and digest verification. */
|
|
65
65
|
async write(relativePath, source, expectedSha256, signal) {
|
|
@@ -123,7 +123,7 @@ export class SafeOutputTree {
|
|
|
123
123
|
/** Remove only this operation's unsealed tree and verify absence. */
|
|
124
124
|
async rollback() {
|
|
125
125
|
if (this.#published)
|
|
126
|
-
return this.#cleanup;
|
|
126
|
+
return structuredClone(this.#cleanup);
|
|
127
127
|
await rm(this.#stagingRoot, { recursive: true, force: true });
|
|
128
128
|
const absent = await isAbsent(this.#stagingRoot);
|
|
129
129
|
this.#cleanup = {
|
|
@@ -133,7 +133,7 @@ export class SafeOutputTree {
|
|
|
133
133
|
};
|
|
134
134
|
if (!absent)
|
|
135
135
|
throw new ArtifactReaderFailure("integrity", "Extraction output cleanup could not be verified");
|
|
136
|
-
return this.#cleanup;
|
|
136
|
+
return structuredClone(this.#cleanup);
|
|
137
137
|
}
|
|
138
138
|
async #prepareParent(relativePath) {
|
|
139
139
|
const parts = relativePath.split("/");
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { browserTargetListSchema, sanitizeBrowserUrl, webPageInspectionSchema, } from "../domain/browserObservation.js";
|
|
2
|
+
import { AnalysisError, BrowserObservationError, ProviderAdapterError, } from "../domain/errors.js";
|
|
3
|
+
import { err, ok } from "../domain/result.js";
|
|
4
|
+
import { CdpConnection } from "./CdpConnection.js";
|
|
5
|
+
import { discoverCdpEndpoint, } from "./CdpEndpoint.js";
|
|
6
|
+
import { capturePage } from "./CdpPageCapture.js";
|
|
7
|
+
const IDENTITY = {
|
|
8
|
+
id: "rea-cdp-browser",
|
|
9
|
+
name: "REA Chrome DevTools Protocol observation provider",
|
|
10
|
+
version: "1",
|
|
11
|
+
};
|
|
12
|
+
/** Passive, origin-scoped browser observation through a user-owned CDP endpoint. */
|
|
13
|
+
export class CdpBrowserProvider {
|
|
14
|
+
identity() {
|
|
15
|
+
return IDENTITY;
|
|
16
|
+
}
|
|
17
|
+
async listTargets(input, options = {}) {
|
|
18
|
+
try {
|
|
19
|
+
const discovery = await discoverCdpEndpoint(input.cdp_endpoint, "list_browser_targets", options.signal);
|
|
20
|
+
const allowedOrigins = new Set(input.allowed_origins);
|
|
21
|
+
const selected = selectTargets(discovery, allowedOrigins);
|
|
22
|
+
const items = selected.allowed.slice(input.offset, input.offset + input.limit);
|
|
23
|
+
const nextOffset = input.offset + items.length;
|
|
24
|
+
return ok(browserTargetListSchema.parse({
|
|
25
|
+
schema_version: 1,
|
|
26
|
+
browser: discovery.version,
|
|
27
|
+
targets: {
|
|
28
|
+
items,
|
|
29
|
+
offset: input.offset,
|
|
30
|
+
limit: input.limit,
|
|
31
|
+
total: selected.allowed.length,
|
|
32
|
+
next_offset: nextOffset < selected.allowed.length ? nextOffset : null,
|
|
33
|
+
has_more: nextOffset < selected.allowed.length,
|
|
34
|
+
},
|
|
35
|
+
excluded: selected.excluded,
|
|
36
|
+
limitations: [
|
|
37
|
+
"Only page targets whose current URL matches an approved exact origin are listed.",
|
|
38
|
+
],
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
catch (cause) {
|
|
42
|
+
return err(providerError(cause, "list_browser_targets"));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async inspectPage(input, options = {}) {
|
|
46
|
+
let connection;
|
|
47
|
+
let sessionId;
|
|
48
|
+
try {
|
|
49
|
+
const discovery = await discoverCdpEndpoint(input.cdp_endpoint, "inspect_web_page", options.signal);
|
|
50
|
+
const target = authorizeTarget(discovery, input);
|
|
51
|
+
connection = await CdpConnection.connect(discovery.browserWebSocketUrl, options.signal);
|
|
52
|
+
const attached = await connection.send("Target.attachToTarget", { targetId: target.id, flatten: true }, undefined, options.signal);
|
|
53
|
+
sessionId = attachedSessionId(attached);
|
|
54
|
+
const result = await capturePage({
|
|
55
|
+
connection,
|
|
56
|
+
sessionId,
|
|
57
|
+
discovery,
|
|
58
|
+
target,
|
|
59
|
+
input,
|
|
60
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
61
|
+
...(options.progress === undefined
|
|
62
|
+
? {}
|
|
63
|
+
: { progress: options.progress }),
|
|
64
|
+
});
|
|
65
|
+
return ok(webPageInspectionSchema.parse(result));
|
|
66
|
+
}
|
|
67
|
+
catch (cause) {
|
|
68
|
+
return err(providerError(cause, "inspect_web_page"));
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (connection !== undefined) {
|
|
72
|
+
if (sessionId !== undefined)
|
|
73
|
+
await bestEffortCleanup(connection, sessionId);
|
|
74
|
+
await connection.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const selectTargets = (discovery, allowedOrigins) => {
|
|
80
|
+
const allowed = [];
|
|
81
|
+
let disallowedOrigin = 0;
|
|
82
|
+
let unsupportedUrl = 0;
|
|
83
|
+
let nonPage = 0;
|
|
84
|
+
for (const target of discovery.targets) {
|
|
85
|
+
if (target.type !== "page") {
|
|
86
|
+
nonPage += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const url = sanitizeBrowserUrl(target.url);
|
|
90
|
+
if (url.origin === null) {
|
|
91
|
+
unsupportedUrl += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!allowedOrigins.has(url.origin)) {
|
|
95
|
+
disallowedOrigin += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
allowed.push({
|
|
99
|
+
target_id: target.id,
|
|
100
|
+
type: target.type,
|
|
101
|
+
title: target.title.slice(0, 16_384),
|
|
102
|
+
url: url.url,
|
|
103
|
+
origin: url.origin,
|
|
104
|
+
attached: target.attached,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
allowed.sort((left, right) => left.target_id.localeCompare(right.target_id));
|
|
108
|
+
return {
|
|
109
|
+
allowed,
|
|
110
|
+
excluded: {
|
|
111
|
+
disallowed_origin: disallowedOrigin,
|
|
112
|
+
unsupported_url: unsupportedUrl,
|
|
113
|
+
non_page: nonPage,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
const authorizeTarget = (discovery, input) => {
|
|
118
|
+
const target = discovery.targets.find((candidate) => candidate.id === input.target_id);
|
|
119
|
+
if (target === undefined)
|
|
120
|
+
throw new BrowserObservationError("inspect_web_page", "target_not_found");
|
|
121
|
+
const origin = sanitizeBrowserUrl(target.url).origin;
|
|
122
|
+
if (target.type !== "page" ||
|
|
123
|
+
origin === null ||
|
|
124
|
+
!input.allowed_origins.includes(origin))
|
|
125
|
+
throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
|
|
126
|
+
return target;
|
|
127
|
+
};
|
|
128
|
+
const attachedSessionId = (value) => {
|
|
129
|
+
if (typeof value !== "object" || value === null || !("sessionId" in value))
|
|
130
|
+
throw new BrowserObservationError("inspect_web_page", "protocol_error");
|
|
131
|
+
if (typeof value.sessionId !== "string" || value.sessionId.length > 256)
|
|
132
|
+
throw new BrowserObservationError("inspect_web_page", "protocol_error");
|
|
133
|
+
return value.sessionId;
|
|
134
|
+
};
|
|
135
|
+
const bestEffortCleanup = async (connection, sessionId) => {
|
|
136
|
+
for (const method of [
|
|
137
|
+
"Network.disable",
|
|
138
|
+
"Debugger.disable",
|
|
139
|
+
"Runtime.disable",
|
|
140
|
+
"Page.disable",
|
|
141
|
+
]) {
|
|
142
|
+
try {
|
|
143
|
+
await connection.send(method, {}, sessionId);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Cleanup continues so detach is still attempted.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await connection.send("Target.detachFromTarget", { sessionId });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Closing REA's socket is the final non-destructive cleanup boundary.
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const providerError = (cause, operation) => cause instanceof AnalysisError
|
|
157
|
+
? cause
|
|
158
|
+
: new ProviderAdapterError(IDENTITY.id, operation, { cause });
|