Skip to content

feat: DevFlow Web Inspector with AJAX refresh and modal support#295

Open
mattleibow wants to merge 13 commits into
mainfrom
mattleibow/jubilant-guide
Open

feat: DevFlow Web Inspector with AJAX refresh and modal support#295
mattleibow wants to merge 13 commits into
mainfrom
mattleibow/jubilant-guide

Conversation

@mattleibow
Copy link
Copy Markdown
Member

Summary

Adds a web-based visual inspector for .NET MAUI DevFlow that renders a running app as an interactive HTML page in the browser. The inspector integrates into the DevFlow broker daemon and is accessible at http://localhost:19223/inspector/.

Features

  • Live app mirroring — screenshot + positioned div overlays for every UI element
  • AJAX refresh — polls every 3s without page flash (DOM diff, not reload)
  • Interaction proxying — click→tap, wheel→scroll, drag→gesture forwarded to native app
  • Modal/dialog support — detects topmost page (modals) and screenshots that
  • Flat element rendering — all divs use window-absolute coordinates for accurate positioning
  • Playwright test suite — 12 integration tests covering viewport, elements, interactions

Architecture

  • Inspector runs in the broker (port 19223), NOT in the native app
  • Routes: /inspector/ (agent list), /inspector/{agentId}/ (app view)
  • GET /api/state returns JSON with screenshot URL + element HTML for AJAX updates
  • Hit-test + bubble-through for tap/scroll (finds tappable/scrollable element)

Files

  • src/Cli/.../Inspector/InspectorServer.cs — HTTP server + broker integration
  • src/Cli/.../Inspector/HtmlRenderer.cs — Tree → flat HTML
  • src/Cli/.../Inspector/Web/ — HTML template, CSS, JS
  • src/DevFlow/.../Inspector.Tests/ — Playwright tests
  • src/DevFlow/.../ElementInfo.cs — Added WindowBounds property

mattleibow and others added 5 commits May 22, 2026 21:51
Adds 'maui devflow inspector' command that serves the running MAUI app
as an interactive HTML page at localhost:5223. An external tool can connect
to this URL and see the native app rendered with:

- Screenshot background with positioned divs for each UI element
- data-* attributes exposing all DevFlow element properties
- Click → tap, wheel → scroll, pointer drag → gesture proxying
- Toolbar with back/refresh buttons
- Auto-refresh via WebSocket when UI tree changes

Architecture: CLI hosts the inspector HTTP server (not in-app), proxies
all interactions to the DevFlow agent via its existing REST API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ewport sizing

- Remove toolbar and hover highlighting (host inspector provides its own)
- Extract HTML to inspector.html template with {{placeholders}}
- Separate CSS into devflow.css (editable with proper syntax highlighting)
- Use actual screenshot PNG dimensions for viewport (not tree bounds)
- Read PNG IHDR chunk for width/height
- Add Playwright tests validating HTML structure, no chrome, correct sizing
- Add ValidateXcodeVersion=false to Directory.Build.props

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…wright tests

- Viewport dimensions now come from agent status API (device.windowWidth/Height)
  which works cross-platform (Android, iOS, Mac, Windows)
- Fallback to PNG IHDR dimensions if status unavailable
- CSS zoom-to-fit: viewport scales down to fit browser window (never upscales)
- JS coordinate conversion accounts for zoom scale factor
- Replace Node.js Playwright tests with proper C# xUnit + Microsoft.Playwright
  tests in src/DevFlow/Microsoft.Maui.DevFlow.Inspector.Tests/
- 12 tests covering: viewport sizing, zoom, no chrome, element positioning,
  nested tree, data attributes, click→tap interaction, screenshot endpoint

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The web inspector is now served directly by the broker at
http://localhost:19223/inspector/ (or /inspector/{agentId}/ for
multi-agent). Since the broker is already running whenever a MAUI
app connects, this removes the need for a separate 'maui devflow
inspector' command.

Changes:
- BrokerServer: add /inspector routes that proxy to InspectorServer
- InspectorServer: add HandleBrokerRequestAsync + WebSocket relay
- Remove standalone 'inspector' CLI subcommand
- Make all asset paths relative (works under any URL prefix)
- devflow.js: derive basePath from location.pathname
- Clean up inspector on agent disconnect

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace full page reload with AJAX state polling (/api/state endpoint)
  to avoid page flash during 3-second refresh interval
- Fix FindRootPageId to use last child of Window (topmost/modal page)
  instead of first child, fixing blank screenshot when modal is showing
- Change tap handler to return 200 {ok:false} instead of 404 when no
  tappable element found at coordinates
- Extract RenderElements method for generating element HTML independently
- Fix wheel scroll closure bug (capture coords before timeout fires)
- Update Playwright tests for flat div structure and AJAX behavior
- Add StateEndpoint and AjaxRefresh tests

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 22, 2026 22:25
Replace naive remove-all/insert-all element refresh with a keyed diff
based on data-id. Elements that haven't changed stay in the DOM,
preserving browser hover state, inspector selection, and DevTools
highlighting. Only style/attributes are updated in-place when they
differ. New elements are inserted, removed ones are deleted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a broker-hosted DevFlow Web Inspector (/inspector/) that renders a connected app as HTML (screenshot + positioned element overlays) and proxies browser interactions (tap/scroll/gesture) back to the DevFlow agent. The PR also introduces inspector web assets, a Playwright-based test project scaffold, documentation, and a small extension to the Driver element model.

Changes:

  • Add broker integration and inspector server implementation (HTML rendering, AJAX polling, interaction proxying, WebSocket proxy).
  • Embed inspector web assets (HTML/CSS/JS) into the CLI and add documentation/spec.
  • Extend ElementInfo with WindowBounds and introduce a Playwright test project + Playwright package version.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/DevFlow/Microsoft.Maui.DevFlow.Inspector.Tests/Microsoft.Maui.DevFlow.Inspector.Tests.csproj New Playwright-based test project (references CLI + Driver).
src/DevFlow/Microsoft.Maui.DevFlow.Inspector.Tests/InspectorPageTests.cs Playwright integration tests exercising inspector rendering and interactions.
src/DevFlow/Microsoft.Maui.DevFlow.Driver/ElementInfo.cs Adds WindowBounds to support window-absolute positioning in the inspector.
src/Cli/Microsoft.Maui.Cli/Microsoft.Maui.Cli.csproj Embeds inspector web assets as resources for serving from the broker/CLI.
src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/Web/inspector.html HTML template for the rendered inspector page.
src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/Web/devflow.js Client-side polling + event interception (click/wheel/pointer) → proxy endpoints.
src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/Web/devflow.css Minimal styles for viewport + overlay elements.
src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs Core inspector HTTP routing, screenshot caching, proxy endpoints, and WS proxy logic.
src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/HtmlRenderer.cs Visual tree → flat, positioned <div> overlay renderer with data-* attributes.
src/Cli/Microsoft.Maui.Cli/DevFlow/Broker/BrokerServer.cs Routes /inspector/... through per-agent InspectorServer instances and serves an agent list page.
docs/DevFlow/inspector.md Inspector design/spec documentation (currently diverges from implementation).
Directory.Packages.props Adds central version for Microsoft.Playwright.
Directory.Build.props Disables Xcode validation globally via ValidateXcodeVersion=false.

Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs Outdated
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Broker/BrokerServer.cs Outdated
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/HtmlRenderer.cs
Comment thread src/DevFlow/Microsoft.Maui.DevFlow.Inspector.Tests/InspectorPageTests.cs Outdated
Comment thread Directory.Build.props Outdated
Comment thread docs/DevFlow/inspector.md
@lextm
Copy link
Copy Markdown

lextm commented May 22, 2026

That's a great idea and can also host in a VS Code extension easily as a side project.

@github-actions
Copy link
Copy Markdown
Contributor

Expert Code Review — PR #295

Methodology: 3 independent reviewers with adversarial consensus + targeted follow-up validation for disputed findings.

Summary

11 findings posted as inline comments — 3 critical, 5 moderate, 3 minor.

Severity Count Key themes
🔴 Critical 3 XSS injection, race condition on shared state, CORS misconfiguration
🟡 Moderate 5 Unbounded body read (DoS), WebSocket hang, socket exhaustion, UTF-8 corruption, resource leak
🟢 Minor 3 PNG validation, repo-wide Xcode suppression, static cache thread safety

Disputed Findings (resolved via follow-up)

Three findings initially flagged by only 1 reviewer were validated via follow-up agents — all 3 achieved consensus (2/2 follow-up agreement) and were included:

  • Socket exhaustion from per-request AgentClient creation (Reviewer 1 → confirmed by Reviewers 2 & 3)
  • Unbounded request body enabling local DoS (Reviewer 2 → confirmed by Reviewers 1 & 3)
  • Inspector resource leak on broker shutdown (Reviewer 1 → confirmed by Reviewers 2 & 3)

Discarded Findings (no consensus)

  • WebSocket handshake doesn't validate Sec-WebSocket-Version header (1/3 only, 🟡) — below follow-up cap
  • Test project not added to solution filter (1/3 only, 🟡) — below follow-up cap
  • Partial agent ID match could cause confusing behavior (1/3 only, 🟢) — below follow-up cap
  • DOM XSS surface via innerHTML in devflow.js (1/3 only, 🟢) — below follow-up cap

CI Status

✅ 11/12 checks passed (CLA, build on macOS/Windows/Ubuntu across all products). 1 check (build-macos for macOS AppKit) still in progress.

Test Coverage

The PR includes a new Playwright-based test project (Microsoft.Maui.DevFlow.Inspector.Tests) with InspectorPageTests.cs covering the inspector HTML rendering. However, the security-sensitive paths (XSS, CORS, body parsing, WebSocket proxy) lack dedicated unit tests. Consider adding tests for input sanitization and boundary conditions.


Generated by Expert Code Review · 3 independent reviewers with adversarial consensus

Generated by Expert Code Review (auto) for issue #295 · ● 21M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Expert Code Review: 11 findings posted inline (3 critical, 5 moderate, 3 minor). See the summary comment for methodology and discarded findings.

Generated by Expert Code Review (auto) for issue #295 · ● 21M

Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Broker/BrokerServer.cs Outdated
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs Outdated
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs Outdated
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs
Comment thread Directory.Build.props
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/HtmlRenderer.cs Outdated
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs Outdated
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Broker/BrokerServer.cs
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs Outdated
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs Outdated
@rmarinho
Copy link
Copy Markdown
Member

@lextm I m poc adding to the vscode maui extension

@lextm
Copy link
Copy Markdown

lextm commented May 25, 2026

Sure. I will add this concept to the wpf-labs repo and vscode-wpf extension as foundation of Live Preview.

rmarinho and others added 5 commits May 30, 2026 01:39
…sts)

Security hardening:
- BrokerServer: replace wildcard CORS with localhost-only Origin mirroring;
  validate Origin BEFORE dispatching POST handlers (the previous check ran
  after HandleShutdown(), so a cross-origin POST still tore down the broker
  even though we returned 403). Verified end-to-end with curl.
- BrokerServer: reject non-loopback Origin on /ws/agent WebSocket upgrade.
- InspectorServer: drop "Access-Control-Allow-Origin: *"; same-origin only.
- InspectorServer: enforce Origin on POST endpoints and /ws/v1/ui/events
  WebSocket proxy (CSRF / cross-origin event subscription).
- Add shared LocalOriginValidator helper with unit tests.

Correctness:
- InspectorServer: cap request bodies at 1 MiB, return 413 on oversize
  (both broker-hosted and standalone TCP paths), with per-read 10 s timeout
  on body reads to prevent slow-drip resource exhaustion.
- InspectorServer: share a single AgentClient (and dispose it) instead of
  constructing one per request; gate the screenshot cache with a lock.
- InspectorServer: fix WS proxy URL (/ws/events -> /ws/v1/ui/events) and
  wire lifetime cancellation through _lifetimeCts so the proxy actually
  tears down on dispose.
- InspectorServer: read request bodies as raw bytes once and decode as
  UTF-8 (the prior ASCII path mangled non-ASCII fill text).
- InspectorServer: validate PNG signature + reject negative dims before
  trusting embedded dimensions.
- HtmlRenderer: format all numeric attributes with InvariantCulture and
  cache the template via Lazy<string> (ExecutionAndPublication).
- BrokerServer: HtmlEncode AppName/Platform/Tfm and UrlEncode agent ids
  in the agent list page (stored XSS via agent metadata).
- BrokerServer: dispose hosted inspectors on shutdown.

Tests:
- New InspectorHtmlRendererTests cover XSS escaping and invariant-culture
  number formatting (forced via pt-PT comma decimal).
- New LocalOriginValidatorTests cover loopback / hostile / malformed Origin.
- InspectorPageTests now compose URLs via Uri and parse doubles with
  InvariantCulture; default URL points at the broker single-agent fallback.

Build:
- Directory.Build.props: scope <ValidateXcodeVersion>false</> to Apple
  TFMs (ios/maccatalyst/macos/tvos) instead of repo-wide.

558 unit tests pass; broker security boundaries verified live via curl.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clicking an Entry, Editor, SearchBar, or other text-input element in the
web inspector now opens a teal-bordered overlay editor (input or
textarea) on top of the field, pre-filled with the field's current text.
Pressing Enter or clicking away commits the change via POST /api/fill;
Escape cancels. The native control still receives a tap so it focuses on
the app side.

Detection uses data-type (Entry, Editor, SearchBar, TextField, TextBox,
TextArea, TextView, UITextField, UITextView, EditText, NSTextField) and
a heuristic on data-traits for 'textinput' / 'editable'.

Uses document.elementFromPoint(clientX, clientY) to find the underlying
element because the existing gesture handler calls
viewport.setPointerCapture(), which causes real mouse clicks to report
the viewport itself as e.target.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The inspector spec previously documented a standalone `maui devflow
inspector` command on port 5223 with a toolbar and nested element tree.
The implemented behavior is broker-hosted at port 19223 with a flat
element list and no toolbar.

Update the architecture diagram and Usage section to reflect the
current broker-hosted setup at http://localhost:19223/inspector/, and
re-label the standalone command as future work.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho requested review from Copilot and rmarinho June 2, 2026 21:59
@rmarinho
Copy link
Copy Markdown
Member

rmarinho commented Jun 2, 2026

/review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 2, 2026

Expert Code Review completed successfully!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.

Comment thread src/Cli/Microsoft.Maui.Cli.UnitTests/LocalOriginValidatorTests.cs
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs
Comment thread src/DevFlow/Microsoft.Maui.DevFlow.Inspector.Tests/InspectorPageTests.cs Outdated
Comment thread Directory.Build.props
- LocalOriginValidator: reject literal Origin "null" (file://, sandboxed
  iframes, data:, opaque origins). Empty/missing Origin still allowed for
  non-browser clients. Tests updated.
- InspectorServer.HandleClientAsync: apply LocalOriginValidator to POSTs
  and /ws/events upgrades in standalone-listener mode, mirroring broker.
- InspectorPageTests.ViewportUsesWindowDimensionsFromAgent: correlate DOM
  data-width/height with /api/state instead of asserting != 390/844 (which
  would false-positive on real iPhone-sized devices).
- InspectorPageTests.DataAttributesUseCamelCase: assert against raw HTML
  text (case-sensitive) instead of CSS attribute selectors (case-insensitive
  in HTML), so lowercase serialization regressions actually fail.
- Directory.Build.props: scope ValidateXcodeVersion=false to non-CI builds
  so CI/official builds keep the Xcode-version guardrail; local dev still
  unblocked.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Expert Code Review: 6 findings posted inline (3 critical, 2 moderate, 1 minor). See the summary comment for full methodology and discarded findings.

Generated by Expert Code Review for issue #295 · ● 22.4M

Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Broker/BrokerServer.cs Outdated
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Broker/BrokerServer.cs
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs Outdated
Comment thread src/Cli/Microsoft.Maui.Cli/DevFlow/Inspector/InspectorServer.cs
- BrokerServer.HandleInspectorRoute: fix ConcurrentDictionary.GetOrAdd race
  where the factory may be invoked concurrently for the same agent id. Use
  TryGetValue + GetOrAdd with dispose-on-loss so the discarded InspectorServer
  doesn't leak its AgentClient/HttpClient, CTS, and TCP listener.
- BrokerServer.HandleRequestAsync: enforce loopback on RemoteEndPoint as a
  defense-in-depth check. The broker is designed for loopback only, but
  HttpListener falls back to binding on all interfaces (http://+:port/) on
  Windows/non-localhost platforms. Origin-based CSRF protection alone
  doesn't help against non-browser LAN callers (curl, scripts, attacker)
  that don't send Origin. All legitimate uses are loopback.
- InspectorServer.HandleWebSocketProxy: bidirectional relay via Task.WhenAny
  so a closed browser tab unblocks the loop immediately instead of leaking
  a task until the agent next sends data (or _lifetimeCts is cancelled).
  Extracted a shared RelayLoopAsync helper.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
Copy link
Copy Markdown
Member

rmarinho commented Jun 3, 2026

/review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

Expert Code Review completed successfully!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 3, 2026

Expert Code Review — PR #295

Methodology: 3 independent reviewers with adversarial consensus

Result: 6 findings posted as inline comments (3 🟡 moderate, 3 🟢 minor)

All findings are posted inline — no overflow table needed.

Discarded Findings (did not reach consensus)

Finding Flagged by Outcome
Double-dispose of CancellationTokenSource in WebSocket proxy Reviewer 1 Follow-up: both other reviewers disagreed (CTS.Dispose() is idempotent)
TcpClient parameter unused in HandleWebSocketProxy Reviewer 2 Below cap — not followed up
Internal exception messages exposed in HTTP responses Reviewer 3 Below cap — not followed up

CI Status

✅ All 13 check runs passed (macOS + Windows builds, CLA)

Test Coverage

The PR includes both unit tests (InspectorServerTests.cs) and Playwright integration tests (InspectorIntegrationTests.cs) covering HTML rendering, action proxying, WebSocket relay, and refresh polling. The test coverage is solid for the core happy-path scenarios. The consensus findings above relate to edge cases (fragmented TCP, tab close, agent reconnect) that are harder to test without lower-level mocking.


Generated by Expert Code Review · 3 independent reviewers with adversarial consensus

Generated by Expert Code Review for issue #295 · ● 22.1M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Expert Code Review: 6 findings posted inline (3 moderate, 3 minor). See the summary comment for methodology and discarded findings.

Generated by Expert Code Review for issue #295 · ● 22.1M

Comment on lines +33 to +35

var host = uri.Host;
return host.Equals("localhost", StringComparison.OrdinalIgnoreCase)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MODERATE · Consensus: 2/3 reviewers

CSRF via any-port localhost origin: IsAllowed validates scheme and host but not port. A malicious page at (localhost/redacted) (e.g., compromised dev server, VS Code webview) can POSTto(localhost/redacted) or inspector action endpoints. Its Origin: (localhost/redacted) passes this check because the host is localhost` regardless of port.

Per RFC 6454, an origin is scheme+host+port — a page on port 3000 is a distinct security principal.

Suggestion: Also validate the port matches the broker/inspector port, or add a CSRF nonce/session token mechanism:

return (host.Equals("localhost", ...) || host == "127.0.0.1" || ...)
    && (uri.Port == expectedPort || uri.Port == -1);

Comment on lines +519 to +521
{
// Try partial match
connection = _agents.Values.FirstOrDefault(a =>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 MINOR · Consensus: 2/3 reviewers

Partial agent-ID prefix match can route to wrong agent: With multiple agents connected (e.g., com.example.FooApp-android and com.example.FooBarApp-ios), a request to /inspector/com.example.Foo/ matches the first one in ConcurrentDictionary iteration order (non-deterministic). Combined with the single-agent fallback on line 525, any invalid ID routes to the only agent — convenient but potentially surprising.

Suggestion: Require the prefix to be unambiguous (error if multiple agents match the prefix), or remove prefix matching entirely and rely only on exact match + single-agent fallback:

if (!_agents.TryGetValue(agentId, out var connection))
{
    if (_agents.Count == 1)
        connection = _agents.Values.First();
    // else: 404 (no ambiguous prefix matching)
}

Comment on lines +764 to +769
timeoutCts.CancelAfter(TimeSpan.FromSeconds(5));

try
{
read = await stream.ReadAsync(buffer, timeoutCts.Token);
if (read == 0) return (null, false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MODERATE · Consensus: 3/3 reviewers

Single-read HTTP parsing drops fragmented headers: ReadRequestAsync performs one stream.ReadAsync of up to 8192 bytes and expects the full HTTP headers within that single read. TCP can fragment data arbitrarily — if \r\n\r\n isn't found in the first read, the request is silently dropped (return (null, false)).

Failing scenario: A browser with many cookies on localhost, or a slow network/proxy, splits headers across multiple TCP segments. The request is silently dropped with no response and no log entry.

Suggestion: Accumulate reads in a loop until \r\n\r\n is found or a max header size (e.g., 64KB) is exceeded:

var ms = new MemoryStream();
while (headerEnd < 0 && ms.Length < MaxHeaderBytes)
{
    read = await stream.ReadAsync(buffer, timeoutCts.Token);
    if (read == 0) break;
    ms.Write(buffer, 0, read);
    // Re-scan for \r\n\r\n
}

Comment on lines +546 to +549
if (!_inspectors.TryGetValue(connection.Registration.Id, out var inspector))
{
var created = new InspectorServer(0, "localhost", connection.Registration.Port);
inspector = _inspectors.GetOrAdd(connection.Registration.Id, created);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MODERATE · Consensus: 3/3 reviewers

Inspector lifecycle gaps — stale/orphaned instances: Three related issues identified by all reviewers:

  1. Race condition: If an agent disconnects between the _agents.TryGetValue (line 517) and this GetOrAdd, the new InspectorServer is permanently orphaned in _inspectorsMonitorAgentConnection already ran its TryRemove for this ID so the inspector is never cleaned up.

  2. Stale port on reconnect: If an agent restarts with a new port, the cached inspector still holds the old AgentClient(host, oldPort). All requests route to the dead port.

  3. Shutdown leak: The Shutdown() method (outside this diff) clears _agents but doesn't dispose _inspectors. If the process exits through Shutdown() without Dispose(), inspectors leak.

Suggestion: After GetOrAdd, verify the agent is still registered. On port mismatch (reconnect), remove and recreate. Ensure Shutdown() also disposes inspectors:

if (!_agents.ContainsKey(connection.Registration.Id))
{
    _inspectors.TryRemove(connection.Registration.Id, out _);
    inspector.Dispose();
    // Return 503
}

function patchElements(newHtml) {
// Parse new elements into a temp container
const temp = document.createElement('div');
temp.innerHTML = newHtml;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 MINOR · Consensus: validated via follow-up (3/3 agree — defense-in-depth)

innerHTML with server-rendered HTML — fragile XSS boundary: patchElements() sets temp.innerHTML = newHtml where content comes from the server's HtmlRenderer. Server-side HttpUtility.HtmlAttributeEncode escapes ", &, < which prevents breakout from double-quoted attributes today. However, innerHTML is inherently XSS-risky — if escaping or output format ever changes, this becomes exploitable.

Not an active exploit with current code, but a fragile trust boundary.

Suggestion: Consider using DOM APIs (createElement + setAttribute) instead of innerHTML parsing, or add a comment documenting the security invariant that the server must always produce safe HTML for this path.

Comment on lines +686 to +690
await agentWs.SendAsync(subscribe, System.Net.WebSockets.WebSocketMessageType.Text, true, ct);

// Relay agent messages to browser
var buffer = new byte[8192];
while (!ct.IsCancellationRequested && agentWs.State == System.Net.WebSockets.WebSocketState.Open)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 MINOR · Consensus: validated via follow-up (3/3 agree)

Standalone WebSocket proxy is unidirectional — closed tabs hang: HandleWebSocketProxy only reads from the agent WebSocket (agent→browser direction). When a browser tab closes and sends a Close frame, nothing reads it from the TCP stream. The relay loop only exits when the agent sends its next message or _cts is cancelled.

Each abandoned browser tab holds a TcpClient + NetworkStream + ClientWebSocket open until the next agent event or global shutdown.

Contrast: The broker-mode HandleBrokerWebSocketProxy correctly uses bidirectional Task.WhenAny(agentToClient, clientToAgent) to detect browser-side closes.

Suggestion: Mirror the broker-mode pattern — add a concurrent task draining the browser→agent direction and use Task.WhenAny to exit when either side closes.

@jfversluis
Copy link
Copy Markdown
Member

Did an independent multi-model review pass against current HEAD (7c712b2a). Almost everything I would have raised was already caught in the two review rounds and addressed:

  • Origin: null accepted → already flagged, fixed in 1636f5f0 (now rejected in LocalOriginValidator).
  • Standalone HandleClientAsync missing Origin check on POST//ws/events → already flagged, fixed in 1636f5f0.
  • GetOrAdd creating an undisposed InspectorServer → already flagged (critical), fixed in 7c712b2a.
  • WS relay teardown on closed tab → fixed in 7c712b2a (bidirectional RelayLoopAsync + Task.WhenAny).

One thing I don't think was raised and is still present on HEAD:

HandleBrokerWebSocketProxy never sends a subscribe frame. The standalone HandleWebSocketProxy sends {"type":"subscribe","data":{"events":["all"]}} to the agent before relaying (InspectorServer.cs:685), but the broker path — the one actually used in production — connects to /ws/v1/ui/events and goes straight into the relay loop with no subscribe. If the agent only emits events after a subscription, the broker-hosted live-event relay would deliver nothing.

Worth noting this may be moot today: the bundled devflow.js uses 3s AJAX polling (/api/state) and never opens /ws/events, so the WebSocket relay path appears unused by the shipped UI. So either (a) the WS relay is dead code that can be dropped, or (b) it's intended for external clients, in which case the broker path should send the subscribe frame like the standalone path does. Either way the two paths should be consistent.

Minor (low impact, optional): GetCachedScreenshotAsync keys the 200ms cache on time only, ignoring elementId; GetPngDimensions rejects negative dimensions but not absurdly-large positive ones that flow into CSS viewport sizing.

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.

5 participants