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.
Files changed (123) hide show
  1. package/README.md +37 -13
  2. package/dist/application/AnalysisSnapshotCache.js +8 -6
  3. package/dist/application/ArtifactGraphConstruction.js +11 -2
  4. package/dist/application/ArtifactInventory.js +3 -2
  5. package/dist/application/AuthorizedArtifactInventory.js +2 -1
  6. package/dist/application/BinarySession.js +26 -11
  7. package/dist/application/BoundedJsonFiles.js +6 -29
  8. package/dist/application/BrowserEvidence.js +107 -0
  9. package/dist/application/BrowserObservationPort.js +1 -0
  10. package/dist/application/BrowserObservationService.js +116 -0
  11. package/dist/application/CapabilityInventory.js +14 -0
  12. package/dist/application/ClientConfigPath.js +33 -0
  13. package/dist/application/ConfiguredRoots.js +19 -0
  14. package/dist/application/CrossVersionInventory.js +16 -3
  15. package/dist/application/CrossVersionInvestigation.js +23 -62
  16. package/dist/application/CrossVersionInvestigationEvidence.js +64 -0
  17. package/dist/application/CrossVersionInvestigationReplay.js +170 -0
  18. package/dist/application/DeferredFileAuthorization.js +61 -0
  19. package/dist/application/DirectAnalysis.js +38 -18
  20. package/dist/application/Doctor.js +16 -11
  21. package/dist/application/ElectronEvidence.js +35 -0
  22. package/dist/application/ElectronObservationPort.js +1 -0
  23. package/dist/application/ElectronObservationService.js +46 -0
  24. package/dist/application/EvidenceLedger.js +11 -7
  25. package/dist/application/InvestigationWorkspaceStore.js +2 -26
  26. package/dist/application/PermissionAuthority.js +30 -0
  27. package/dist/application/ProcessCaptureAuthority.js +3 -2
  28. package/dist/application/ProcessCaptureLifecycle.js +19 -13
  29. package/dist/application/ProcessCheckpoints.js +1 -1
  30. package/dist/application/ProcessCli.js +17 -5
  31. package/dist/application/ProcessHarness.js +19 -26
  32. package/dist/application/ProcessNormalization.js +17 -2
  33. package/dist/application/ProcessOwnership.js +44 -23
  34. package/dist/application/ProcessSampling.js +111 -25
  35. package/dist/application/ProjectPermissionStore.js +107 -3
  36. package/dist/application/ReferenceSourceImport.js +13 -16
  37. package/dist/application/ReferenceSourceImportPolicy.js +3 -2
  38. package/dist/application/Setup.js +89 -30
  39. package/dist/application/SetupClients.js +7 -4
  40. package/dist/application/SetupInstallFailure.js +24 -0
  41. package/dist/application/SetupPlan.js +11 -7
  42. package/dist/application/SetupSkill.js +20 -3
  43. package/dist/application/TerminalRenderer.js +1 -1
  44. package/dist/application/Uninstall.js +20 -7
  45. package/dist/application/Upgrade.js +14 -3
  46. package/dist/artifacts/MachOSliceArtifactReader.js +1 -1
  47. package/dist/artifacts/NativeDmgArtifactReader.js +1 -1
  48. package/dist/artifacts/SafeOutputTree.js +3 -3
  49. package/dist/browser/CdpBrowserProvider.js +261 -0
  50. package/dist/browser/CdpCaptureCompleteness.js +90 -0
  51. package/dist/browser/CdpCaptureDocuments.js +299 -0
  52. package/dist/browser/CdpCaptureEvents.js +547 -0
  53. package/dist/browser/CdpCaptureStorage.js +53 -0
  54. package/dist/browser/CdpCaptureValues.js +68 -0
  55. package/dist/browser/CdpConnection.js +241 -0
  56. package/dist/browser/CdpElectronInspection.js +330 -0
  57. package/dist/browser/CdpElectronProvider.js +134 -0
  58. package/dist/browser/CdpEndpoint.js +173 -0
  59. package/dist/browser/CdpObservationSession.js +301 -0
  60. package/dist/browser/CdpOptionalCommand.js +15 -0
  61. package/dist/browser/CdpPageCapture.js +376 -0
  62. package/dist/browser/CdpSafeMetadata.js +203 -0
  63. package/dist/browser/CdpScreenshot.js +101 -0
  64. package/dist/browser/CdpTargetSession.js +47 -0
  65. package/dist/browser/CdpWebMcpDiscovery.js +242 -0
  66. package/dist/browser/ElectronFileScope.js +50 -0
  67. package/dist/browser/PngVisualDiff.js +177 -0
  68. package/dist/browser/SensitiveTextCapture.js +25 -0
  69. package/dist/browser/WebSourceMapFetcher.js +357 -0
  70. package/dist/catalogIdentity.js +10 -0
  71. package/dist/cli.js +6 -0
  72. package/dist/cliBrowserAdvancedCommands.js +169 -0
  73. package/dist/cliBrowserCommands.js +312 -0
  74. package/dist/cliElectronCommands.js +114 -0
  75. package/dist/cliInvestigationCommands.js +26 -34
  76. package/dist/cliPolicyCommands.js +60 -12
  77. package/dist/config.js +80 -1
  78. package/dist/contracts/browserToolContracts.js +381 -0
  79. package/dist/contracts/electronToolContracts.js +73 -0
  80. package/dist/contracts/errorSchemas.js +2 -0
  81. package/dist/contracts/toolContracts.js +5 -5
  82. package/dist/domain/browserCompleteness.js +101 -0
  83. package/dist/domain/browserObservation.js +587 -0
  84. package/dist/domain/browserSession.js +63 -0
  85. package/dist/domain/changedBehavior.js +4 -15
  86. package/dist/domain/electronObservation.js +162 -0
  87. package/dist/domain/errors.js +41 -0
  88. package/dist/domain/investigationWorkspace.js +4 -0
  89. package/dist/domain/jsonLimits.js +26 -0
  90. package/dist/domain/jsonShape.js +125 -0
  91. package/dist/domain/permissionPolicy.js +6 -2
  92. package/dist/domain/processCapture.js +3 -1
  93. package/dist/domain/processComparison.js +22 -34
  94. package/dist/domain/reconstructionVerification.js +2 -15
  95. package/dist/domain/reconstructionVerificationSchemas.js +2 -0
  96. package/dist/domain/referenceSourceGraph.js +3 -1
  97. package/dist/domain/staticRuntimeCorrelation.js +2 -0
  98. package/dist/domain/symbolAnalysis.js +1 -1
  99. package/dist/domain/webBundleAnalysis.js +195 -0
  100. package/dist/domain/webBundleAnalyzer.js +423 -0
  101. package/dist/domain/webCaptureDiff.js +184 -0
  102. package/dist/domain/webContentArtifact.js +47 -0
  103. package/dist/domain/webInventory.js +61 -0
  104. package/dist/domain/webMcpDiscovery.js +68 -0
  105. package/dist/domain/webScreenshot.js +114 -0
  106. package/dist/generatedPackageMetadata.js +2 -2
  107. package/dist/hopper/BridgeLauncher.js +2 -3
  108. package/dist/hopper/HopperClient.js +51 -11
  109. package/dist/hopper/HopperProvider.js +6 -1
  110. package/dist/main.js +42 -31
  111. package/dist/native/CommandRunner.js +11 -4
  112. package/dist/native/NativeMacOSProvider.js +3 -2
  113. package/dist/native/NativeMachoInspection.js +21 -6
  114. package/dist/server/createServer.js +14 -0
  115. package/dist/server/registerBrowserTools.js +95 -0
  116. package/dist/server/registerElectronTools.js +36 -0
  117. package/dist/server/registerInvestigationTools.js +31 -37
  118. package/dist/server/registerProcessComparisonTool.js +2 -0
  119. package/dist/server/registerSessionTools.js +16 -2
  120. package/dist/server/runDerivedOperation.js +10 -2
  121. package/package.json +6 -1
  122. package/scripts/rea.mjs +2 -2
  123. 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
- kind: "install_skill",
27
- target: "~/.agents/skills/rea-analysis/SKILL.md",
28
- detail: "Install or update the bundled REA analysis skill.",
29
- external: false,
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
- const destination = join(home, ".agents/skills", PRODUCT_IDENTITY.skillName, "SKILL.md");
8
- const backup = `${destination}.rea.backup`;
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 content = await readFile(new URL(`../../skills/${PRODUCT_IDENTITY.skillName}/SKILL.md`, import.meta.url), "utf8");
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(client.configPath);
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(client.configPath, backupPath);
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(client.configPath, encoded);
94
+ await fileSystem.writeText(transactionPath, encoded);
89
95
  const readback = objectSchema.parse(client.format === "toml"
90
- ? parseToml(await fileSystem.readText(client.configPath))
91
- : JSON.parse(await fileSystem.readText(client.configPath)));
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(client.configPath, original);
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
- if (latestVersion === currentVersion)
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: latestVersion ?? null,
42
- versionCheck: latestVersion === undefined ? "unavailable" : "available",
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();
@@ -67,7 +67,7 @@ export class NativeDmgArtifactReader {
67
67
  return this.#directory.open(entry, signal);
68
68
  }
69
69
  provenance() {
70
- return this.#provenance;
70
+ return structuredClone(this.#provenance);
71
71
  }
72
72
  async close() {
73
73
  let detachFailure;
@@ -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
+ });