feat(bridge): vendor smux tmux-bridge as layer-2 agent comms#256
Conversation
- scripts/tmux-bridge: byte-identical copy from smux commit 95bf0b6 (github.com/ShawnPana/smux, MIT). 403-line bash CLI for cross-pane comms: list, read, type, keys, message, name, resolve, doctor. - Read-before-act guard via /tmp/tmux-bridge-read-<pane_id> sentinel prevents agents from blindly writing to panes they have not inspected. - Installed at /usr/local/bin/tmux-bridge in container images, sits alongside existing deva-bridge-tmux (layer 1 socat TCP tunnel). - Provenance pinned in scripts/tmux-bridge.VENDORED (upstream commit + SHA256 ed66862b...); upstream MIT license shipped in scripts/THIRD_PARTY_LICENSES/smux-LICENSE. - docs/tmux-bridge-agent-comms.md explains two-layer composition, security model, socket detection order, and read-guard semantics. - CI smoke step builds an ephemeral tmux server inside the image and exercises the full CLI surface (list/name/resolve/read/type) plus a negative assertion on the read-guard. - Zero touches to deva.sh, auth, or docker-entrypoint.sh; purely additive layer-2 adoption.
|
Claude finished @lroolle's task in 2m 35s —— View job Security Audit of tmux-bridge IntegrationPerforming paranoid security audit focusing on container boundaries, auth flows, shell entrypoints, privilege escalation, and permission bypasses. Todo List
Critical Security Findings🚨 BLOCKING ISSUES
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a2276d240
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| read_guard_path() { | ||
| local pane_id="$1" | ||
| # Sanitize: %66 → _66 | ||
| echo "/tmp/tmux-bridge-read-${pane_id//%/_}" | ||
| } |
There was a problem hiding this comment.
Scope read guard by tmux socket
The read-before-act sentinel is keyed only by pane_id (/tmp/tmux-bridge-read-...), but pane IDs are reused across different tmux servers. Because detect_socket chooses the server per invocation, a read against server A can leave a guard file that lets type/message/keys proceed against server B with the same %N without any read on B. In environments where both local and bridged host tmux servers are reachable, this defeats the safety guarantee and can send commands to an unseen pane.
Useful? React with 👍 / 👎.
| result=$(tmx list-panes -a -F '#{pane_id} #{@name}' 2>/dev/null \ | ||
| | awk -v lbl="$label" '$2 == lbl { print $1; exit }') |
There was a problem hiding this comment.
Parse labels without whitespace splitting
resolve_label formats output as "#{pane_id} #{@name}" and matches with awk '$2 == lbl', so labels containing spaces are split and never match the full label string. Since name currently accepts arbitrary label text, users can create labels (for example, "worker one") that cannot be resolved by resolve, read, type, or message until renamed.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
Vendors upstream smux’s tmux-bridge bash CLI into the deva container image as a “layer-2” agent-to-agent communication tool over tmux panes, designed to compose with the existing “layer-1” deva-bridge-tmux host tunnel.
Changes:
- Add vendored
scripts/tmux-bridgeplus provenance + third-party license files. - Install
tmux-bridgeinto the container image and document the two-layer bridge composition and safety model. - Extend CI with a smoke test that exercises the
tmux-bridgeCLI against an ephemeral tmux server inside the built image.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/tmux-bridge | Vendored layer-2 tmux pane communication CLI with socket detection + read-before-act guard. |
| scripts/tmux-bridge.VENDORED | Vendoring provenance (upstream commit + SHA256) and refresh instructions. |
| scripts/THIRD_PARTY_LICENSES/smux-LICENSE | MIT license text for the vendored upstream script. |
| docs/tmux-bridge-agent-comms.md | Explains layer-1/layer-2 composition, security model, socket detection, and guard semantics. |
| Dockerfile | Copies tmux-bridge into /usr/local/bin and marks it executable. |
| .github/workflows/ci.yml | Adds a smoke step validating installed CLI surface + guard behavior. |
| CHANGELOG.md | Records the addition under [Unreleased]. |
| DEV-LOGS.md | Adds a devlog entry describing motivation, contents, and outcome. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| What it is: | ||
| - tmux-bridge is a 403-line bash CLI for cross-pane communication between | ||
| AI agents running in tmux. Read/type/keys/label/envelope primitives. |
| read_guard_path() { | ||
| local pane_id="$1" | ||
| # Sanitize: %66 → _66 | ||
| echo "/tmp/tmux-bridge-read-${pane_id//%/_}" |
| socat TCP tunnel via host.docker.internal:41555 | ||
|
|
||
| Layer 2 tmux-bridge semantic CLI | ||
| (scripts/tmux-bridge) read/type/keys/label/envelope |
| ## [Unreleased] | ||
|
|
||
| ### Added | ||
| - `scripts/tmux-bridge` vendored from upstream smux (commit 95bf0b6, MIT) for layer-2 agent-to-agent communication over tmux panes: read/type/keys/label/envelope/doctor |
| - Reference issue numbers in the format `#<issue-number>` for easy linking. | ||
|
|
||
| # [2026-04-13] Dev Log: adopt smux tmux-bridge as layer-2 agent comms | ||
| - Why: deva-v2 proposal left the agent-to-agent transport CLI as an open question; smux (github.com/ShawnPana/smux) shipped exactly the shape we sketched (read/type/keys/label/envelope) in 403 lines of bash, MIT-licensed. Reinventing it would be waste. The existing `deva-bridge-tmux` (socat TCP) is layer 1 (kernel boundary); `tmux-bridge` is layer 2 (semantic). They compose cleanly. |
Mount dispatch walked every CONFIG_ROOT subdir and emitted every loose
child as a bind mount. With ~/.config/deva/sessions/ holding 200+ files
this produced 200+ -v flags, and validate_bind_mount_shape's O(N^2) loop
with python3 forks per call turned a dry-run into a 2m42s stall.
- Narrow dispatch to known agent subdirs only (agents/<name>.sh gate)
- Mount only canonical entries per agent (.claude+.claude.json, .codex,
.gemini) instead of blind glob walk
- Delete dead should_mount_home_item / mount_loose_home_item /
mount_dir_contents_into_home — allowlist replaces denylist
- Default to hybrid mounts: all populated agent subdirs mount into every
container; --config-home DIR still isolates to a single home
- Replace python3 path helpers with pure bash (_normalize_path,
absolute_path, canonical_path, path_is_strict_descendant,
relative_subpath) — parity-tested against python across 30+ inputs
- Drop node/python probes from get_host_tmpdir; ${TMPDIR:-/tmp} suffices
- Add progressive --debug breadcrumbs (_step) at 9 phase boundaries
- Register claude-trace in TOOL_REGISTRY so version-upgrade can see it
- Extend test-mount-shape.sh: hybrid-default, --config-home isolation,
CLI -v override, zero-match count_target fix
Result: 228-file CONFIG_ROOT -> 4 mounts, 42ms wall-clock.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
claude-trace's npm fetch garbles the version-report display. version-upgrade.sh pins it via $CLAUDE_TRACE_VERSION directly (line 88), so adding it to the registry only broke reporting without enabling auto-upgrade. Keep it pinned-only like playwright. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
version-upgrade.sh pinned claude_trace_ver and playwright_ver to env vars instead of calling get_latest(). Also load_versions case statement missed claude-trace, so env_var kept the previous iteration's value (COPILOT_API_VERSION) — garbling the display with the copilot hash. - Add claude-trace to TOOL_REGISTRY with correct npm source - Add claude-trace case to load_versions env-var mapping - Add catch-all *) env_var="" to prevent future stale-variable bugs - Wire get_latest for claude-trace and playwright in version-upgrade.sh Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
load_versions used env vars as "latest", but versions.env always populates them via version-pins.sh — so the check compared pinned values against themselves and reported "up-to-date" unconditionally, even when npm had newer releases. - Remove env-var shortcut from load_versions; always fetch from upstream (npm/github) for the reporting path - Snapshot CLI overrides in version-upgrade.sh BEFORE version-pins.sh fills defaults, so explicit `CLAUDE_CODE_VERSION=X make versions-up` still forces that version at build time - Plain `make versions-up` now correctly detects and upgrades to the real latest upstream versions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rename display sections: "Pinned Agent Tools" -> "Agent Tools (auto-upgraded by make versions-up)", toolchains and playwright sections note "pinned — edit versions.env to bump" - Playwright stays pinned in version-upgrade.sh (uses $PLAYWRIGHT_VERSION not get_latest) — browser version bumps break test stability - Playwright still in TOOL_REGISTRY for `make versions` comparison, so you can see when a newer version exists without auto-upgrading Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove `npm install -g npm@latest` from copilot-api install step; npm replacing itself mid-execution corrupts its own node_modules when the cached layer has stale state - Remove the 5-attempt retry loop that only masked the real failure - Node 22 ships npm 10.x which handles `npm install -g pnpm` fine - Mount ~/.agents into container for agent state persistence Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add deva.sh codex --browser-mcp: injects session-only Playwright MCP override via Codex --config, switches to rust image profile - Add CODEX_BROWSER_MCP=true and repeatable CODEX_CONFIG=key=value in .deva config files for persistent browser sessions - Bump Go 1.26.3, Claude Code 2.1.143, Codex 0.131.0, Gemini CLI 0.42.0, Playwright 1.60.0 - Redesign container slug: deva--<agent>--<auth_tag>--<slug>..<hash> replaces the old ..i/..v/..c suffix soup; agent always in name - Normalize short_hash to SHA-256; drop cksum fallback (non-hex) - Add scripts/test-codex-browser-shape.sh dry-run coverage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
USER_ENVS=("${retained[@]}") crashes under set -u when retained
is empty (all existing entries filtered out). Use explicit length
check to assign empty array safely.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ion-pins fix(deva): tighten mount dispatch and drop python path helpers
|
Claude finished @lroolle's task in 2m 27s —— View job Security Audit of tmux-bridge IntegrationPerforming paranoid security audit focusing on container boundaries, auth flows, shell entrypoints, privilege escalation, and permission bypasses. Todo List
Critical Security Findings🚨 BLOCKING ISSUES
|
|
Claude finished @lroolle's task in 1m 38s —— View job Security Audit of tmux-bridge IntegrationPerforming paranoid security audit focusing on container boundaries, auth flows, shell entrypoints, privilege escalation, and permission bypasses. Todo List
Critical Security Findings🚨 BLOCKING ISSUES
|
| VERSION="0.10.0" | ||
| DEVA_DOCKER_IMAGE="${DEVA_DOCKER_IMAGE:-ghcr.io/thevibeworks/deva}" | ||
| DEVA_DOCKER_TAG="${DEVA_DOCKER_TAG:-latest}" | ||
| DEVA_CONTAINER_PREFIX="${DEVA_CONTAINER_PREFIX:-deva}" | ||
| DEFAULT_AGENT="${DEVA_DEFAULT_AGENT:-claude}" | ||
| DEVA_CODEX_BROWSER_MCP_PACKAGE="${DEVA_CODEX_BROWSER_MCP_PACKAGE:-${DEVA_PLAYWRIGHT_MCP_PACKAGE:-@playwright/mcp@0.0.75}}" | ||
| DEVA_PLAYWRIGHT_MCP_PACKAGE="${DEVA_PLAYWRIGHT_MCP_PACKAGE:-$DEVA_CODEX_BROWSER_MCP_PACKAGE}" | ||
|
|
| dir="$(dirname -- "$p")" | ||
| base="$(basename -- "$p")" |
| short_hash() { | ||
| local input="$1" | ||
| local length="${2:-8}" | ||
|
|
||
| if command -v md5sum >/dev/null 2>&1; then | ||
| printf '%s' "$input" | md5sum | cut -c1-"$length" | ||
| if command -v sha256sum >/dev/null 2>&1; then | ||
| printf '%s' "$input" | sha256sum | cut -c1-"$length" | ||
| elif command -v shasum >/dev/null 2>&1; then | ||
| printf '%s' "$input" | shasum | cut -c1-"$length" | ||
| printf '%s' "$input" | shasum -a 256 | cut -c1-"$length" | ||
| else | ||
| printf '%s' "$input" | cksum | cut -d' ' -f1 | cut -c1-"$length" | ||
| printf '%s' "$input" | md5sum | cut -c1-"$length" | ||
| fi |
| prepare_base_docker_args | ||
| dedup_user_volumes |
| done < <( | ||
| bash -lc ' | ||
| set -euo pipefail | ||
| set -a | ||
| # shellcheck disable=SC1090 | ||
| source "$1" | ||
| set +a | ||
| shift | ||
| for var in "$@"; do | ||
| printf "%s=%s\n" "$var" "${!var}" | ||
| done | ||
| ' bash "$VERSION_PINS_FILE" "${VERSION_PIN_VARS[@]}" | ||
| ) |
|
Claude finished @lroolle's task in 2m 1s —— View job Security Audit of tmux-bridge IntegrationPerforming paranoid security audit focusing on container boundaries, auth flows, shell entrypoints, privilege escalation, and permission bypasses. Todo List
Critical Security Findings🚨 BLOCKING ISSUES
|
What
Vendor upstream
smuxtmux-bridge(github.com/ShawnPana/smux, MIT) as thelayer-2 agent-to-agent communication CLI inside deva container images. Sits
alongside the existing
deva-bridge-tmux(layer 1, socat TCP tunnel to hosttmux server). Zero touches to
deva.sh, auth wiring, ordocker-entrypoint.sh.Why
The deva-v2 proposal (docs/devlog/260401-deva-v2-proposal.org) left the
agent-to-agent transport CLI as an open design question. Upstream smux shipped
almost exactly the primitives we sketched (list/read/type/keys/label + text
envelopes) in 403 lines of bash, MIT-licensed. Reinventing it would be waste.
Adopting it unblocks experiments on top (layer 3: coordinator/router,
scratchpad, state detection) without us having to own the transport layer.
The piece smux does not cover - the container-to-host kernel boundary - is
already solved by our existing
deva-bridge-tmux. The two layers composecleanly: once the socat tunnel is up,
TMUX_BRIDGE_SOCKET=/tmp/host-tmux.socklets
tmux-bridgedrive panes on the host tmux server from inside thecontainer.
What is in the commit
95bf0b639e64a4c67b4f007b1bedc26395344e01, SHA256 ed66862b...)
security model, socket detection priority, read-before-act guard semantics,
quick start, provenance
the existing smoke job. Builds an ephemeral tmux server inside the image,
exercises list/name/resolve/read/type, and asserts the read-guard blocks
a second type without re-reading.
Test plan
Verified locally against real tmux 3.6a (same version container ships):
shellcheck -S error scripts/tmux-bridge scripts/deva-bridge-tmux scripts/deva-bridge-tmux-host-> clean (matches CI severity)tmux-bridge version->tmux-bridge 2.0.0tmux-bridge --help-> contains "cross-pane communication"tmux-bridge idoutside tmux -> errors cleanlytmux-bridge listagainst ephemeral tmux server -> showssmoke:0panetmux-bridge name <pane> smoke-worker+resolve smoke-worker-> round-trip workstmux-bridge read <target> 5-> succeeds, sets guardtmux-bridge type smoke-worker "echo hi"after read -> succeedstmux-bridge type smoke-worker "echo twice"without re-read -> fails with "must read the pane before interacting" (guard works)Security
Both layers are privileged host bridges. If the host tmux bridge is running
and an agent in the container drives
tmux-bridge, it cansend-keys,run-shell, and read scrollback on the host tmux server - effectively asandbox escape, which is the deliberate tradeoff for trusted dev workflows.
Documented loudly in
docs/tmux-bridge-agent-comms.mdand unchanged from theexisting
deva-bridge-tmuxtrust model.Non-goals
scratchpad) is tracked as an open design in
docs/devlog/260401-deva-v2-proposal.organd explicitly out of scope here.This PR is additive transport only.