Skip to content

fix(browser): the console channel sees CSP violations and resource failures - #751

Open
vaibhav8a wants to merge 1 commit into
reticlehq:mainfrom
vaibhav8a:fix/console-sees-violations-and-resource-errors
Open

fix(browser): the console channel sees CSP violations and resource failures#751
vaibhav8a wants to merge 1 commit into
reticlehq:mainfrom
vaibhav8a:fix/console-sees-violations-and-resource-errors

Conversation

@vaibhav8a

Copy link
Copy Markdown
Contributor

#666half 2, the capture gaps. Half 1 is the larger one and I've left it; see the end.

Why this is the worst class of bug we ship

reticle_assert({ kind: "console", level: "error", absent: true }) is a negative. A dead channel and a clean page are the same reading, and we graded the dead channel as proof.

Two whole classes of failure could never reach this channel:

CSP violations are not console calls. The browser never routes them through console, so DevTools printed dozens of font-src failures while this channel reported nothing. securitypolicyviolation exists precisely so a page can observe what the browser refused.

A resource failure fires on the element and does not bubble. The error listener was registered without capture, so a broken <img>, <script> or <link> could not be observed from window under any circumstances — only uncaught exceptions, which reach window directly. Capture phase is the only way to see an event that never travels upward.

The care that took

  • A resource failure is an Event, not an ErrorEvent: no message, no filename, and the thing that failed is the target. Reporting it as an empty-message uncaught error would be worse than not reporting it, so it is named from the element — tag plus URL, read from src or href depending on which the element carries.
  • An element error with no URL to name falls through to the existing path rather than being invented into a resource event it structurally is not.
  • An ErrorEvent on window still reports as an uncaught exception, with its message, filename and stack — pinned by a test, because rewriting kaboom into failed to load window is the regression this could most easily cause.
  • Teardown removes each listener with the same options it added. A capture listener removed without capture is not the listener being removed, and would survive teardown.

Tests

7 new cases in console.test.ts. 4 of them fail on main — I checked by stashing the source and re-running:

Tests  4 failed | 8 passed (12)      # source reverted
Tests  12 passed (12)                # with the fix

They cover: a font-src violation, a violation with an empty blockedURI (inline content), an <img> failure, a <link> failure read off href, an uncaught exception staying an uncaught exception, an element error with no URL, and full teardown.

pnpm --filter @reticlehq/browser exec vitest run   # 132 files, 1265 passed
tsc --noEmit / eslint                              # clean

Not in this PR: the startup blind window

Grading an absent: true console predicate as undecidable over a window that starts before the channel was installed needs the install timestamp carried on the session and read by the grader — it crosses from packages/browser into the server's honesty engine, and it changes what a verdict says. It is the larger half and worth its own review. This PR closes the half where the channel was blind by construction.

🤖 Generated with Claude Code

@vaibhav8a

Copy link
Copy Markdown
Contributor Author

The bench red is a coverage-shrink from competitor-harness timeouts, not something this change can cause.

{"s":"hidden-api-500","t":"playwright", ... "n":"error: Error: timeout after 60000ms on initialize"}
{"s":"hidden-api-500","t":"devtools",   ... "n":"error: Error: timeout after 60000ms on initialize"}

- measured coverage SHRANK: 46 cells this run vs 48 in the baseline.
  Not measured this run: hidden-api-500/playwright, hidden-api-500/devtools,
  broken-form-validation/playwright, cross-component-regression/*, strictmode-duplicate-effect/*

Every lost cell is a playwright or devtools harness failing to initialize (plus one Reticle cell in cross-component-regression). This PR adds two window listeners in packages/browsersecuritypolicyviolation, and capture: true on the existing error listener. There is no path from that to a competitor MCP server timing out on its own handshake.

Worth noting the console cell in the same run went the right way:

=== console-clean: console-error regression CAUGHT | clean successOk=true bugged successOk=false drift=false | ~378 tok/run ===

Happy to re-run if you'd like, but I don't think another attempt tells us anything new.

…ilures

`reticle_assert({ kind: "console", level: "error", absent: true })`
returned a confident pass on pages visibly full of errors, because two
whole classes of failure never reached this channel at all (reticlehq#666). A
negative check where a dead channel and a clean page read alike is a
false green in the one place the product's claim rests.

**CSP violations are not console calls.** The browser never routes them
through console, so DevTools printed dozens of `font-src` failures while
this channel reported nothing. `securitypolicyviolation` exists precisely
so a page can observe what the browser refused, and it is now listened
for.

**A resource failure fires on the ELEMENT and does not bubble.** The
`error` listener was registered without `capture`, so a broken `<img>`,
`<script>` or `<link>` could not be observed from `window` under any
circumstances -- only uncaught exceptions, which reach `window` directly.
Capture phase is the only way to see an event that never travels upward.

A resource failure is an `Event`, not an `ErrorEvent`: no message, no
filename, and the thing that failed is the target. Reporting it as an
empty-message uncaught error would be worse than not reporting it, so it
is named from the element -- tag plus the URL, read from `src` or `href`
depending on which the element carries. An element error with no URL to
name falls through to the existing path rather than being invented into a
resource event it structurally is not.

The teardown removes each listener with the SAME options it added,
because a capture listener removed without `capture` is not the listener
being removed and would survive teardown.

Not fixed here: the startup blind window, the issue's other half. Grading
an `absent: true` console predicate as undecidable over a window that
starts before the channel was installed needs the install timestamp
carried on the session and read by the grader, which crosses from this
package into the server's honesty engine. It is the larger half and worth
its own review.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: Vaibhav Srivastava <vaibhavsri1712@gmail.com>
@vaibhav8a
vaibhav8a force-pushed the fix/console-sees-violations-and-resource-errors branch from 4d24acc to e675d76 Compare September 5, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant