cef agent-control P2: multi-tile CDP relay multiplex (+ mandatory token, P1 multi-view base)#3
Merged
Merged
Conversation
…t port-scan)
The per-tile CDP relay's token was "validated if present" — a tokenless
connection was accepted, because vanilla agent-browser connects by bare
`--cdp <port>` and attaches no secret. That left the classic attack open:
malware scans 127.0.0.1, finds the ephemeral relay port, and connects to
drive the page.
Make the token MANDATORY: the ws upgrade is rejected (401) without a valid
`Authorization: Bearer <token>` (a `?token=` query is an accepted fallback).
Playwright forwards request headers on the upgrade, so the integrator's CDP
client presents it via connectOverCDP({ headers }). Discovery (/json/*) stays
token-free, so a port-scanner learns the ws-url but can't upgrade — it never
sees the token. The integrator (Campus) delivers the token to its CDP client
in memory (never disk/argv/env), which also closes the same-UID case.
- tokenAcceptable now takes the header map; accepts Authorization: Bearer
(preferred) or ?token= (fallback); rejects absent/empty/wrong; constant-time.
- readRequestHead rejects a 16 KiB-truncated head instead of parsing a partial.
- Filter test suite: +18 token cases (header/query precedence, last-token-wins,
empty/bare/lookalike query keys, tab-vs-space, Bearer case/empty) — all green.
- Docs (README / CHANGELOG / agent-control PLAN) updated to the mandatory-token
model (the old "validated if present / Playwright can't attach one" notes were
superseded — connectOverCDP({ headers }) works).
NOTE: this must land in Campus together with the agent-browser-broker change
(Campus presents the token); a Campus that hasn't updated would be rejected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ession
Two changes that unblock Campus's shared-login web tiles:
1. cef_host: in agent-control (remote-debugging-pipe) mode, also pass
--disable-blink-features=AutomationControlled so navigator.webdriver is
false. Chromium flips it on under remote debugging, which makes Google's
OAuth (and similar) refuse human sign-in ("this browser may not be secure").
We drive over CDP, never WebDriver, so suppressing it costs nothing.
2. plugin: lift the single-view-per-named-profile guard. resolveOrSpawnHost
already de-dups the host by profile key + serializes early creates via
pendingCreates, and cef_host already multiplexes N browsers (Slot/wire_id)
over one process sharing one cookie jar — so N views on a named profile now
share ONE cef_host (all render, one shared login) instead of colliding on
Chromium's SingletonLock. Agent-control stays single-relay per host for now
(2nd concurrent agent tile gets a clear "already active" error); concurrent
multi-tile agent-control follows with the per-target CDP relay demux.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…relay multiplex
Lifts the one-agent-tile-per-host limit: N tiles sharing one cef_host (one
named profile) can each be agent-controlled concurrently, each over its own
per-target CDP relay, all multiplexed over the single browser-wide
remote-debugging-pipe.
CefProfileHost: replace the scalar cdpRelay/relayBrowserId with
cdpRelays:[UInt32:CdpRelay] keyed by browserId; onCdpMessage fans every pipe
line out to all relays (deliverCdpToRelays — snapshot then deliver outside the
lock); enable/disable/removeBrowser/shutdown are per-browserId; assert
browserIds are strictly monotonic / never reused (the relayId<<21 id space
relies on it).
CdpRelay: per-relay CDP-id rewrite so browser-level commands (no sessionId —
Playwright's connect handshake) from sibling relays don't collide in the
shared pipe id space. pipeId = (relayId<<21)|localSeq; responses demuxed back
to the owning relay by pipeId, siblings' dropped. Session-routed traffic still
demuxes by sessionId via the existing scope filter.
FlutterCefPlugin: route disableAgentControl by the session's browserId; add
shutdownAllHosts on NSApplication.willTerminate (the disposeAll orphan-check —
SIGTERM+reap every live cef_host on app quit so none orphans a profile's
Chromium SingletonLock).
Deviation from specs/cef-multiview/PLAN.md §3.2: the plan expected sessionId
demux to suffice ("no CDP id-namespacing"); browser-level commands forced a
per-relay id rewrite. Documented inline.
WIP — recovered from lost session 31a9d1e2; not yet build-verified on a signed
CEF_HOST_ADHOC=OFF GPU build (PLAN Phase 5 / Test H).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the standalone CdpRelay filter tests (cef-multiview PLAN Test I): two relays sharing one browser-wide pipe, asserting per-relay CDP-id namespacing (pipeId = relayId<<21|localSeq — same client id on two relays yields different pipe ids, no collision), response demux routing each reply only to its issuing relay (the sibling drops it — including the browser-level no-sessionId response that the id-rewrite exists to disambiguate, the documented PLAN §3.2 fix), and event scope-filtering. 15 new checks, all green alongside the existing 53. To make the multiplex unit-testable without a socket, extract the pure decision out of deliverToClient into demuxPipeToClient(_:)->String? (returns the client bytes, or nil to drop; deliverToClient is now a thin send wrapper — behavior identical) and make rewriteOutgoingId internal. Run: packages/flutter_cef_macos/test/run_filter_tests.sh Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auto-running, headless-friendly verification of the multi-tile agent-control
gate (cef-multiview PLAN Tests A/D/E), run as an alternate example entrypoint:
flutter build macos --debug -t lib/multiview_probe.dart
FLUTTER_CEF_HOST=<cef_host> FLUTTER_CEF_ALLOW_INSECURE_PROFILE=1 <built-binary>
Mounts two CefWebViews on one isolated named profile ('p2probe') with
agentControl, then with no user interaction enables agent-control on both and
drives a real CDP check over the two brokered relays, writing results to
/tmp/cef_multiview_probe.json. Validated live this session (all PASS):
A — both views create on ONE shared cef_host (pgrep == 1 host)
D — two grants with distinct relay ports + tokens
E — each relay's Target.getTargets exposes only its OWN target (A can't see
B), and tile A's token is rejected on tile B's port
Plus: graceful quit reaps the host (no orphan holding the profile lock —
shutdownAllHosts / willTerminate).
Note: the ad-hoc cef_host refuses a persistent named profile unless
FLUTTER_CEF_ALLOW_INSECURE_PROFILE=1 (mock keychain). The signed-build GPU
peer-validation gate (PLAN Phase 5 / Test H, CEF_HOST_ADHOC=OFF) is the one
piece still unverified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mments The agent-control surface was documented as "one agent-controlled tile per cef_host process" — superseded by P2-step2 (N tiles per shared host, one per-target relay each, multiplexed over the shared pipe). Update the user-facing + design surfaces to match the shipped behavior: - README "Limits, by design": replace the single-tile note with the multi-view relay-multiplex description (sessionId scoping + per-relay CDP-id rewrite). - CHANGELOG 0.2.0 (unreleased): same, plus the on-quit SIGTERM-reap of hosts. - FlutterCefPlugin.resolveOrSpawnHost doc: a named profile is MULTI-view since P2; an agent-control create() resolving to a pre-existing host is the normal 2nd+-tile path, not an anomaly. - specs/agent-control/PLAN.md: mark the deferred "multi-grant (per-tile relays + CDP id remapping)" as implemented in P2-step2. - multiview_probe.dart: use the null-aware map entry (`'params': ?params`) so `flutter analyze` is clean (no issues). No code-behavior change. Verified: flutter analyze clean, 121 dart tests pass, CdpRelay filter+multiplex tests (68 checks) pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…targetId timer Edge-harden the multi-tile agent-control paths (cef-multiview PLAN Tests F-ish), extending the live probe and fixing a latent timer bug surfaced along the way. multiview_probe.dart: - F (concurrency): enable agent-control on BOTH views CONCURRENTLY (Future.wait, not sequentially) — the P1 scalar relay/relayBrowserId would lose this race; the per-browserId dict + cdpHandlerLock must bring up two isolated relays. PASS. - F (lifecycle): disabling A frees ONLY A's relay — A's old endpoint goes dead while sibling B keeps driving its own target untouched. PASS. - Documented why PLAN Test G (reader-stall / SO_SNDTIMEO reaping) isn't faithfully reproducible from the bare probe (synthesized getTargets bypasses the shared reader; stalling precludes the handshake) — it belongs on the canvas side under a real agent-browser driver. CefProfileHost.resolveTargetId: epoch-guard the 5s timeout. An early kOpTargetId response did not cancel the timer, so a later resolve for the SAME browser (within 5s) could be clobbered and fulfilled with nil. Each resolve now carries an epoch; its timeout only fulfills if still current. Correct latent-bug fix. Discovered (NOT a P2-step2 regression; recorded as a non-fatal probe diagnostic): re-enabling agent-control on a tile AFTER disabling it hangs — the 2nd per-browser Target.getTargetInfo resolve never returns kOpTargetId. Root cause is in cef_host's targetId re-resolution (untouched by P2-step2; the epoch fix above is necessary but not sufficient), tracked for a separate cef_host fix. Verified: flutter analyze clean, CdpRelay filter+multiplex tests pass, live probe suite green (concurrency + teardown), example app compiles with the epoch fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Lands P2-step2 multi-tile agent-control: N
cefWebviewtiles sharing onecef_host(one named profile) can each be agent-controlled concurrently,each over its own token-gated CDP relay, all multiplexed over the single
browser-wide
--remote-debugging-pipe. This completes the multi-view arc(shared login across tiles + relaunch) while keeping hard per-tile isolation:
an agent holding tile A's token can neither observe nor drive tile B.
This branch is the full unmerged stack (intended to land together):
59312f46da82a74d72f83browserIdCDP-relay multiplex (cdpRelays:[UInt32:CdpRelay], fan-outonCdpMessage, per-relay CDP-id rewrite for browser-level commands) +shutdownAllHostson app quitcdae93cdemuxPipeToClientseam05357b1b377e61b8db708How isolation holds
Each relay is pinned to one
scopeTargetId. Inbound pipe traffic is scoped bysessionId(deny-by-default, flatten-only); browser-level commands (nosessionId— e.g. Playwright's connect handshake) are disambiguated by aper-relay CDP-id rewrite (
pipeId = relayId<<21 | localSeq, demuxed back to theissuing relay). A sibling's response/event is dropped,
Target.getTargetsissynthesized to only the own target, and browser-context-wide CDP stays refused.
On host quit every
cef_hostis SIGTERM-reaped (no orphan holding the profileSingletonLock).Verification
Static —
flutter analyzeclean · 121 Dart tests pass ·CdpRelayfilter+multiplex tests (68 checks) pass · native plugin compiles.
Live (
example/lib/multiview_probe.dart, ad-hoc and signedcef_host):two views on one shared host → one
cef_host; concurrentenableAgentControl→ distinct ports+tokens; each relay's
getTargetsexposes only its owntarget; tile A's token rejected on tile B's port; disable A kills only A's
grant while B keeps driving; graceful quit reaps the host.
Before merge / known items
CEF_HOST_ADHOC=OFF)cef_host, Chromium 144's enforced Mach-port peer-validation-67030s theGPU→browser handoff unless the whole tree is notarized (not reproducible
with a non-notarized local harness; Developer-ID signing alone is insufficient).
Confirm two tiles both GPU-render on a notarized Codemagic
macos-releasebuild. Low risk: the shipped release already passes this validation for the
existing single webview, and P2's 2nd browser shares the same GPU process.
cefWebviewtiles viaagent-browser) — also the right place to validatePLAN Test G (reader-stall isolation), which the bare probe can't faithfully
reproduce.
after disabling it hangs — the 2nd per-browser
Target.getTargetInforesolvenever returns. Root cause is in
cef_host's targetId re-resolution; needs afocused fix. Recorded as a non-fatal probe diagnostic.
🤖 Generated with Claude Code