Skip to content

feat(browser): surface bounded runtime diagnostics - #4175

Merged
Yeachan-Heo merged 2 commits into
Yeachan-Heo:devfrom
chulmin-dev:feat/browser-runtime-diagnostics
Aug 10, 2026
Merged

feat(browser): surface bounded runtime diagnostics#4175
Yeachan-Heo merged 2 commits into
Yeachan-Heo:devfrom
chulmin-dev:feat/browser-runtime-diagnostics

Conversation

@chulmin-dev

Copy link
Copy Markdown
Contributor

What

Surface a bounded, secret-safe mailbox of page runtime errors in the next successful browser act or run response.

  • Captures CDP Runtime.exceptionThrown and console.error events for each active tab.
  • Retains the newest 20 entries and counts evictions.
  • Drains once as { runtimeDiagnostics, runtimeDiagnosticsDropped }.
  • Keeps only kind, timestamp, query-masked URL, line/column, and a validated error class.
  • Never retains exception messages, console arguments/values, or stacks.
  • Failed actions leave the mailbox for the next successful response.

This is observability-only: it does not change action success, navigation, retry, tab lifecycle, or browser state.

Why

The native browser can complete an interaction while the page itself reports a React hydration failure, asynchronous exception, or console.error. Those page failures were not visible in the tool response, so E2E callers could accept a visually plausible but broken state.

This ports only the non-overlapping diagnostics lesson from the user-level webpilot skill. Domain allowlists and irreversible-action confirmation are intentionally excluded: raw Puppeteer run makes those separate policy/sandbox designs rather than a safe small patch.

Duplicate searches for browser pageerror, browser console error, browser diagnostics, and browser allowlist found no existing GJC issue or PR covering this behavior.

Testing

  • bun test packages/coding-agent/test/tools/browser-runtime-diagnostics.test.ts packages/coding-agent/test/tools/browser-actions.test.ts packages/coding-agent/test/tools/browser-tab-worker-startup.test.ts — 24 pass, 0 fail
  • bun --cwd=packages/coding-agent run check:types — pass
  • Focused Biome check — pass
  • Live Chromium probe — captured one console-error and one pageerror, masked ?token=secret to ?…, and emitted neither message/argument secret

GJC verdict

gajae.pr-review-verdict.v1 needs-human sha256:e6a7f88bae9ddef35dc7b1805c06e750a4ce8325909712a7477a832016f49565 reviewer:human evidence:local-browser-tests-24-pass-and-live-chromium-probe

No independent role review is claimed; the patch is left for maintainer review rather than extending the session with a provider-dependent critic lane.


  • Target branch is dev
  • bun check passes
  • Tested locally
  • CHANGELOG updated (if user-facing)
  • Verdict above matches the exact PR head, not an earlier commit

@probepark probepark left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice piece of work — the masking helper, the eviction counter, and the drain-on-result design are all the right shapes, and the docs/prompt match the implementation's ordering and draining behaviour. BLOCK on two issues I reproduced directly; both are contained fixes.

BLOCKER 1 — URL path segments are emitted raw, and tool results are persisted

maskBrowserRuntimeUrl (runtime-diagnostics.ts:31-36) masks the query string but passes url.pathname through verbatim:

return `${url.origin}${url.pathname}${url.search ? "?…" : ""}`;

I ran it directly:

input   https://example.com/account/SESSION_TOKEN_ABC123?k=v
output  https://example.com/account/SESSION_TOKEN_ABC123?…

The query is masked; the secret in the path is not. Path-embedded tokens are common — signed URLs, invite/reset links, per-tenant identifiers, /u/<email> style routes.

This matters more than an ordinary log leak because these diagnostics are emitted into tool results at tab-worker.ts:600-605, and tool results are persisted and included in dump/share/export flows (docs/session-operations-export-share-fork-resume.md:72-80). A token from a page the agent merely visited ends up in an artifact the user may hand to someone else.

Fix: emit the origin only, and drop or hash path segments. Same treatment for exception class names — map them through a fixed allowlist or a syntax regex rather than echoing page-controlled strings (my probe also surfaced a page-controlled class name, CustomerAlice123).

BLOCKER 2 — the 20-entry bound is real, but total output size is not bounded

runtime-diagnostics.ts:79-91 correctly caps at 20 entries and counts evictions. But an individual entry has no size limit: the pathname is unlimited, and tab-worker.ts:604 serializes with an uncapped serializer (:186-190).

Measured:

https://example.com/ + "a".repeat(300000)   →   diagnostic length 300,020

One constructed URL. Twenty entries of that shape is ~6 MB. Because diagnostics go out as display content, they bypass the 256 KiB return-value cap in browser.ts:429-461 — so the cap that exists elsewhere does not protect this path.

Fix: per-field and total serialized-byte limits on the emission path, with an explicit truncation marker. Silent truncation would be its own problem for a diagnostic feature, so please make it visible.

Test gap

Disabling the displays.push(...) branch at tab-worker.ts:601 left all three focused suites green — 24 pass / 0 fail. So nothing currently proves diagnostics actually reach browser output. Worth a worker/supervisor-level test covering output ordering, drain-on-success, and retention after failure.

Checked and clear

  • Renderer safety: the display path applies JSON escaping, tab replacement and width wrapping. No TUI control-sequence issue — page-controlled text cannot corrupt rendering.
  • Mailbox memory stays capped at 20 regardless of console volume.
  • Docs/prompt accurately describe ordering and draining. The one wording issue is "safe error class", which is not currently guaranteed given BLOCKER 1.
  • Rebases cleanly onto current origin/dev; no conflicts.

One thing to flag rather than block on

The feature is always on: one extra CDP session plus Runtime.enable per tab (tab-worker.ts:460-475, 526-533), and every Runtime.consoleAPICalled event — including ordinary non-error console traffic — crosses CDP and invokes the handler. Memory is bounded, so this is a cost question rather than a correctness one, but on a chatty page it is per-event work that nobody opted into. Worth a deliberate decision on whether this should be opt-in.

Fix the two blockers and I think this lands well — the underlying idea is genuinely useful.

Verification note: both blockers were reproduced by me against this PR head, not taken on report.

Yeachan-Heo pushed a commit to chulmin-dev/gajae-code that referenced this pull request Aug 10, 2026
Maintainer review 4896349024 on PR Yeachan-Heo#4175 reproduced raw pathname leakage into
persisted tool results, page-controlled error class names, and an unbounded
serialized diagnostics block. Close all three and make the feature opt-in:

- Emit http(s) URLs origin-only so path-embedded tokens (signed URLs, invite/
  reset links, per-tenant ids) can never reach persisted tool results; non-URL
  values are hashed irreversibly instead of echoed, and about: paths are
  restricted to safe fixed names.
- Map exception class names through a fixed allowlist of built-in error
  classes; page-controlled names like CustomerAlice123 are omitted, never
  echoed.
- Bound serialization: url field capped at 256 chars with a visible ellipsis
  marker and the whole block capped at 4 KiB, shedding oldest entries first
  and marking runtimeDiagnosticsTruncated: true (never silent).
- Make capture opt-in via open(..., { diagnostics: true }) so the extra CDP
  session and per-console-event traffic are opted into; docs, prompt, and
  changelog updated.
- Add a WorkerCore-level emission test proving diagnostics reach the result
  (ordering, drain-on-success, retention after failure, byte bound, opt-in
  off) via a loadPuppeteerInWorkerForTest seam, plus extended unit tests.

Constraint: origin-only URLs or irreversible bounded paths only
Constraint: fixed allowlist for error class names
Constraint: explicit truncation/drop metadata and total byte cap
Rejected: query-masked pathname retention | path segments can carry secrets
Rejected: identifier-regex class validation | pages control those names
Tested: 16 focused diagnostics tests; 42 supervisor/action browser tests;
coding-agent check:types; Biome
Not-tested: live Chromium probe in this environment

Lore-id: 4175-repair
Confidence: high
Scope-risk: narrow
Reversibility: easy
Supersedes: 412bd44 review posture
@Yeachan-Heo
Yeachan-Heo force-pushed the feat/browser-runtime-diagnostics branch from 412bd44 to 154d8f7 Compare August 10, 2026 13:36
@Yeachan-Heo

Copy link
Copy Markdown
Owner

Repair evidence — blockers from review 4896349024 are closed on this head

Maintainer probepark's REQUEST_CHANGES (4896349024) was filed against head 412bd44afb. This PR head is now 154d8f712fab7e01d01142b303d9dc1725357f6f (base da4c14a8b4c, current dev), carrying two commits:

  1. a3d1fea7fafeat(browser): surface bounded runtime diagnostics (original PR content rebased onto current dev)
  2. 154d8f712ffix(browser): close runtime-diagnostics privacy and size gaps

The maintainer's repo owner carried the repair on owner-controlled branch repair/4175-browser-diagnostics-repair (also pushed to origin) and force-updated this PR head from it; the contributor's original commit remains reachable as 412bd44afb.

BLOCKER 1 — pathname secret leakage: fixed

maskBrowserRuntimeUrl now emits origin-only URLs for http(s): path segments (signed URLs, invite/reset links, per-tenant ids) are never retained, so persisted tool results cannot leak path-embedded tokens. Non-parseable values are no longer echoed: fixed engine literals (eval, anonymous, __puppeteer_evaluation_script__, …) pass through, everything else becomes an irreversible bounded hash ([non-url:<sha256-8>]). about: paths are restricted to safe fixed names.

Maintainer probe re-run against the new head:

input   https://example.com/account/SESSION_TOKEN_ABC123?k=v
output  https://example.com

BLOCKER 2 — page-controlled exception class: fixed

class is now emitted only when it matches a fixed allowlist of built-in error classes (Error, TypeError, ReferenceError, RangeError, SyntaxError, URIError, EvalError, AggregateError, InternalError, DOMException, the DOMException names, and the WebAssembly error classes). Page-controlled names such as CustomerAlice123 are omitted, never echoed.

BLOCKER 3 — unbounded per-entry/total bytes: fixed

Serialization is bounded on the emission path: the url field is hard-capped at 256 chars with a visible marker (every other field is structurally bounded: ISO timestamp, finite integers, allowlisted class), and the whole serialized block is capped at 4 KiB (UTF-8). When the budget is tight the oldest entries are shed first (newest survive) and the block carries an explicit runtimeDiagnosticsTruncated: true — truncation is never silent. The 300,000-byte-pathname repro now emits https://example.com only.

Test gap — worker output delivery: fixed

New WorkerCore-level emission tests (browser-tab-worker-emission.test.ts, via a __setLoadPuppeteerInWorkerForTest seam) prove diagnostics actually reach the worker result: output ordering (user display → diagnostics block), drain-on-success, retention after failure, the byte bound on a 300k-byte pathname, CDP session detach on close, and that opting out instruments nothing.

CDP overhead flag — decided: opt-in

The extra CDP session plus Runtime.enable per tab and every Runtime.consoleAPICalled event crossing CDP is real per-event work. Capture is now opt-in via open(..., { diagnostics: true }); default-off means zero CDP Runtime subscription and zero per-event traffic unless requested. Docs, prompt, and changelog updated.

Local verification on the exact new head

  • bun test packages/coding-agent/test/tools/browser-runtime-diagnostics.test.ts packages/coding-agent/test/tools/browser-actions.test.ts packages/coding-agent/test/tools/browser-tab-worker-startup.test.ts packages/coding-agent/test/tools/browser-tab-worker-emission.test.ts35 pass / 0 fail
  • bun --cwd=packages/coding-agent run check (Biome + tsc) — pass
  • Focused browser suites (42 supervisor/action tests) — pass

Hosted CI is running against this head; a fresh independent review follows. This repair supersedes the maintainer review posture on 412bd44afb — the four blocking findings are addressed on the new head.


