Skip to content

feat: Add native reverse port and configuration plumbing for Chrome DevTools MCP browser automation #282

Description

@pofallon

Problem

An agent (e.g. Claude Code) running inside a deacon-managed devcontainer cannot drive a Chrome instance running on the developer's host desktop — there's no network path from the container to a host process, and no standard way to tell an in-container MCP client where to find it. This blocks browser-automation workflows (visual debugging, screenshot loops, page-navigation QA) that assume the agent can reach chrome-devtools-mcp pointed at the host browser's --remote-debugging-port.

Note on the original ask: the request specifies deacon shell --browser. That subcommand doesn't exist — crates/deacon/src/cli.rs:245's Commands enum lists only Up, Build, Exec, ReadConfiguration, Config, Templates, Upgrade, SetUp, RunUserCommands, Down, Doctor, Outdated, ForwardDaemon. This issue reframes the flag as living on deacon up (and/or deacon exec), consistent with the real subcommand surface.

Two problems bundled together — recommend splitting scope

The request bundles (1) a network-reachability problem and (2) an app-specific config-injection problem. These have very different risk profiles and should likely be split:

1. Container→host reachability (new plumbing, needs its own design)

This is the reverse direction of everything deacon currently does for port handling. The existing 015-auto-forward-ports daemon (crates/core/src/port_forward/{daemon.rs,relay.rs,registry.rs}) solves host→container reachability: the host binds a loopback listener and, per connection, runs docker exec -i <id> nc/socat ... to dial into the container's netns (relay.rs:1-6,56, the relay program dials {dial_host}:{container_port} from inside the container, driven by a host-side listener). There is currently zero code path for the opposite direction — confirmed via grep: no hits for host.docker.internal, host-gateway, add-host, or extra_hosts/ExtraHosts anywhere in docker.rs, compose.rs, container.rs, or up/*. Making a container process reach a host port is genuinely new work, not an extension of the existing daemon's direction.

