Skip to content

feat: opt-in host-tmux (container->host tmux), sandbox-safe by default#406

Closed
lroolle wants to merge 3 commits into
mainfrom
feat/host-tmux
Closed

feat: opt-in host-tmux (container->host tmux), sandbox-safe by default#406
lroolle wants to merge 3 commits into
mainfrom
feat/host-tmux

Conversation

@lroolle

@lroolle lroolle commented Jul 10, 2026

Copy link
Copy Markdown
Member

Closes #405

The shape

deva's model: the container IS the sandbox. So this splits by direction and
keeps host reach opt-in.

Direction           tool             sandbox
---------           ----             -------
host -> container   deva.sh shell    intact — default, recommended
                    (docker exec)    host reaches in, container gains nothing
agent <-> agent     tmux-bridge      intact — inside one container
container -> host   host-tmux        BROKEN — opt-in only (--host-tmux)

What

  • scripts/host-tmux — ssh to host tmux: attach/ls/tmux (host's own tmux
    client, zero protocol coupling), bridge (ssh -L forwards the host
    socket to /tmp/host-tmux.sock, 0600, keeps tmux-bridge Layer 2), setup
    (installs your pubkey once via docker host mount; probes the real home
    read-only first; prints undo), doctor (target -> dns -> port 22 -> auth
    -> tmux).
  • NOT baked into the image — it lets the container run commands on the host.
    deva.sh --host-tmux provisions it (script + host ~/.ssh ro) per run,
    dedup-safe against a user .deva that already mounts ~/.ssh.
  • delete the socat pair deva-bridge-tmux{,-host}.
  • docs reframed around the sandbox; --no-docker hardened config stays
    sealed unless --host-tmux is passed.

Why not the socat bridge

Unauthenticated TCP 41555 gave every container and local process run-shell
on the host; the host daemon died on reboot; native attach coupled
client/server tmux protocol versions. ssh is authenticated, reboot-durable
(launchd), and version-decoupled.

Test plan

  • scripts/host-tmux live against OrbStack/macOS: doctor, setup idempotence,
    spaced-arg quoting, tty guard, bridge lifecycle, tmux-bridge Layer 2 over
    the forwarded socket.
  • deva.sh --host-tmux: scripts/test-mount-shape.sh (all pass) covers off
    / on-without-ssh / on-with-user-ssh-dedup; dry-run confirms the docker
    argv.
  • Not verified here: a real --host-tmux container run end-to-end (needs an
    image build/run outside the dev container). Dry-run + mount-shape suite
    cover the argv assembly only.

🤖 Generated with Claude Code

- host-tmux: attach/ls/tmux run the host tmux client over ssh;
  bridge forwards the host socket to /tmp/host-tmux.sock (ssh -L,
  0600) keeping tmux-bridge layer 2 unchanged; setup installs the
  user's own pubkey once via docker host mount and prints the undo;
  doctor walks target -> dns -> port 22 -> auth -> tmux
- drop deva-bridge-tmux{,-host}: unauthenticated TCP 41555 gave any
  container or local process run-shell on the host, the host daemon
  died on every reboot, and native attach coupled client/server
  tmux protocol versions
- sshd is launchd-managed: container -> host tmux now survives host
  reboots with no host-side ritual

Closes #405

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 03:09
@claude-yolo

claude-yolo Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Claude finished @lroolle's task in 1s —— View job


I'll analyze this and get back to you.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces deva’s Layer-1 tmux transport from an unauthenticated socat TCP bridge to an SSH-based host transport (host-tmux), while keeping the Layer-2 tmux-bridge workflow and /tmp/host-tmux.sock socket path intact.

Changes:

  • Added scripts/host-tmux providing setup, attach/ls/tmux, bridge, and doctor commands over SSH.
  • Removed the socat-based Layer-1 scripts scripts/deva-bridge-tmux and scripts/deva-bridge-tmux-host.
  • Updated docs + images to install and document host-tmux as the default Layer-1 transport.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
scripts/host-tmux New SSH-based Layer-1 transport and optional socket-forwarding bridge.
scripts/deva-bridge-tmux-host Removes the host-side socat TCP listener.
scripts/deva-bridge-tmux Removes the container-side socat unix-socket proxy.
docs/tmux-bridge-agent-comms.md Rewrites Layer-1 documentation to host-tmux + SSH forwarding.
Dockerfile Installs host-tmux into the main image instead of deva-bridge-tmux.
Dockerfile.rust Installs host-tmux into the rust image instead of deva-bridge-tmux.
DEV-LOGS.md Adds dev-log entry describing the rationale and verification notes.
CHANGELOG.md Documents the addition of host-tmux and removal of the socat bridge.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/host-tmux
Comment on lines +155 to +161
echo "installing pubkey into $home/.ssh/authorized_keys via docker host mount..."
"${dockercmd[@]}" run --rm -e PUB="$pub" -e MARKER="$KEY_MARKER" -v "$home/.ssh:/hostssh" alpine sh -c '
touch /hostssh/authorized_keys
grep -qxF "$PUB $MARKER" /hostssh/authorized_keys || grep -qxF "$PUB" /hostssh/authorized_keys \
|| printf "%s %s\n" "$PUB" "$MARKER" >> /hostssh/authorized_keys
chmod 600 /hostssh/authorized_keys' \
|| die "docker key install failed; $manual"
Comment thread scripts/host-tmux
Comment on lines +199 to +207
stop)
if [[ -f "$BRIDGE_PID" ]]; then
kill "$(cat "$BRIDGE_PID")" 2>/dev/null || true
rm -f "$BRIDGE_PID" "$BRIDGE_SOCK"
echo "bridge stopped"
else
echo "bridge not running"
fi
;;

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 501c8d5a8a

ℹ️ 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".

Comment thread scripts/host-tmux
Comment on lines +61 to +62
local user="${DEVA_HOST_USER:-${DEVA_CHROME_HOST_USER:-}}"
[[ -n "$user" ]] || die "cannot determine host user; set DEVA_HOST_USER=<host username>"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Set the host user for normal containers

In containers started without Claude Chrome, this fallback is empty because prepare_claude_chrome_bridge is the only launcher path that exports DEVA_CHROME_HOST_USER, and it returns unless the active agent is Claude with --chrome. As a result, host_target aborts before setup, ls, bridge, or doctor can do anything in the default deva/codex workflows, so the documented host-tmux setup quick start fails unless every user manually supplies DEVA_HOST_USER; pass the host user from the launcher or derive it independently here.

Useful? React with 👍 / 👎.

Comment thread scripts/host-tmux
;;
stop)
if [[ -f "$BRIDGE_PID" ]]; then
kill "$(cat "$BRIDGE_PID")" 2>/dev/null || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify the bridge PID before killing it

If bridge.pid is stale, for example after the ssh forwarder exits or a container with a persisted home is restarted while /tmp/host-tmux.sock is gone, this kill can signal an unrelated process that reused the recorded PID because stop only checks that the pid file exists. Use bridge_alive or verify the recorded process is the ssh forwarder before sending SIGTERM.

Useful? React with 👍 / 👎.

- doctor's closed-port message now includes the one-liner
  (sudo systemsetup -setremotelogin on) next to the Settings path
- docs: no-sshd fallbacks — container-side tmux via docker exec
  (zero host config, covers agent comms), or the retired socat
  bridge from git history with its caveats; enabling Remote Login
  from inside a container is impossible by design (file access,
  never process execution)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 03:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Comment thread scripts/host-tmux
Comment on lines +167 to +187
cmd_ls() {
local bin
bin=$(host_tmux_bin)
hssh "$(host_target)" "$bin ls"
}

cmd_attach() {
[[ -t 0 ]] || die "attach needs a tty; use 'host-tmux tmux ...' instead"
local bin cmd="attach"
bin=$(host_tmux_bin)
[[ -n "${1:-}" ]] && cmd="new-session -A -s $(printf '%q' "$1")"
mkdir -p "$STATE_DIR"
exec ssh -t "${SSH_OPTS[@]}" "$(host_target)" "$bin $cmd"
}

cmd_tmux() {
[[ $# -ge 1 ]] || die "'tmux' needs arguments"
local bin
bin=$(host_tmux_bin)
hssh "$(host_target)" "$bin $(printf '%q ' "$@")"
}
Comment thread scripts/host-tmux
Comment on lines +155 to +161
echo "installing pubkey into $home/.ssh/authorized_keys via docker host mount..."
"${dockercmd[@]}" run --rm -e PUB="$pub" -e MARKER="$KEY_MARKER" -v "$home/.ssh:/hostssh" alpine sh -c '
touch /hostssh/authorized_keys
grep -qxF "$PUB $MARKER" /hostssh/authorized_keys || grep -qxF "$PUB" /hostssh/authorized_keys \
|| printf "%s %s\n" "$PUB" "$MARKER" >> /hostssh/authorized_keys
chmod 600 /hostssh/authorized_keys' \
|| die "docker key install failed; $manual"
The container is the sandbox. host-tmux reaches OUT to run commands on
the host, so baking it into every image handed every agent a host-exec
path (deva mounts host ~/.ssh and auto-mounts docker.sock — either is a
turnkey escape once the script is present).

- do not install host-tmux in the image (drop Dockerfile COPY)
- add `deva.sh --host-tmux`: provisions the script + host ~/.ssh (ro)
  per run; the ssh mount dedups against a user .deva that already
  mounts it (added after user volumes, guarded by new
  docker_args_has_mount_target)
- docs reframed around the sandbox: safe default is host->container
  (deva.sh shell / docker exec — host reaches in, container gains
  nothing); host-tmux is the explicit opt-in escape hatch; the
  hardened --no-docker config stays sealed unless --host-tmux is set
- tests: 3 --host-tmux cases in scripts/test-mount-shape.sh

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 10, 2026 04:06
@lroolle lroolle changed the title feat!: replace socat tmux bridge with host-tmux ssh transport feat: opt-in host-tmux (container->host tmux), sandbox-safe by default Jul 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread scripts/host-tmux
Comment on lines +238 to +242
local target
if ! target=$(host_target); then
echo "target: FAILED (set DEVA_HOST_USER=<host username>)"
return 1
fi
Comment thread deva.sh
Comment on lines +3512 to +3518
# --host-tmux needs the host ssh key inside the container to authenticate.
# Add it here, after all user volumes, so we skip it when the user's .deva
# already mounts ~/.ssh (else the dedup guard below would reject it).
if [ "${HOST_TMUX:-false}" = true ] && [ -d "$HOME/.ssh" ] \
&& ! docker_args_has_mount_target "/home/deva/.ssh"; then
DOCKER_ARGS+=("-v" "$HOME/.ssh:/home/deva/.ssh:ro")
fi
@lroolle

lroolle commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

Closing as superseded rather than rejected on substance — the ssh transport analysis here is right (unauthenticated TCP 41555 was a hole, reboot durability matters, protocol decoupling matters) and the direction-split framing is keeping. What changes: tmux interaction becomes a built-in deva module supporting BOTH modes — the in-container agent<->agent bridge and the opt-in container->host transport — instead of loose scripts riding alongside. New issue + design PR incoming; this branch's work carries forward into it.

@lroolle lroolle closed this Jul 10, 2026
lroolle added a commit that referenced this pull request Jul 14, 2026
tmux interaction was four loose scripts with zero deva.sh integration;
the #406 draft built the right transport (ssh) but deleted the socat
fallback and stayed a loose script. This makes it a module:

- scripts/deva-tmux (baked into both images): ls/attach/tmux/bridge/
  setup/doctor over two transport backends. ssh default: authenticated,
  reboot-durable, runs the HOST's tmux client so there is no
  client/server protocol coupling. socat fallback: the old
  unauthenticated TCP daemon, kept for hosts without sshd, loud warning
  on every start. Both land on /tmp/host-tmux.sock — tmux-bridge
  (Layer 2) and htmux muscle memory work unchanged.
- Inert without provisioning: baked binary + no key + no daemon = no
  host reach. deva.sh --host-tmux is the opt-in — mounts ~/.ssh
  read-only (dedup-safe, user -v wins), passes DEVA_HOST_USER, and in
  repo mode mounts the launcher's deva-tmux over the image copy.
- deva.sh tmux command group (host side): setup writes your OWN
  authorized_keys — the #406 draft did that from inside the container
  via a docker host-mount, exactly the host-reach pattern this tool
  exists to gate; moving it host-side deletes the scariest code.
  host-daemon start|stop|status wraps the socat daemon (inline
  fallback for installed launchers). doctor for the host view.
- deva-bridge-tmux becomes a compat shim (old flags -> env ->
  deva-tmux bridge start --transport socat --foreground);
  deva-bridge-tmux-host unchanged; tmux-bridge stays vendored
  byte-for-byte.
- Fixed in live testing: a backgrounded ssh -N -L inherited the
  caller's stdout, so piping 'bridge start' hung forever waiting for
  EOF (the #406 draft shared this bug). Bridge children now detach
  stdio to a state-dir log.

Verified live against a real macOS host from inside a container: both
transports bridge the host server onto the socket, native tmux client
and tmux-bridge Layer 2 drive real panes through it, lifecycle
idempotent, socket cleaned on stop. Mount-shape suite asserts the
off/on/dedup provisioning shapes.

Close #412

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

host-tmux: SSH transport for container-to-host tmux

2 participants