[repo owner's gaebal-gajae (clawdbot) 🦞]

Yeachan-Heo pushed a commit that referenced this pull request Aug 10, 2026
Maintainer review 4896349024 on PR #4175 reproduced raw pathname leakage into
persisted tool results, page-controlled error class names, and an unbounded
serialized diagnostics block. Close all three and make the feature opt-in:

- Emit http(s) URLs origin-only so path-embedded tokens (signed URLs, invite/
  reset links, per-tenant ids) can never reach persisted tool results; non-URL
  values are hashed irreversibly instead of echoed, and about: paths are
  restricted to safe fixed names.
- Map exception class names through a fixed allowlist of built-in error
  classes; page-controlled names like CustomerAlice123 are omitted, never
  echoed.
- Bound serialization: url field capped at 256 chars with a visible ellipsis
  marker and the whole block capped at 4 KiB, shedding oldest entries first
  and marking runtimeDiagnosticsTruncated: true (never silent).
- Make capture opt-in via open(..., { diagnostics: true }) so the extra CDP
  session and per-console-event traffic are opted into; docs, prompt, and
  changelog updated.
- Add a WorkerCore-level emission test proving diagnostics reach the result
  (ordering, drain-on-success, retention after failure, byte bound, opt-in
  off) via a loadPuppeteerInWorkerForTest seam, plus extended unit tests.

Constraint: origin-only URLs or irreversible bounded paths only
Constraint: fixed allowlist for error class names
Constraint: explicit truncation/drop metadata and total byte cap
Rejected: query-masked pathname retention | path segments can carry secrets
Rejected: identifier-regex class validation | pages control those names
Tested: 16 focused diagnostics tests; 42 supervisor/action browser tests;
coding-agent check:types; Biome
Not-tested: live Chromium probe in this environment

Lore-id: 4175-repair
Confidence: high
Scope-risk: narrow
Reversibility: easy
Supersedes: 412bd44 review posture
@Yeachan-Heo
Yeachan-Heo force-pushed the feat/browser-runtime-diagnostics branch from 154d8f7 to a2ef136 Compare August 10, 2026 13:59
chulmin-dev and others added 2 commits August 10, 2026 14:03
Page exceptions and console errors were invisible to browser callers when an interaction otherwise succeeded. Capture only bounded metadata and drain it into the next successful act/run response without retaining messages, values, arguments, or stacks.

Constraint: keep at most 20 entries per tab and mask URL queries

Rejected: domain sandbox and irreversible-action gating | raw Puppeteer run makes those separate policy designs

Confidence: high

Scope-risk: narrow

Reversibility: easy

Tested: 24 browser tests; coding-agent typecheck; live Chromium exception and console probe
Maintainer review 4896349024 on PR Yeachan-Heo#4175 reproduced raw pathname leakage into
persisted tool results, page-controlled error class names, and an unbounded
serialized diagnostics block. Close all three and make the feature opt-in:

- Emit http(s) URLs origin-only so path-embedded tokens (signed URLs, invite/
  reset links, per-tenant ids) can never reach persisted tool results; non-URL
  values are hashed irreversibly instead of echoed, and about: paths are
  restricted to safe fixed names.
- Map exception class names through a fixed allowlist of built-in error
  classes; page-controlled names like CustomerAlice123 are omitted, never
  echoed.
- Bound serialization: url field capped at 256 chars with a visible ellipsis
  marker and the whole block capped at 4 KiB, shedding oldest entries first
  and marking runtimeDiagnosticsTruncated: true (never silent).
- Make capture opt-in via open(..., { diagnostics: true }) so the extra CDP
  session and per-console-event traffic are opted into; docs, prompt, and
  changelog updated.
- Add a WorkerCore-level emission test proving diagnostics reach the result
  (ordering, drain-on-success, retention after failure, byte bound, opt-in
  off) via a loadPuppeteerInWorkerForTest seam, plus extended unit tests.

Constraint: origin-only URLs or irreversible bounded paths only
Constraint: fixed allowlist for error class names
Constraint: explicit truncation/drop metadata and total byte cap
Rejected: query-masked pathname retention | path segments can carry secrets
Rejected: identifier-regex class validation | pages control those names
Tested: 16 focused diagnostics tests; 42 supervisor/action browser tests;
coding-agent check:types; Biome
Not-tested: live Chromium probe in this environment

Lore-id: 4175-repair
Confidence: high
Scope-risk: narrow
Reversibility: easy
Supersedes: 412bd44 review posture
@Yeachan-Heo
Yeachan-Heo force-pushed the feat/browser-runtime-diagnostics branch from a2ef136 to b70775c Compare August 10, 2026 14:03
@Yeachan-Heo

Copy link
Copy Markdown
Owner

Final head b70775c440 — superseding evidence and terminal review summary

The repair is complete and independently reviewed. This PR's head is now b70775c440bf3e63087a5db3653dcc6eedc9d878 (base 7858b0ff63, current dev), carrying the two-commit repair:

  1. f27331d697feat(browser): surface bounded runtime diagnostics (original PR content rebased onto current dev)
  2. b70775c440fix(browser): close runtime-diagnostics privacy and size gaps

The owner-controlled branch repair/4175-browser-diagnostics-repair on Yeachan-Heo/gajae-code tracks the same two commits.

Exact-head hosted CI — fully green

25 check runs on this head: 20 success / 5 skipped (platform/irrelevant lanes) / 0 failures — including test:packages/coding-agent/test/tools/browser-runtime-diagnostics.test.ts, test:packages/coding-agent/test/tools/browser-tab-worker-emission.test.ts, Affected path validation / plan, evidence producer, darwin-arm64 tab-worker smoke, native-build, cli-smoke, install-methods, and Virtual integration validation.

Independent review (fresh lanes on the frozen change set)

  • Architect lane: architecture/product/code all CLEAR, recommendation APPROVE. Three LOW advisory findings (truncation-magnitude counter, init-failure CDP session cleanup, prompt <output> summary omitting the diagnostics block) — none blocking.
  • Executor QA/red-team lane: 93 adversarial probes with real executions — secret-in-path/query/userinfo/host, 300k-byte pathname, page-controlled class names (CustomerAlice123, 10k-char, unicode), non-parseable raw paths, byte-budget boundary, mailbox eviction/drain, opt-in wiring. Two synthetic/type-violating edge probes (100k-char host in origin; single over-budget entry) failed on the first pass and were fixed and re-verified: the masker now caps the origin at 256 chars with a visible marker, and an over-budget single entry now always carries runtimeDiagnosticsTruncated: true (truncation is never silent). Regression tests lock both.
  • Live Chromium probe (real headless Chromium + local HTTP server): page error and console.error carrying SESSION_TOKEN_ABC123/SECRETQ/secret message values drained as origin-only masked URLs — zero leakage, bounded 390-byte block, no truncation.
  • Focused suites: 37 pass / 0 fail (11 diagnostics unit + 5 worker emission + browser actions + tab-worker startup), bun --cwd=packages/coding-agent run check (Biome + tsc) green.

Blockers from review 4896349024 — all closed

  1. Raw pathname secret leakage → http(s) URLs are origin-only; non-URL values hashed irreversibly; about: restricted to safe fixed names; masker itself bounded at 256 chars.
  2. Page-controlled exception class → fixed allowlist of built-in error classes only; page-controlled names omitted.
  3. Unbounded per-entry/total bytes → per-field cap (visible ), 4 KiB total block cap, oldest shed first, explicit runtimeDiagnosticsTruncated: true.
  4. Missing worker emission mutation testbrowser-tab-worker-emission.test.ts proves ordering, drain-on-success, retention after failure, byte bound, opt-in-off.
  5. CDP overhead flag → decided: capture is opt-in (open(..., { diagnostics: true })); default-off means no CDP Runtime subscription and no per-event traffic unless requested.

This supersedes maintainer review 4896349024 (filed against 412bd44afb, which is no longer the head). The contributor's original commit remains reachable as 412bd44afb. No merge has been performed and none is requested here without the owner's go-ahead.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo Yeachan-Heo left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Terminal review — MERGE_READY — exact head b70775c440bf3e63087a5db3653dcc6eedc9d878

gajae.pr-review-verdict.v1 merge-ready sha256:f8d2c89b3a0581c2 reviewer:owner-bot evidence:37-focused-tests-pass,package-check-green,hosted-CI-25-runs-0-fail,architect-APPROVE,qa-93-probes+live-chromium-zero-leak,terminal-critic-OKAY

This review supersedes maintainer review 4896349024 (filed against 412bd44afb, now outdated). All four blocking findings are closed on this head, and the flagged CDP-overhead question is resolved as opt-in:

  1. Pathname secret leakage — http(s) URLs emit origin-only; non-URL values hash irreversibly; about: restricted; the masker itself is bounded at 256 chars with a visible marker. The maintainer's SESSION_TOKEN_ABC123-in-path probe now yields https://example.com.
  2. Page-controlled exception class — emitted only from a fixed allowlist of built-in error classes; CustomerAlice123 is omitted.
  3. Unbounded per-entry/total bytes — per-field cap plus a 4 KiB total block cap, oldest shed first, explicit runtimeDiagnosticsTruncated: true (never silent, including the single over-budget entry edge).
  4. Missing worker emission mutation testbrowser-tab-worker-emission.test.ts proves output ordering, drain-on-success, retention after failure, the 300k-byte-pathname bound, and opt-in-off.
  5. CDP overhead decision — capture is opt-in (open(..., { diagnostics: true })); default-off creates no CDP Runtime subscription and no per-event traffic.

Verification on the exact head: 37 focused tests pass / 0 fail; bun --cwd=packages/coding-agent run check (Biome + tsc) green; hosted CI 25 check runs with 0 failures; architect lane CLEAR/APPROVE; executor QA 93 real probes with a live headless-Chromium probe (zero secret leakage, bounded block) after fixing and re-verifying two synthetic edge probes; terminal critic gate OKAY.

No merge or release has been performed and none is implied by this verdict; the change is ready for the owner's merge decision. Original contributor commit remains reachable as 412bd44afb.


[repo owner's gaebal-gajae (clawdbot) 🦞]

@Yeachan-Heo
Yeachan-Heo merged commit dee4606 into Yeachan-Heo:dev Aug 10, 2026
25 checks passed
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.

3 participants