[GH-ISSUE #7407] [Bug] Page capture assigns main-frame tracking to a child when the root ID is missing #4414

Closed
opened 2026-10-31 00:49:36 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 27, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7407

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Process capture or runtime observation

Observed behavior

When page inspection receives an in-scope frame-tree root without an id, followed by a valid same-origin child, the production CDP provider accepts the capture and uses the child for main-frame tracking. I reproduced this through CdpBrowserProvider.inspectPage over the repository's HTTP/WebSocket browser fixture. The returned target still names the root document, while frames contains only the child. A script explicitly associated with the real main frame through its execution context is returned with frame_id: null; the child script retains its association. All three observation domains were enabled despite the unresolved root identity.

In the same fixture, delivering Page.frameNavigated for that child during Network.enable makes inspection fail with BrowserObservationError.reason: protocol_error. Adding the correct root ID makes both variants succeed, including the identical child-navigation event. This is one identity-selection defect with effects on attribution and navigation handling. The CLI path to this provider was checked in source; a compiled CLI invocation was not independently executed.

Reproduction

Use a fresh source checkout at commit f8f7ac96de8a66a4ab9a23d87e04779bfcd11045, Node.js 24.22.1, and dependencies installed from the lockfile with npm ci. No runtime build is needed. Create the following temporary test and run the focused source test. This is the probe I executed; its assertions describe the baseline's incorrect behavior and its valid-root controls.

cat > src/browser/verify-probe.test.ts <<'TEST'
import { expect, it } from "vitest";
import { BrowserObservationError } from "../domain/browserObservationError.js";
import { CdpBrowserProvider } from "./CdpBrowserProvider.js";
import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
import { startFakeCdpBrowser } from "../../tests/fixtures/fakeCdpBrowser.js";

it("compares root identity and child navigation over the production CDP transport", async () => {
  for (const rootId of [undefined, "frame-main"]) {
    for (const navigateChild of [false, true]) {
      const browser = await startFakeCdpBrowser({
        commandResult: (command, origin) => command.method === "Page.getFrameTree" ? {
          frameTree: {
            frame: { ...(rootId === undefined ? {} : { id: rootId }), url: `${origin}/app`, loaderId: "loader-main" },
            childFrames: [{ frame: { id: "frame-child", parentId: "frame-main", loaderId: "loader-child", url: `${origin}/child` } }],
          },
        } : undefined,
        commandEvents: (command, origin) => {
          const event = (method: string, params: Record<string, unknown>) => ({ method, params, sessionId: command.sessionId });
          if (command.method === "Runtime.enable") return [
            event("Runtime.executionContextCreated", { context: { id: 101, origin, name: "main", auxData: { frameId: "frame-main" } } }),
            event("Runtime.executionContextCreated", { context: { id: 102, origin, name: "child", auxData: { frameId: "frame-child" } } }),
          ];
          if (command.method === "Debugger.enable") return [
            event("Debugger.scriptParsed", { scriptId: "main-script", url: `${origin}/app.js`, executionContextId: 101, hash: "main-hash" }),
            event("Debugger.scriptParsed", { scriptId: "child-script", url: `${origin}/child.js`, executionContextId: 102, hash: "child-hash" }),
          ];
          if (command.method === "Network.enable" && navigateChild) return [event("Page.frameNavigated", { frame: { id: "frame-child", parentId: "frame-main", loaderId: "loader-child-2", url: `${origin}/child2` } })];
          return [];
        },
      });
      const provider = new CdpBrowserProvider();
      try {
        const result = await provider.inspectPage(inspectWebPageInputSchema.parse({ cdp_endpoint: browser.endpoint, target_id: "allowed-page", observation_ms: 1 }));
        const enabled = browser.commands.filter(c => ["Runtime.enable", "Debugger.enable", "Network.enable"].includes(c.method)).map(c => c.method);
        const summary = result.ok ? { ok: true, target: result.value.target, frames: result.value.frames, scripts: result.value.scripts.items.map(s => ({ url: s.url, frame_id: s.frame_id })), unavailable: result.value.completeness.unavailable_sections } : { ok: false, reason: (result.error instanceof BrowserObservationError ? result.error.reason : result.error.message) };
        console.log(JSON.stringify({ rootId: rootId ?? "omitted", navigateChild, enabled, ...summary }));
        if (rootId === undefined && navigateChild) {
          expect(result.ok).toBe(false);
          if (!result.ok) expect((result.error instanceof BrowserObservationError ? result.error.reason : result.error.message)).toBe("protocol_error");
        } else {
          expect(result.ok).toBe(true);
          if (result.ok) {
            expect(result.value.frames.map(f => f.frame_id)).toEqual(rootId === undefined ? ["frame-child"] : ["frame-main", "frame-child"]);
            expect(result.value.target.url).toBe(`${browser.endpoint}/app`);
            expect(result.value.scripts.items.find(s => s.url.endsWith("/app.js"))?.frame_id).toBe(rootId === undefined ? null : "frame-main");
          }
        }
      } finally {
        await provider.close();
        await browser.close();
      }
    }
  }
}, 30_000);
TEST
review_home="$(mktemp -d)"
HOME="$review_home" npm run test:local -- src/browser/verify-probe.test.ts
rm src/browser/verify-probe.test.ts
rm -rf "$review_home"

The test runs four configurations sequentially. The completed run passed all assertions: missing root ID with and without the child event, then valid root ID with and without the same event. An initial verifier-only error inspected the wrong error property; the completed run above uses the provider's actual error type.

Expected behavior

An unresolved root identity must stay unresolved rather than acquire the first readable child's ID. Page capture must establish the selected root identity before using it to classify main-frame events, or return a typed failure when that identity is unavailable. A correctly identified child navigation must not become evidence that the main frame has malformed parentage.

Expected-source references: AGENTS.md:19 requires preservation of identity, omission, and uncertainty across boundaries; AGENTS.md:21 requires unresolved identity to remain unknown. docs/browser-observation.md:378 describes the main-frame boundary and main-frame navigation handling for page capture. docs/browser-observation.md:490 promises retention of supplied authorized script/frame associations. The explicit prohibition on a child substituting for the root at docs/browser-observation.md:390 applies to screenshots; it is supporting sibling behavior, not an explicit page-inspection error-code contract.

Impact and scope

P2: this is a bounded malformed-protocol-input defect that changes evidence attribution and can fail a capture. In the synthetic missing-root-ID case, the main script loses known frame attribution, and navigation of the selected child causes a protocol failure. The result does signal unavailable frame coverage, so it does not claim a complete frame inventory; that signal does not correct the internal root selection. Both valid-root controls succeed.

The demonstrated trigger requires a root with no ID and at least one retained same-origin child. Real Chrome-family browser frequency is unknown; no real browser producing this malformed reply was tested. The valid-root control demonstrates that correcting the producer's reply avoids the defect, but access to such a producer correction is not established for affected users. No security-boundary crossing or severe downstream harm is claimed.

REA version

6.8.0 from package.json; baseline f8f7ac96de8a66a4ab9a23d87e04779bfcd11045.

Runtime and platform

Measured Node.js v24.22.1; macOS 27.0.1; Darwin 27.0.0; arm64.

Target and artifact context

A local HTTP/WebSocket CDP fixture with target allowed-page, root document /app, and same-origin child /child. It supplies two execution contexts and two script metadata events. No live browser, external target, executable artifact, or private page content was involved.

Interface

CLI

Earliest failure stage

Capture or observation

Relevant output or evidence

The focused test logs the following projected facts from production-provider results. URLs are shortened to their paths; no other displayed values are changed.

Root ID Child navigation delivered Result Retained frame IDs Main script frame ID Unavailable sections
Omitted No Success frame-child null frames
Omitted Yes protocol_error No successful result No successful result No successful result
frame-main No Success frame-main, frame-child frame-main None
frame-main Yes Success frame-main, frame-child frame-main None

For each configuration, the fixture recorded Runtime.enable, Debugger.enable, and Network.enable. Successful results retained target URL /app. The missing-root-ID success retained the child with parent_frame_id: frame-main; its child script kept frame_id: frame-child.

Failure-source references: src/browser/CdpAuthorizedMainFrame.ts:34 checks root URL scope without requiring an ID. src/browser/CdpCaptureDocuments.ts:66 omits the id-less frame from the inventory. src/browser/CdpPageCapture.ts:297 selects the first remaining inventory item as the initial main frame; src/browser/CdpPageCapture.ts:167 repeats this for final capture. src/browser/CdpCaptureEventHandlers.ts:750 interprets the child's navigation using that selected identity and sets unknown-root state when it sees the child's parent. src/browser/CdpPageCaptureScripts.ts:153 derives script attribution from the retained frame set, which lacks the real root.

Supported-entry-point evidence is source-derived: src/cli/browserCommands.ts:101 registers inspect-web-page, validates its input, and calls the shared inspection workflow. src/composition/browserObservation.ts:5 constructs the provider exercised here; src/application/BrowserObservationService.ts:62 calls its inspectPage method. The reproduction passes production discovery, target selection, origin admission, session attachment, transport parsing, and final result validation.

Typed outcome or recovery expected

Return a typed malformed or unresolved-main-frame outcome before relying on a substituted frame identity. protocol_error is consistent with the sibling screenshot policy, but the precise page-inspection error code is not specified by the cited documentation. With a valid root identity, the same child event should continue to be ignored for main-frame tracking, as demonstrated by the control.

Additional context

This verification used only local fixtures at the stated commit. Related, distinct: #5665 (closed) concerned malformed Page navigation frame identity being treated as non-main.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7407 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Process capture or runtime observation ### Observed behavior When page inspection receives an in-scope frame-tree root without an `id`, followed by a valid same-origin child, the production CDP provider accepts the capture and uses the child for main-frame tracking. I reproduced this through `CdpBrowserProvider.inspectPage` over the repository's HTTP/WebSocket browser fixture. The returned target still names the root document, while `frames` contains only the child. A script explicitly associated with the real main frame through its execution context is returned with `frame_id: null`; the child script retains its association. All three observation domains were enabled despite the unresolved root identity. In the same fixture, delivering `Page.frameNavigated` for that child during `Network.enable` makes inspection fail with `BrowserObservationError.reason: protocol_error`. Adding the correct root ID makes both variants succeed, including the identical child-navigation event. This is one identity-selection defect with effects on attribution and navigation handling. The CLI path to this provider was checked in source; a compiled CLI invocation was not independently executed. ### Reproduction Use a fresh source checkout at commit `f8f7ac96de8a66a4ab9a23d87e04779bfcd11045`, Node.js 24.22.1, and dependencies installed from the lockfile with `npm ci`. No runtime build is needed. Create the following temporary test and run the focused source test. This is the probe I executed; its assertions describe the baseline's incorrect behavior and its valid-root controls. ```bash cat > src/browser/verify-probe.test.ts <<'TEST' import { expect, it } from "vitest"; import { BrowserObservationError } from "../domain/browserObservationError.js"; import { CdpBrowserProvider } from "./CdpBrowserProvider.js"; import { inspectWebPageInputSchema } from "../domain/browserObservation.js"; import { startFakeCdpBrowser } from "../../tests/fixtures/fakeCdpBrowser.js"; it("compares root identity and child navigation over the production CDP transport", async () => { for (const rootId of [undefined, "frame-main"]) { for (const navigateChild of [false, true]) { const browser = await startFakeCdpBrowser({ commandResult: (command, origin) => command.method === "Page.getFrameTree" ? { frameTree: { frame: { ...(rootId === undefined ? {} : { id: rootId }), url: `${origin}/app`, loaderId: "loader-main" }, childFrames: [{ frame: { id: "frame-child", parentId: "frame-main", loaderId: "loader-child", url: `${origin}/child` } }], }, } : undefined, commandEvents: (command, origin) => { const event = (method: string, params: Record<string, unknown>) => ({ method, params, sessionId: command.sessionId }); if (command.method === "Runtime.enable") return [ event("Runtime.executionContextCreated", { context: { id: 101, origin, name: "main", auxData: { frameId: "frame-main" } } }), event("Runtime.executionContextCreated", { context: { id: 102, origin, name: "child", auxData: { frameId: "frame-child" } } }), ]; if (command.method === "Debugger.enable") return [ event("Debugger.scriptParsed", { scriptId: "main-script", url: `${origin}/app.js`, executionContextId: 101, hash: "main-hash" }), event("Debugger.scriptParsed", { scriptId: "child-script", url: `${origin}/child.js`, executionContextId: 102, hash: "child-hash" }), ]; if (command.method === "Network.enable" && navigateChild) return [event("Page.frameNavigated", { frame: { id: "frame-child", parentId: "frame-main", loaderId: "loader-child-2", url: `${origin}/child2` } })]; return []; }, }); const provider = new CdpBrowserProvider(); try { const result = await provider.inspectPage(inspectWebPageInputSchema.parse({ cdp_endpoint: browser.endpoint, target_id: "allowed-page", observation_ms: 1 })); const enabled = browser.commands.filter(c => ["Runtime.enable", "Debugger.enable", "Network.enable"].includes(c.method)).map(c => c.method); const summary = result.ok ? { ok: true, target: result.value.target, frames: result.value.frames, scripts: result.value.scripts.items.map(s => ({ url: s.url, frame_id: s.frame_id })), unavailable: result.value.completeness.unavailable_sections } : { ok: false, reason: (result.error instanceof BrowserObservationError ? result.error.reason : result.error.message) }; console.log(JSON.stringify({ rootId: rootId ?? "omitted", navigateChild, enabled, ...summary })); if (rootId === undefined && navigateChild) { expect(result.ok).toBe(false); if (!result.ok) expect((result.error instanceof BrowserObservationError ? result.error.reason : result.error.message)).toBe("protocol_error"); } else { expect(result.ok).toBe(true); if (result.ok) { expect(result.value.frames.map(f => f.frame_id)).toEqual(rootId === undefined ? ["frame-child"] : ["frame-main", "frame-child"]); expect(result.value.target.url).toBe(`${browser.endpoint}/app`); expect(result.value.scripts.items.find(s => s.url.endsWith("/app.js"))?.frame_id).toBe(rootId === undefined ? null : "frame-main"); } } } finally { await provider.close(); await browser.close(); } } } }, 30_000); TEST review_home="$(mktemp -d)" HOME="$review_home" npm run test:local -- src/browser/verify-probe.test.ts rm src/browser/verify-probe.test.ts rm -rf "$review_home" ``` The test runs four configurations sequentially. The completed run passed all assertions: missing root ID with and without the child event, then valid root ID with and without the same event. An initial verifier-only error inspected the wrong error property; the completed run above uses the provider's actual error type. ### Expected behavior An unresolved root identity must stay unresolved rather than acquire the first readable child's ID. Page capture must establish the selected root identity before using it to classify main-frame events, or return a typed failure when that identity is unavailable. A correctly identified child navigation must not become evidence that the main frame has malformed parentage. Expected-source references: `AGENTS.md:19` requires preservation of identity, omission, and uncertainty across boundaries; `AGENTS.md:21` requires unresolved identity to remain unknown. `docs/browser-observation.md:378` describes the main-frame boundary and main-frame navigation handling for page capture. `docs/browser-observation.md:490` promises retention of supplied authorized script/frame associations. The explicit prohibition on a child substituting for the root at `docs/browser-observation.md:390` applies to screenshots; it is supporting sibling behavior, not an explicit page-inspection error-code contract. ### Impact and scope P2: this is a bounded malformed-protocol-input defect that changes evidence attribution and can fail a capture. In the synthetic missing-root-ID case, the main script loses known frame attribution, and navigation of the selected child causes a protocol failure. The result does signal unavailable frame coverage, so it does not claim a complete frame inventory; that signal does not correct the internal root selection. Both valid-root controls succeed. The demonstrated trigger requires a root with no ID and at least one retained same-origin child. Real Chrome-family browser frequency is unknown; no real browser producing this malformed reply was tested. The valid-root control demonstrates that correcting the producer's reply avoids the defect, but access to such a producer correction is not established for affected users. No security-boundary crossing or severe downstream harm is claimed. ### REA version 6.8.0 from package.json; baseline `f8f7ac96de8a66a4ab9a23d87e04779bfcd11045`. ### Runtime and platform Measured Node.js v24.22.1; macOS 27.0.1; Darwin 27.0.0; arm64. ### Target and artifact context A local HTTP/WebSocket CDP fixture with target `allowed-page`, root document `/app`, and same-origin child `/child`. It supplies two execution contexts and two script metadata events. No live browser, external target, executable artifact, or private page content was involved. ### Interface CLI ### Earliest failure stage Capture or observation ### Relevant output or evidence The focused test logs the following projected facts from production-provider results. URLs are shortened to their paths; no other displayed values are changed. | Root ID | Child navigation delivered | Result | Retained frame IDs | Main script frame ID | Unavailable sections | | --- | --- | --- | --- | --- | --- | | Omitted | No | Success | `frame-child` | `null` | `frames` | | Omitted | Yes | `protocol_error` | No successful result | No successful result | No successful result | | `frame-main` | No | Success | `frame-main`, `frame-child` | `frame-main` | None | | `frame-main` | Yes | Success | `frame-main`, `frame-child` | `frame-main` | None | For each configuration, the fixture recorded `Runtime.enable`, `Debugger.enable`, and `Network.enable`. Successful results retained target URL `/app`. The missing-root-ID success retained the child with `parent_frame_id: frame-main`; its child script kept `frame_id: frame-child`. Failure-source references: `src/browser/CdpAuthorizedMainFrame.ts:34` checks root URL scope without requiring an ID. `src/browser/CdpCaptureDocuments.ts:66` omits the id-less frame from the inventory. `src/browser/CdpPageCapture.ts:297` selects the first remaining inventory item as the initial main frame; `src/browser/CdpPageCapture.ts:167` repeats this for final capture. `src/browser/CdpCaptureEventHandlers.ts:750` interprets the child's navigation using that selected identity and sets unknown-root state when it sees the child's parent. `src/browser/CdpPageCaptureScripts.ts:153` derives script attribution from the retained frame set, which lacks the real root. Supported-entry-point evidence is source-derived: `src/cli/browserCommands.ts:101` registers `inspect-web-page`, validates its input, and calls the shared inspection workflow. `src/composition/browserObservation.ts:5` constructs the provider exercised here; `src/application/BrowserObservationService.ts:62` calls its `inspectPage` method. The reproduction passes production discovery, target selection, origin admission, session attachment, transport parsing, and final result validation. ### Typed outcome or recovery expected Return a typed malformed or unresolved-main-frame outcome before relying on a substituted frame identity. `protocol_error` is consistent with the sibling screenshot policy, but the precise page-inspection error code is not specified by the cited documentation. With a valid root identity, the same child event should continue to be ignored for main-frame tracking, as demonstrated by the control. ### Additional context This verification used only local fixtures at the stated commit. Related, distinct: #5665 (closed) concerned malformed Page navigation frame identity being treated as non-main.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4414
No description provided.