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
|
@@ -43,6 +43,30 @@ export const setupInstallFailure = (reason) => {
|
|
|
43
43
|
code: "setup_cancelled",
|
|
44
44
|
remediation: "Setup was cancelled before Hopper was installed. Rerun rea setup when ready.",
|
|
45
45
|
};
|
|
46
|
+
case "destination_exists":
|
|
47
|
+
return {
|
|
48
|
+
status: "failed",
|
|
49
|
+
code: "destination_exists",
|
|
50
|
+
remediation: "The Hopper installation destination already exists and was not replaced. Reuse the existing installation or move it aside, then rerun rea setup.",
|
|
51
|
+
};
|
|
52
|
+
case "mount":
|
|
53
|
+
return {
|
|
54
|
+
status: "failed",
|
|
55
|
+
code: "mount_failed",
|
|
56
|
+
remediation: "The verified Hopper disk image could not be mounted. Eject any stale Hopper image, check local disk access, then rerun rea setup.",
|
|
57
|
+
};
|
|
58
|
+
case "bundle":
|
|
59
|
+
return {
|
|
60
|
+
status: "failed",
|
|
61
|
+
code: "bundle_invalid",
|
|
62
|
+
remediation: "The mounted package did not contain the expected Hopper application bundle. Retry setup; if it repeats, update REA before installing.",
|
|
63
|
+
};
|
|
64
|
+
case "copy":
|
|
65
|
+
return {
|
|
66
|
+
status: "failed",
|
|
67
|
+
code: "copy_failed",
|
|
68
|
+
remediation: "The verified Hopper application could not be copied to the installation destination. Check destination permissions and free space, then rerun rea setup.",
|
|
69
|
+
};
|
|
46
70
|
default:
|
|
47
71
|
return {
|
|
48
72
|
status: "failed",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Build the complete setup mutation plan before approval. */
|
|
2
|
-
export const setupPlan = (platform, installHopper, clients) => [
|
|
2
|
+
export const setupPlan = (platform, installHopper, installSkill, clients) => [
|
|
3
3
|
...(installHopper
|
|
4
4
|
? [
|
|
5
5
|
{
|
|
@@ -22,10 +22,14 @@ export const setupPlan = (platform, installHopper, clients) => [
|
|
|
22
22
|
detail: `Add the REA MCP registration for ${client.name}; preserve unrelated configuration.`,
|
|
23
23
|
external: false,
|
|
24
24
|
})),
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
...(installSkill
|
|
26
|
+
? [
|
|
27
|
+
{
|
|
28
|
+
kind: "install_skill",
|
|
29
|
+
target: "~/.agents/skills/rea-analysis/SKILL.md",
|
|
30
|
+
detail: "Install or update the bundled REA analysis skill.",
|
|
31
|
+
external: false,
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
: []),
|
|
31
35
|
];
|
|
@@ -2,13 +2,30 @@ import { mkdir, readFile, rm } from "node:fs/promises";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import writeFileAtomic from "write-file-atomic";
|
|
4
4
|
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
5
|
+
const canonicalSkillFiles = async (home) => ({
|
|
6
|
+
destination: join(home, ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md"),
|
|
7
|
+
content: await readFile(new URL(`../../skills/${PRODUCT_IDENTITY.skillName}/SKILL.md`, import.meta.url), "utf8"),
|
|
8
|
+
});
|
|
9
|
+
/** Report whether setup would change the managed REA skill. */
|
|
10
|
+
export const canonicalSkillNeedsInstall = async (home) => {
|
|
11
|
+
try {
|
|
12
|
+
const { content, destination } = await canonicalSkillFiles(home);
|
|
13
|
+
return (await readFile(destination, "utf8")) !== content;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
5
19
|
/** Transactionally install or upgrade the versioned canonical REA skill. */
|
|
6
20
|
export const installCanonicalSkill = async (home) => {
|
|
7
|
-
|
|
8
|
-
|
|
21
|
+
let destination = join(home, ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md");
|
|
22
|
+
let backup = `${destination}.rea.backup`;
|
|
9
23
|
let original;
|
|
10
24
|
try {
|
|
11
|
-
const
|
|
25
|
+
const canonical = await canonicalSkillFiles(home);
|
|
26
|
+
destination = canonical.destination;
|
|
27
|
+
backup = `${destination}.rea.backup`;
|
|
28
|
+
const { content } = canonical;
|
|
12
29
|
original = await readFile(destination, "utf8").catch(() => undefined);
|
|
13
30
|
if (original === content)
|
|
14
31
|
return "unchanged";
|
|
@@ -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() {
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { copyFile, lstat, readFile, rm } from "node:fs/promises";
|
|
1
|
+
import { copyFile, lstat, readFile, realpath, rm } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, isAbsolute, join } from "node:path";
|
|
4
4
|
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 { resolveClientConfigTransactionPath } from "./ClientConfigPath.js";
|
|
8
9
|
import { supportedClients } from "./SupportedClients.js";
|
|
9
10
|
const systemFileSystem = {
|
|
10
11
|
readText: (path) => readFile(path, "utf8"),
|
|
11
12
|
copy: (source, destination) => copyFile(source, destination),
|
|
12
13
|
writeText: (path, contents) => writeFileAtomic(path, contents, { encoding: "utf8" }),
|
|
13
14
|
stat: (path) => lstat(path),
|
|
15
|
+
realpath: (path) => realpath(path),
|
|
14
16
|
remove: (path) => rm(path, { recursive: true }),
|
|
15
17
|
};
|
|
16
18
|
/** Remove only REA-owned registrations and managed files, optionally including local state. */
|
|
@@ -46,9 +48,13 @@ export const systemUninstallHost = (home = homedir(), fileSystem = systemFileSys
|
|
|
46
48
|
const removeClient = async (client, fileSystem) => {
|
|
47
49
|
if (client.format === "unsupported")
|
|
48
50
|
return item(client.name, "skipped", "This client has no documented local MCP configuration boundary.");
|
|
51
|
+
const resolved = await resolveUninstallConfigPath(client, fileSystem);
|
|
52
|
+
if (typeof resolved !== "string")
|
|
53
|
+
return resolved;
|
|
54
|
+
const transactionPath = resolved;
|
|
49
55
|
let original;
|
|
50
56
|
try {
|
|
51
|
-
original = await fileSystem.readText(
|
|
57
|
+
original = await fileSystem.readText(transactionPath);
|
|
52
58
|
}
|
|
53
59
|
catch (cause) {
|
|
54
60
|
return isMissing(cause)
|
|
@@ -76,7 +82,7 @@ const removeClient = async (client, fileSystem) => {
|
|
|
76
82
|
document[key] = remaining;
|
|
77
83
|
const backupPath = `${client.configPath}.rea.backup`;
|
|
78
84
|
try {
|
|
79
|
-
await fileSystem.copy(
|
|
85
|
+
await fileSystem.copy(transactionPath, backupPath);
|
|
80
86
|
}
|
|
81
87
|
catch {
|
|
82
88
|
return item(client.name, "failed", "Configuration could not be backed up, so no change was made. Check file permissions, then rerun uninstall.");
|
|
@@ -85,17 +91,17 @@ const removeClient = async (client, fileSystem) => {
|
|
|
85
91
|
const encoded = client.format === "toml"
|
|
86
92
|
? stringifyToml(document)
|
|
87
93
|
: `${JSON.stringify(document, null, 2)}\n`;
|
|
88
|
-
await fileSystem.writeText(
|
|
94
|
+
await fileSystem.writeText(transactionPath, encoded);
|
|
89
95
|
const readback = objectSchema.parse(client.format === "toml"
|
|
90
|
-
? parseToml(await fileSystem.readText(
|
|
91
|
-
: JSON.parse(await fileSystem.readText(
|
|
96
|
+
? parseToml(await fileSystem.readText(transactionPath))
|
|
97
|
+
: JSON.parse(await fileSystem.readText(transactionPath)));
|
|
92
98
|
if (PRODUCT_IDENTITY.mcpServerKey in optionalObject(readback[key]))
|
|
93
99
|
throw new Error("registration readback mismatch");
|
|
94
100
|
return item(client.name, "removed", `Removed registration from ${client.configPath}.`);
|
|
95
101
|
}
|
|
96
102
|
catch {
|
|
97
103
|
try {
|
|
98
|
-
await fileSystem.writeText(
|
|
104
|
+
await fileSystem.writeText(transactionPath, original);
|
|
99
105
|
}
|
|
100
106
|
catch {
|
|
101
107
|
return item(client.name, "failed", "Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall.");
|
|
@@ -103,6 +109,13 @@ const removeClient = async (client, fileSystem) => {
|
|
|
103
109
|
return item(client.name, "failed", "Configuration could not be updated. The original was restored and its `.rea.backup` was retained. Repair the configuration or restore the backup, then rerun uninstall.");
|
|
104
110
|
}
|
|
105
111
|
};
|
|
112
|
+
const resolveUninstallConfigPath = async (client, fileSystem) => {
|
|
113
|
+
const path = await resolveClientConfigTransactionPath(client.configPath, fileSystem.realpath === undefined
|
|
114
|
+
? { lstat: fileSystem.stat }
|
|
115
|
+
: { lstat: fileSystem.stat, realpath: fileSystem.realpath });
|
|
116
|
+
return (path ??
|
|
117
|
+
item(client.name, "failed", "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 uninstall."));
|
|
118
|
+
};
|
|
106
119
|
/** Ownership rule for persistent MCP registrations created by REA setup. */
|
|
107
120
|
const isOwnedRegistration = (value) => {
|
|
108
121
|
const parsed = registrationSchema.safeParse(value);
|
|
@@ -3,6 +3,7 @@ import { realpath } from "node:fs/promises";
|
|
|
3
3
|
import { basename, dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { gt, valid } from "semver";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
@@ -11,7 +12,17 @@ const UPGRADE_COMMAND = "npm install --global rea-agents@latest";
|
|
|
11
12
|
/** Check the npm registry and update the same global REA installation. */
|
|
12
13
|
export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), output = "human") => {
|
|
13
14
|
const latestVersion = await host.latestVersion();
|
|
14
|
-
|
|
15
|
+
const currentIsValid = valid(currentVersion) !== null;
|
|
16
|
+
const latestIsValid = latestVersion !== undefined && valid(latestVersion) !== null;
|
|
17
|
+
if (!currentIsValid || !latestIsValid)
|
|
18
|
+
return {
|
|
19
|
+
status: "failed",
|
|
20
|
+
currentVersion,
|
|
21
|
+
latestVersion: latestVersion ?? null,
|
|
22
|
+
reason: "version-check",
|
|
23
|
+
remediation: "REA could not verify a newer registry release. Check npm registry access and version metadata, then rerun rea upgrade.",
|
|
24
|
+
};
|
|
25
|
+
if (!gt(latestVersion, currentVersion))
|
|
15
26
|
return {
|
|
16
27
|
status: "current",
|
|
17
28
|
currentVersion,
|
|
@@ -38,8 +49,8 @@ export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), out
|
|
|
38
49
|
return {
|
|
39
50
|
status: "upgraded",
|
|
40
51
|
previousVersion: currentVersion,
|
|
41
|
-
latestVersion
|
|
42
|
-
versionCheck:
|
|
52
|
+
latestVersion,
|
|
53
|
+
versionCheck: "available",
|
|
43
54
|
installMethod: "npm",
|
|
44
55
|
command: UPGRADE_COMMAND,
|
|
45
56
|
clientRestartRequired: true,
|
|
@@ -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,261 @@
|
|
|
1
|
+
import { browserTargetListSchema, sanitizeBrowserUrl, webPageInspectionSchema, } from "../domain/browserObservation.js";
|
|
2
|
+
import { analyzeCapturedWebBundle } from "../domain/webBundleAnalyzer.js";
|
|
3
|
+
import { webBundleAnalysisSchema, } from "../domain/webBundleAnalysis.js";
|
|
4
|
+
import { webObservationSessionSchema, } from "../domain/browserSession.js";
|
|
5
|
+
import { webMcpDiscoverySchema, } from "../domain/webMcpDiscovery.js";
|
|
6
|
+
import { compareWebCaptures, webCaptureDiffSchema, } from "../domain/webCaptureDiff.js";
|
|
7
|
+
import { webScreenshotDiffSchema, webScreenshotSchema, } from "../domain/webScreenshot.js";
|
|
8
|
+
import { AnalysisError, BrowserObservationError, ProviderAdapterError, } from "../domain/errors.js";
|
|
9
|
+
import { err, ok } from "../domain/result.js";
|
|
10
|
+
import { discoverCdpEndpoint, hasCdpTargetWebSocket, } from "./CdpEndpoint.js";
|
|
11
|
+
import { closeCdpTargetSession, openCdpTargetSession, } from "./CdpTargetSession.js";
|
|
12
|
+
import { capturePage } from "./CdpPageCapture.js";
|
|
13
|
+
import { fetchWebSourceMaps } from "./WebSourceMapFetcher.js";
|
|
14
|
+
import { observeCdpSession } from "./CdpObservationSession.js";
|
|
15
|
+
import { discoverWebMcp } from "./CdpWebMcpDiscovery.js";
|
|
16
|
+
import { captureCdpScreenshot } from "./CdpScreenshot.js";
|
|
17
|
+
import { comparePngScreenshots } from "./PngVisualDiff.js";
|
|
18
|
+
const IDENTITY = {
|
|
19
|
+
id: "rea-cdp-browser",
|
|
20
|
+
name: "REA Chrome DevTools Protocol observation provider",
|
|
21
|
+
version: "2",
|
|
22
|
+
};
|
|
23
|
+
const CLEANUP_DOMAINS = ["Network", "Debugger", "Runtime", "Page"];
|
|
24
|
+
/** Passive, origin-scoped browser observation through a user-owned CDP endpoint. */
|
|
25
|
+
export class CdpBrowserProvider {
|
|
26
|
+
identity() {
|
|
27
|
+
return IDENTITY;
|
|
28
|
+
}
|
|
29
|
+
async listTargets(input, options = {}) {
|
|
30
|
+
try {
|
|
31
|
+
const discovery = await discoverCdpEndpoint(input.cdp_endpoint, "list_browser_targets", options.signal);
|
|
32
|
+
const allowedOrigins = new Set(input.allowed_origins);
|
|
33
|
+
const selected = selectTargets(discovery, allowedOrigins);
|
|
34
|
+
const items = selected.allowed.slice(input.offset, input.offset + input.limit);
|
|
35
|
+
const nextOffset = input.offset + items.length;
|
|
36
|
+
return ok(browserTargetListSchema.parse({
|
|
37
|
+
schema_version: 1,
|
|
38
|
+
browser: discovery.version,
|
|
39
|
+
targets: {
|
|
40
|
+
items,
|
|
41
|
+
offset: input.offset,
|
|
42
|
+
limit: input.limit,
|
|
43
|
+
total: selected.allowed.length,
|
|
44
|
+
next_offset: nextOffset < selected.allowed.length ? nextOffset : null,
|
|
45
|
+
has_more: nextOffset < selected.allowed.length,
|
|
46
|
+
},
|
|
47
|
+
excluded: selected.excluded,
|
|
48
|
+
limitations: [
|
|
49
|
+
"Only page targets whose current URL matches an approved exact origin are listed.",
|
|
50
|
+
...(selected.unconnectable === 0
|
|
51
|
+
? []
|
|
52
|
+
: [
|
|
53
|
+
`${String(selected.unconnectable)} otherwise allowed page target(s) lacked a validated direct CDP WebSocket and were excluded.`,
|
|
54
|
+
]),
|
|
55
|
+
],
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
catch (cause) {
|
|
59
|
+
return err(providerError(cause, "list_browser_targets"));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async inspectPage(input, options = {}) {
|
|
63
|
+
try {
|
|
64
|
+
const captured = await this.#capture(input, options, "inspect_web_page");
|
|
65
|
+
return ok(webPageInspectionSchema.parse(captured.inspection));
|
|
66
|
+
}
|
|
67
|
+
catch (cause) {
|
|
68
|
+
return err(providerError(cause, "inspect_web_page"));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async analyzeBundle(input, options = {}) {
|
|
72
|
+
try {
|
|
73
|
+
const captured = await this.#capture(input, options, "analyze_web_bundle");
|
|
74
|
+
const sourceMaps = input.fetch_source_maps
|
|
75
|
+
? await fetchWebSourceMaps(captured.sourceMapRequests, input, options.signal)
|
|
76
|
+
: undefined;
|
|
77
|
+
return ok(webBundleAnalysisSchema.parse(analyzeCapturedWebBundle(captured.inspection, input, sourceMaps)));
|
|
78
|
+
}
|
|
79
|
+
catch (cause) {
|
|
80
|
+
return err(providerError(cause, "analyze_web_bundle"));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async observeSession(input, options = {}) {
|
|
84
|
+
let targetSession;
|
|
85
|
+
try {
|
|
86
|
+
const discovery = await discoverCdpEndpoint(input.cdp_endpoint, "observe_web_session", options.signal);
|
|
87
|
+
const target = authorizeTarget(discovery, input);
|
|
88
|
+
targetSession = await openCdpTargetSession(discovery, target, "observe_web_session", options.signal);
|
|
89
|
+
return ok(webObservationSessionSchema.parse(await observeCdpSession({
|
|
90
|
+
connection: targetSession.connection,
|
|
91
|
+
sessionId: targetSession.sessionId,
|
|
92
|
+
discovery,
|
|
93
|
+
target,
|
|
94
|
+
input,
|
|
95
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
96
|
+
...(options.progress === undefined
|
|
97
|
+
? {}
|
|
98
|
+
: { progress: options.progress }),
|
|
99
|
+
})));
|
|
100
|
+
}
|
|
101
|
+
catch (cause) {
|
|
102
|
+
return err(providerError(cause, "observe_web_session"));
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
if (targetSession !== undefined)
|
|
106
|
+
await closeCdpTargetSession(targetSession, CLEANUP_DOMAINS);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async discoverWebMcpTools(input, options = {}) {
|
|
110
|
+
let targetSession;
|
|
111
|
+
try {
|
|
112
|
+
const discovery = await discoverCdpEndpoint(input.cdp_endpoint, "discover_webmcp_tools", options.signal);
|
|
113
|
+
const target = authorizeTarget(discovery, input);
|
|
114
|
+
targetSession = await openCdpTargetSession(discovery, target, "discover_webmcp_tools", options.signal);
|
|
115
|
+
return ok(webMcpDiscoverySchema.parse(await discoverWebMcp({
|
|
116
|
+
connection: targetSession.connection,
|
|
117
|
+
sessionId: targetSession.sessionId,
|
|
118
|
+
discovery,
|
|
119
|
+
target,
|
|
120
|
+
input,
|
|
121
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
122
|
+
...(options.progress === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { progress: options.progress }),
|
|
125
|
+
})));
|
|
126
|
+
}
|
|
127
|
+
catch (cause) {
|
|
128
|
+
return err(providerError(cause, "discover_webmcp_tools"));
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (targetSession !== undefined)
|
|
132
|
+
await closeCdpTargetSession(targetSession, CLEANUP_DOMAINS);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async compareCaptures(input) {
|
|
136
|
+
try {
|
|
137
|
+
return ok(webCaptureDiffSchema.parse(compareWebCaptures(input)));
|
|
138
|
+
}
|
|
139
|
+
catch (cause) {
|
|
140
|
+
return err(providerError(cause, "compare_web_captures"));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async captureScreenshot(input, options = {}) {
|
|
144
|
+
let targetSession;
|
|
145
|
+
try {
|
|
146
|
+
const discovery = await discoverCdpEndpoint(input.cdp_endpoint, "capture_web_screenshot", options.signal);
|
|
147
|
+
const target = authorizeTarget(discovery, input);
|
|
148
|
+
targetSession = await openCdpTargetSession(discovery, target, "capture_web_screenshot", options.signal);
|
|
149
|
+
return ok(webScreenshotSchema.parse(await captureCdpScreenshot({
|
|
150
|
+
connection: targetSession.connection,
|
|
151
|
+
sessionId: targetSession.sessionId,
|
|
152
|
+
discovery,
|
|
153
|
+
target,
|
|
154
|
+
input,
|
|
155
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
156
|
+
...(options.progress === undefined
|
|
157
|
+
? {}
|
|
158
|
+
: { progress: options.progress }),
|
|
159
|
+
})));
|
|
160
|
+
}
|
|
161
|
+
catch (cause) {
|
|
162
|
+
return err(providerError(cause, "capture_web_screenshot"));
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
if (targetSession !== undefined)
|
|
166
|
+
await closeCdpTargetSession(targetSession, CLEANUP_DOMAINS);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async compareScreenshots(input) {
|
|
170
|
+
try {
|
|
171
|
+
return ok(webScreenshotDiffSchema.parse(comparePngScreenshots(input)));
|
|
172
|
+
}
|
|
173
|
+
catch (cause) {
|
|
174
|
+
return err(providerError(cause, "compare_web_screenshots"));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async #capture(input, options, operation) {
|
|
178
|
+
let targetSession;
|
|
179
|
+
try {
|
|
180
|
+
const discovery = await discoverCdpEndpoint(input.cdp_endpoint, operation, options.signal);
|
|
181
|
+
const target = authorizeTarget(discovery, input);
|
|
182
|
+
targetSession = await openCdpTargetSession(discovery, target, operation, options.signal);
|
|
183
|
+
return await capturePage({
|
|
184
|
+
connection: targetSession.connection,
|
|
185
|
+
sessionId: targetSession.sessionId,
|
|
186
|
+
operation,
|
|
187
|
+
discovery,
|
|
188
|
+
target,
|
|
189
|
+
input,
|
|
190
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
191
|
+
...(options.progress === undefined
|
|
192
|
+
? {}
|
|
193
|
+
: { progress: options.progress }),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
if (targetSession !== undefined)
|
|
198
|
+
await closeCdpTargetSession(targetSession, CLEANUP_DOMAINS);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const selectTargets = (discovery, allowedOrigins) => {
|
|
203
|
+
const allowed = [];
|
|
204
|
+
let disallowedOrigin = 0;
|
|
205
|
+
let unsupportedUrl = 0;
|
|
206
|
+
let nonPage = 0;
|
|
207
|
+
let unconnectable = 0;
|
|
208
|
+
for (const target of discovery.targets) {
|
|
209
|
+
if (target.type !== "page") {
|
|
210
|
+
nonPage += 1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const url = sanitizeBrowserUrl(target.url);
|
|
214
|
+
if (url.origin === null) {
|
|
215
|
+
unsupportedUrl += 1;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (!allowedOrigins.has(url.origin)) {
|
|
219
|
+
disallowedOrigin += 1;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (!hasCdpTargetWebSocket(discovery, target)) {
|
|
223
|
+
unconnectable += 1;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
allowed.push({
|
|
227
|
+
target_id: target.id,
|
|
228
|
+
type: target.type,
|
|
229
|
+
title: target.title.slice(0, 16_384),
|
|
230
|
+
url: url.url,
|
|
231
|
+
origin: url.origin,
|
|
232
|
+
attached: target.attached,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
allowed.sort((left, right) => left.target_id.localeCompare(right.target_id));
|
|
236
|
+
return {
|
|
237
|
+
allowed,
|
|
238
|
+
excluded: {
|
|
239
|
+
disallowed_origin: disallowedOrigin,
|
|
240
|
+
unsupported_url: unsupportedUrl,
|
|
241
|
+
non_page: nonPage,
|
|
242
|
+
},
|
|
243
|
+
unconnectable,
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
const authorizeTarget = (discovery, input) => {
|
|
247
|
+
const target = discovery.targets.find((candidate) => candidate.id === input.target_id);
|
|
248
|
+
if (target === undefined)
|
|
249
|
+
throw new BrowserObservationError("inspect_web_page", "target_not_found");
|
|
250
|
+
const origin = sanitizeBrowserUrl(target.url).origin;
|
|
251
|
+
if (target.type !== "page" ||
|
|
252
|
+
origin === null ||
|
|
253
|
+
!input.allowed_origins.includes(origin))
|
|
254
|
+
throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
|
|
255
|
+
return target;
|
|
256
|
+
};
|
|
257
|
+
const providerError = (cause, operation) => cause instanceof BrowserObservationError && cause.operation !== operation
|
|
258
|
+
? new BrowserObservationError(operation, cause.reason, { cause })
|
|
259
|
+
: cause instanceof AnalysisError
|
|
260
|
+
? cause
|
|
261
|
+
: new ProviderAdapterError(IDENTITY.id, operation, { cause });
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { classifyBrowserCompleteness, } from "../domain/browserCompleteness.js";
|
|
2
|
+
const EVENT_SECTIONS = {
|
|
3
|
+
scripts: "scripts",
|
|
4
|
+
network_requests: "network_requests",
|
|
5
|
+
console_events: "console_events",
|
|
6
|
+
websocket_connections: "websocket_connections",
|
|
7
|
+
websocket_frames: "websocket_frames",
|
|
8
|
+
webmcp_tools: "webmcp_tools",
|
|
9
|
+
timeline_events: "timeline",
|
|
10
|
+
};
|
|
11
|
+
const POLICY_REASONS = new Set([
|
|
12
|
+
"disallowed_origin",
|
|
13
|
+
"unsupported_url",
|
|
14
|
+
"unattributed_origin",
|
|
15
|
+
"not_approved",
|
|
16
|
+
"out_of_target_scope",
|
|
17
|
+
]);
|
|
18
|
+
/** Mutable adapter ledger projected into immutable, canonical completeness v2. */
|
|
19
|
+
export class CdpCaptureCompleteness {
|
|
20
|
+
#initialAttachLimited;
|
|
21
|
+
#policyFiltered = new Set();
|
|
22
|
+
#attachLimited = new Set();
|
|
23
|
+
#truncated = new Set();
|
|
24
|
+
#unavailable = new Set();
|
|
25
|
+
#excluded = new Map();
|
|
26
|
+
#dropped = emptyDropped();
|
|
27
|
+
constructor(initialAttachLimited = []) {
|
|
28
|
+
this.#initialAttachLimited = [...initialAttachLimited];
|
|
29
|
+
this.reset();
|
|
30
|
+
}
|
|
31
|
+
reset() {
|
|
32
|
+
this.#policyFiltered.clear();
|
|
33
|
+
this.#attachLimited.clear();
|
|
34
|
+
this.#truncated.clear();
|
|
35
|
+
this.#unavailable.clear();
|
|
36
|
+
this.#excluded.clear();
|
|
37
|
+
Object.assign(this.#dropped, emptyDropped());
|
|
38
|
+
for (const section of this.#initialAttachLimited)
|
|
39
|
+
this.#attachLimited.add(section);
|
|
40
|
+
}
|
|
41
|
+
exclude(section, reason, count = 1) {
|
|
42
|
+
if (POLICY_REASONS.has(reason))
|
|
43
|
+
this.#policyFiltered.add(section);
|
|
44
|
+
if (reason === "provider_unavailable" ||
|
|
45
|
+
reason === "invalid_protocol_value")
|
|
46
|
+
this.#unavailable.add(section);
|
|
47
|
+
const key = `${section}\0${reason}`;
|
|
48
|
+
const existing = this.#excluded.get(key);
|
|
49
|
+
const combined = count === null || existing?.count === null
|
|
50
|
+
? null
|
|
51
|
+
: (existing?.count ?? 0) + count;
|
|
52
|
+
this.#excluded.set(key, { section, reason, count: combined });
|
|
53
|
+
}
|
|
54
|
+
attachLimited(section) {
|
|
55
|
+
this.#attachLimited.add(section);
|
|
56
|
+
}
|
|
57
|
+
unavailable(section) {
|
|
58
|
+
this.#unavailable.add(section);
|
|
59
|
+
this.exclude(section, "provider_unavailable", null);
|
|
60
|
+
}
|
|
61
|
+
truncate(section) {
|
|
62
|
+
this.#truncated.add(section);
|
|
63
|
+
}
|
|
64
|
+
drop(kind, count = 1) {
|
|
65
|
+
this.#dropped[kind] += count;
|
|
66
|
+
this.#truncated.add(EVENT_SECTIONS[kind]);
|
|
67
|
+
}
|
|
68
|
+
get droppedTotal() {
|
|
69
|
+
return Object.values(this.#dropped).reduce((sum, count) => sum + count, 0);
|
|
70
|
+
}
|
|
71
|
+
snapshot() {
|
|
72
|
+
return classifyBrowserCompleteness({
|
|
73
|
+
policyFilteredSections: this.#policyFiltered,
|
|
74
|
+
attachLimitedSections: this.#attachLimited,
|
|
75
|
+
truncatedSections: this.#truncated,
|
|
76
|
+
unavailableSections: this.#unavailable,
|
|
77
|
+
excluded: [...this.#excluded.values()],
|
|
78
|
+
droppedEvents: this.#dropped,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const emptyDropped = () => ({
|
|
83
|
+
scripts: 0,
|
|
84
|
+
network_requests: 0,
|
|
85
|
+
console_events: 0,
|
|
86
|
+
websocket_connections: 0,
|
|
87
|
+
websocket_frames: 0,
|
|
88
|
+
webmcp_tools: 0,
|
|
89
|
+
timeline_events: 0,
|
|
90
|
+
});
|