From 3a2276d240cf1ced7c09b34c940bf0384c18a70c Mon Sep 17 00:00:00 2001 From: deva Date: Tue, 14 Apr 2026 08:24:47 -0700 Subject: [PATCH] feat(bridge): vendor smux tmux-bridge as layer-2 agent comms - 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- 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. --- .github/workflows/ci.yml | 38 ++ CHANGELOG.md | 9 + DEV-LOGS.md | 11 + Dockerfile | 4 + docs/tmux-bridge-agent-comms.md | 85 +++++ scripts/THIRD_PARTY_LICENSES/smux-LICENSE | 21 ++ scripts/tmux-bridge | 403 ++++++++++++++++++++++ scripts/tmux-bridge.VENDORED | 29 ++ 8 files changed, 600 insertions(+) create mode 100644 docs/tmux-bridge-agent-comms.md create mode 100644 scripts/THIRD_PARTY_LICENSES/smux-LICENSE create mode 100755 scripts/tmux-bridge create mode 100644 scripts/tmux-bridge.VENDORED diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b054a96..a2d4b1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,6 +147,44 @@ jobs: deva-smoke:ci \ bash -lc 'link="/tmp/claude-mcp-browser-bridge-$(id -un)"; test -L "$link"; test "$(readlink "$link")" = "/deva-host-chrome-bridge"' + - name: Smoke tmux-bridge CLI surface + shell: bash + run: | + set -euo pipefail + # tmux-bridge is the layer-2 agent comms CLI vendored from smux. + # We assert it's installed, executable, runs outside a tmux pane for + # the version/help/id error paths, and that tmux itself is present + # so a future operator can drive tmux-bridge against host tmux. + docker run --rm deva-smoke:ci bash -lc ' + set -euo pipefail + command -v tmux-bridge + command -v tmux + tmux-bridge version + tmux-bridge --help | grep -q "cross-pane communication" + # id requires $TMUX_PANE; outside tmux it must error cleanly. + if tmux-bridge id 2>/dev/null; then + echo "tmux-bridge id should fail outside a tmux pane" >&2 + exit 1 + fi + # Ephemeral tmux server to prove list/name/read/type work end-to-end. + sock="/tmp/tmux-smoke.sock" + tmux -S "$sock" new-session -d -s smoke "sleep 30" + export TMUX_BRIDGE_SOCKET="$sock" + tmux-bridge list | tee /tmp/list.out + grep -q "smoke:0" /tmp/list.out + target="$(tmux -S "$sock" display-message -p "#{pane_id}")" + tmux-bridge name "$target" smoke-worker + tmux-bridge resolve smoke-worker + tmux-bridge read "$target" 5 >/dev/null + # read-guard: must read before type; second type without read must fail + tmux-bridge type smoke-worker "echo hi" + if tmux-bridge type smoke-worker "echo twice" 2>/dev/null; then + echo "read-guard should have blocked second type without read" >&2 + exit 1 + fi + tmux -S "$sock" kill-server + ' + docs: name: Docs Build runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 671cdf4..6e09920 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to deva.sh will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [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 +- `tmux-bridge` installed at `/usr/local/bin/tmux-bridge` in container images; composes with existing `deva-bridge-tmux` (layer 1, kernel boundary) +- `scripts/THIRD_PARTY_LICENSES/smux-LICENSE` and `scripts/tmux-bridge.VENDORED` provenance metadata pinning upstream commit + SHA256 +- `docs/tmux-bridge-agent-comms.md` explaining the two-layer bridge composition, read-before-act guard, and socket detection order +- CI smoke test exercising the full `tmux-bridge` CLI surface (list/name/resolve/read/type/read-guard) against an ephemeral tmux server inside the built image + ## [0.10.0] - 2026-03-24 ### Added diff --git a/DEV-LOGS.md b/DEV-LOGS.md index fb64ba9..89fe832 100644 --- a/DEV-LOGS.md +++ b/DEV-LOGS.md @@ -13,6 +13,17 @@ - Minimal markdown markers, no unnecessary formatting, minimal emojis. - Reference issue numbers in the format `#` 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. +- What: + - vendored `scripts/tmux-bridge` byte-for-byte from upstream commit `95bf0b6` (SHA256 `ed66862b...`), not downloaded at build time, so image builds stay reproducible and auditable + - recorded provenance in `scripts/tmux-bridge.VENDORED` and shipped the upstream MIT license at `scripts/THIRD_PARTY_LICENSES/smux-LICENSE` + - installed `tmux-bridge` into container images at `/usr/local/bin/tmux-bridge` alongside existing `deva-bridge-tmux` (Dockerfile COPY + chmod) + - wrote `docs/tmux-bridge-agent-comms.md` covering the two-layer composition, security model, socket detection order, and the read-before-act guard (sentinel at `/tmp/tmux-bridge-read-`, cleared on any write — main safety net against an agent hallucinating into the wrong pane) + - added CI smoke step that builds an ephemeral tmux server inside the image and exercises the full CLI surface: `list`, `name`, `resolve`, `read`, `type`, plus a negative assertion that the read-guard blocks a second `type` without an intervening `read` + - did not touch `deva.sh`, auth wiring, or `docker-entrypoint.sh` — layer 2 is purely additive +- Result: agents inside deva containers can now drive each other's tmux panes with a single 403-line CLI that was not ours to write. Layer 1 (our own socat tunnel) and layer 2 (vendored smux) compose: once `deva-bridge-tmux` is running, setting `TMUX_BRIDGE_SOCKET=/tmp/host-tmux.sock` (or just attaching to tmux via `-S`) lets `tmux-bridge` target panes on the HOST tmux server from inside the container. Layer 3 (coordinator/router with text envelopes, state detection, scratchpad) remains an open design question tracked in `docs/devlog/260401-deva-v2-proposal.org`; this PR intentionally does not touch it. + # [2026-03-11] Dev Log: deva.sh docs spine for OSS release - Why: the repo had a decent landing page but still dumped too much context into one README and did not read like an organized OSS project - What: diff --git a/Dockerfile b/Dockerfile index fb92507..5a7d2a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -237,9 +237,13 @@ USER root COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh COPY scripts/deva-bridge-tmux /usr/local/bin/deva-bridge-tmux +# tmux-bridge: vendored from smux (layer-2 agent comms CLI over tmux panes) +# See scripts/tmux-bridge.VENDORED for upstream commit and SHA256 pin. +COPY scripts/tmux-bridge /usr/local/bin/tmux-bridge RUN chmod 755 /usr/local/bin/docker-entrypoint.sh && \ chmod 755 /usr/local/bin/deva-bridge-tmux && \ + chmod 755 /usr/local/bin/tmux-bridge && \ chmod -R 755 /usr/local/bin/scripts || true WORKDIR /root diff --git a/docs/tmux-bridge-agent-comms.md b/docs/tmux-bridge-agent-comms.md new file mode 100644 index 0000000..9064e4c --- /dev/null +++ b/docs/tmux-bridge-agent-comms.md @@ -0,0 +1,85 @@ +# tmux-bridge: agent-to-agent comms in deva containers + +deva ships two tmux bridge layers. They compose. + + Layer 1 deva-bridge-tmux kernel boundary + (scripts/deva-bridge-tmux) container tmux client -> host tmux server + socat TCP tunnel via host.docker.internal:41555 + + Layer 2 tmux-bridge semantic CLI + (scripts/tmux-bridge) read/type/keys/label/envelope + vendored from smux for agents to drive each other's panes + +Layer 1 is the plumbing that lets the container see host tmux at all. Layer 2 +is what agents actually call. + +## Security + +Both layers are privileged host bridges. If you run them, the container can +execute arbitrary commands on the host tmux server (send-keys, run-shell, +scrollback). This is deliberate for trusted dev workflows. Do not enable on +untrusted code. + +## Quick start + +Host (macOS): + + deva-bridge-tmux-host # expose host tmux over TCP:41555 + +Container (inside a deva agent): + + deva-bridge-tmux # start socat; creates /tmp/host-tmux.sock + tmux -S /tmp/host-tmux.sock attach # optional: attach to host session + +From another pane (or the same container, any agent CLI that can shell out): + + tmux-bridge list # see all panes + tmux-bridge name %1 planner # label a pane + tmux-bridge read planner 50 # read last 50 lines + tmux-bridge message planner "found 3 issues in auth.py" + tmux-bridge type planner "rerun tests" + tmux-bridge keys planner Enter + +## Socket detection + +`tmux-bridge` auto-detects the tmux server socket in this order: + +1. `$TMUX_BRIDGE_SOCKET` env var (explicit override) +2. `$TMUX` (set automatically when you are inside a tmux pane) +3. Scan `/tmp/tmux-/*` for a server that owns `$TMUX_PANE` +4. Default tmux server + +For deva containers talking to host tmux via the layer-1 bridge, attach to +tmux first (step 2 fires) or set the override: + + export TMUX_BRIDGE_SOCKET=/tmp/host-tmux.sock + +## Read-before-act guard + +`tmux-bridge` enforces that agents `read` a pane before they can `type`, +`message`, or `keys` into it. This is the main safety net against "agent +blindly hallucinates into the wrong pane." + +The guard is a sentinel at `/tmp/tmux-bridge-read-`. Reading sets +it; any write clears it. So the contract is: + +1. `tmux-bridge read ` — look at the pane's current state +2. `tmux-bridge type "..."` — act on what you saw +3. To act again, read again. + +## Diagnostics + + tmux-bridge doctor + +Prints env vars, detected socket, visible panes, and a pass/fail summary. +Run this first when things go wrong. + +## Provenance + +`scripts/tmux-bridge` is vendored byte-for-byte from upstream smux +(github.com/ShawnPana/smux). See `scripts/tmux-bridge.VENDORED` for the +pinned commit and SHA256. License is MIT, reproduced in +`scripts/THIRD_PARTY_LICENSES/smux-LICENSE`. + +`scripts/deva-bridge-tmux` and `scripts/deva-bridge-tmux-host` are deva's +own work (see `docs/devlog/20260108-deva-bridge-tmux.org`). diff --git a/scripts/THIRD_PARTY_LICENSES/smux-LICENSE b/scripts/THIRD_PARTY_LICENSES/smux-LICENSE new file mode 100644 index 0000000..4eb6ede --- /dev/null +++ b/scripts/THIRD_PARTY_LICENSES/smux-LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 shawn pana + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/tmux-bridge b/scripts/tmux-bridge new file mode 100755 index 0000000..ba3ae76 --- /dev/null +++ b/scripts/tmux-bridge @@ -0,0 +1,403 @@ +#!/usr/bin/env bash +# tmux-bridge — Agent-agnostic CLI for cross-pane communication in tmux. +# Any tool that can run bash can use this to talk to other panes. +set -euo pipefail + +VERSION="2.0.0" + +# --- Helpers --- + +die() { echo "error: $*" >&2; exit 1; } + +# --- Read Guard --- +# Enforces read-before-act: agents must read a pane before typing/keys. + +read_guard_path() { + local pane_id="$1" + # Sanitize: %66 → _66 + echo "/tmp/tmux-bridge-read-${pane_id//%/_}" +} + +mark_read() { + touch "$(read_guard_path "$1")" +} + +require_read() { + local guard + guard=$(read_guard_path "$1") + if [[ ! -f "$guard" ]]; then + die "must read the pane before interacting. Run: tmux-bridge read $1" + fi +} + +clear_read() { + rm -f "$(read_guard_path "$1")" +} + +# Detect the correct tmux server socket. +# Priority: TMUX_BRIDGE_SOCKET env > $TMUX (if socket alive) > scan for pane owner. +detect_socket() { + # Explicit override + if [[ -n "${TMUX_BRIDGE_SOCKET:-}" ]]; then + if [[ -S "$TMUX_BRIDGE_SOCKET" ]]; then + echo "$TMUX_BRIDGE_SOCKET" + return + fi + die "TMUX_BRIDGE_SOCKET=$TMUX_BRIDGE_SOCKET is not a valid socket" + fi + + # Extract from $TMUX (format: "socket_path,pid,session_index") + if [[ -n "${TMUX:-}" ]]; then + local socket="${TMUX%%,*}" + if [[ -S "$socket" ]]; then + # Verify the socket is actually reachable + if tmux -S "$socket" list-sessions &>/dev/null; then + echo "$socket" + return + fi + fi + fi + + # Fallback: scan tmux server sockets for one that owns $TMUX_PANE + local pane="${TMUX_PANE:-}" + if [[ -n "$pane" ]]; then + local uid + uid=$(id -u) + # Check both /tmp and /private/tmp (macOS symlinks /tmp → /private/tmp, + # but some sandboxed environments only see one or the other) + local sock_dirs=("/tmp/tmux-${uid}" "/private/tmp/tmux-${uid}") + for sock_dir in "${sock_dirs[@]}"; do + [[ -d "$sock_dir" ]] || continue + for sock in "$sock_dir"/*; do + [[ -S "$sock" ]] || continue + if tmux -S "$sock" display-message -t "$pane" -p '#{pane_id}' &>/dev/null; then + echo "$sock" + return + fi + done + done + # Also try the default socket (no -S flag) + if tmux display-message -t "$pane" -p '#{pane_id}' &>/dev/null; then + echo "__default__" + return + fi + fi + + # Last resort: try default tmux server + if tmux list-sessions &>/dev/null; then + echo "__default__" + return + fi + + die "cannot find a reachable tmux server (TMUX=${TMUX:-}, TMUX_PANE=${TMUX_PANE:-})" +} + +# Resolved once at startup, used by tmx() for all tmux calls. +TMUX_SOCKET="" + +init_socket() { + TMUX_SOCKET=$(detect_socket) +} + +# Wrapper: run tmux with the detected socket. +tmx() { + if [[ "$TMUX_SOCKET" == "__default__" || -z "$TMUX_SOCKET" ]]; then + tmux "$@" + else + tmux -S "$TMUX_SOCKET" "$@" + fi +} + +usage() { + cat <<'EOF' +tmux-bridge — cross-pane communication for AI agents + +Usage: tmux-bridge [args...] + +Commands: + list Show all panes (target, pid, command, size, label) + type Type text without pressing Enter + message Type text with auto-prepended sender info and reply target + read [lines] Read last N lines from pane (default: 50) + keys ... Send special keys (Enter, Escape, C-c, etc.) + name