This same container→host reachability gap is also the crux of a related open issue (host clipboard bridge, #281) — worth designing once and sharing the underlying mechanism (e.g. a generic "reverse tunnel" primitive) rather than building two bespoke, direction-specific solutions. Recommend cross-referencing/consolidating design work between the two.

Candidate approaches (need a design spike, not assumed as decided):

  • --add-host=host.docker.internal:host-gateway at container-create time (Linux; Docker Desktop macOS/Windows already auto-injects this), then have deacon write/inject a small in-container proxy (e.g. socat TCP-LISTEN:<local_port>,fork,reuseaddr TCP:host.docker.internal:<host_cdp_port>) so the agent can still dial 127.0.0.1:<PORT> as the request expects. Podman's rootless netns gateway needs separate handling.
  • Extend relay.rs for container-initiated dial-out, mirroring the existing daemon/registry/marker lifecycle but reversing the dial direction — reuses proven daemon plumbing (daemonize(), SIGTERM-on-teardown, user_data_folder-scoped marker files) at the cost of nontrivial changes to relay.rs's current host→container assumption.

2. MCP config injection — likely out of deacon's scope, should be a Feature instead

Writing ~/.config/Claude/mcp.json with a chrome-devtools block hardcodes assumptions about one specific AI tool's config format into deacon core. Grep confirms zero existing references to claude, Claude, mcp.json, or MCP anywhere in the codebase — deacon has no precedent of special-casing any single IDE/agent/tool (consistent with the Consumer-Only Scope constitution: deacon implements the containers.dev spec surface, not app-specific tooling).

The sanctioned extension point for "install/configure app X inside the container" already exists: the Feature mechanism (crates/core/src/features.rsFeatureMetadata, InstallationPlan, FeatureMerger; invoked via install_features_for_compose in up/compose.rs:748 and apply_features_and_lockfile in build/mod.rs:1610). Writing an MCP config for chrome-devtools-mcp is exactly the shape of thing a Feature's install.sh should do — a workspace opts into a chrome-devtools-mcp Feature in devcontainer.json, and that Feature's install script writes whatever config file the agent it targets expects.

What deacon should own, if anything, is exposing the forwarded host port/address to the container in a discoverable way (e.g. via remoteEnv, following the existing containerEnv/remoteEnv PATH-append precedent in container_env_probe.rs:593-648) — e.g. a DEACON_HOST_CDP_PORT or similar env var — so a Feature or postCreateCommand can consume it without deacon needing to know anything about MCP, Claude, or any other specific agent.

Security — this needs prominent, explicit treatment

The Chrome DevTools Protocol has no built-in authentication. Anything with network access to a CDP endpoint can execute arbitrary JavaScript in any open tab, read cookies/session storage for any site the user is logged into, and navigate to file:// URLs to read local files — this is why Chrome's own docs and multiple public security advisories warn against binding --remote-debugging-port beyond loopback. Making that endpoint reachable from inside a devcontainer (which may run code from an untrusted repo, a compromised Feature, or a prompt-injected agent) is a materially larger exposure than the existing port-forward or clipboard-bridge proposals, both of which are loopback-only and narrowly scoped.

This should follow the same trust-boundary precedent already established for --auto-forward's browser auto-open (SECURITY.md:104-131): forwarded-port binds are loopback-only, and the machine owner controls the sensitive knob (DEACON_BROWSER env var / settings.json, never workspace-sourced) even though the workspace can request the behavior (onAutoForward). Applied here:

  • The flag enabling this bridge must be explicit, opt-in, and off by default — never auto-enabled by a workspace's devcontainer.json.
  • Loud documentation (in --help and SECURITY.md) of exactly what the flag exposes: any process in the container gets full remote-control of the host browser session.
  • No silent default port (the request suggests 9222/4222 ambiguously) — require the user to specify which host CDP port to bridge, and fail loudly if unreachable rather than silently no-oping (Principle IV).
  • Consider whether deacon should refuse to proceed unless the host Chrome was launched with --remote-debugging-address=127.0.0.1 (never 0.0.0.0) and, ideally, --remote-allow-origins scoped — though deacon doesn't control how the user launches Chrome, so this may only be enforceable via a warning, not a hard check.

Open questions

  1. Is deacon launching/managing the host Chrome process in scope at all, or does the user launch it themselves (google-chrome --remote-debugging-port=9222) and deacon only bridges to an already-running instance? (Recommend: deacon does not manage host GUI apps — bridge-only.)
  2. What's the actual reachability mechanism (see options above) — needs a design spike before implementation, ideally shared with feat: Implement seamless IDE-independent host clipboard bridge via dynamic shims #281's clipboard-bridge reachability work.
  3. Should the forwarded port be discoverable via remoteEnv, a generated file under .devcontainer/, or both?
  4. Does this need Podman-specific handling for the rootless netns gateway, matching the same open question flagged in feat: Implement seamless IDE-independent host clipboard bridge via dynamic shims #281?
  5. Confirm the actual Claude Code MCP config path/schema before any Feature is written — ~/.config/Claude/mcp.json in the original request has not been verified against current Claude Code conventions and may be stale/incorrect.

Suggested implementation phases

  • Phase 0 — Design spike: settle the container→host reachability mechanism (shared with feat: Implement seamless IDE-independent host clipboard bridge via dynamic shims #281), confirm it works across Docker Desktop (macOS/Windows), Docker on Linux, and flag Podman as a follow-up if not solvable in the same pass.
  • Phase 1 — Reachability only: opt-in flag on deacon up/exec (e.g. --host-cdp-port <PORT>), loopback-only bridge, port/address exposed to the container via remoteEnv, no MCP/Claude-specific code in deacon core. Security documentation in SECURITY.md from day one.
  • Phase 2 (separate effort, likely a community Feature, not deacon core): a chrome-devtools-mcp Dev Container Feature that consumes the exposed env var and writes the appropriate MCP config for whichever agent's format is targeted.

Acceptance criteria (Phase 1 only — deacon core)

  • Bridge is fully opt-in; deacon up/exec with no flag is byte-identical to today's behavior.
  • Host-side bind (if any new listener is introduced) is loopback-only, matching the port_forward daemon's existing posture.
  • The forwarded port/host address is discoverable inside the container via an env var (name TBD), with no deacon-side knowledge of MCP, Claude, or chrome-devtools-mcp baked in.
  • SECURITY.md documents the exposure explicitly before merge, not as a follow-up.
  • down/container teardown leaves no orphaned host-side process or listener, matching the port_forward daemon's cleanup guarantee.
  • cargo clippy --all-targets --all-features -- -D warnings and cargo fmt --all -- --check clean; new integration tests correctly grouped in .config/nextest.toml.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementFeature enhancementfeatNew feature

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions