get2knowio / get2knowio/deacon
feat: Add native reverse port and configuration plumbing for Chrome DevTools MCP browser automation
- Dominant language
- Rust
- Stars
- 2
- Forks
- 0
- Avg merge
- 2h 36m
- Merged PRs (30d)
- 78
Description
## 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 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**, get2knowio/deacon#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:,fork,reuseaddr TCP:host.docker.internal:`) so the agent can still dial `127.0.0.1:` 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.rs` — `FeatureMetadata`, `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 #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 #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 #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 `), 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`.
Contributor guide
Research direction
Start with the design spike shared with issue #281, then read crates/core/src/port_forward/{daemon.rs,relay.rs,registry.rs} and crates/deacon/src/cli.rs:245 to understand existing port and command lifecycles. Review up/compose.rs:748, build/mod.rs:1610, container_env_probe.rs:593-648, and SECURITY.md:104-131 for feature and environment conventions. Done means a documented, opt-in Phase 1 design with security boundaries, lifecycle cleanup, environment discovery, and test locations agreed before implementation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- docker, rust
- Domain
- devops, infrastructure, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 28/100