Skip to content

Add a tmux-socket mount word for session tongs - #96

Open
CrypticSwarm wants to merge 10 commits into
masterfrom
tmux-support
Open

CrypticSwarm wants to merge 10 commits into
masterfrom
tmux-support

Conversation

@CrypticSwarm

Copy link
Copy Markdown
Owner

Stacked on #95, which makes closing a tmux pane tear down session tongs; this mount requires the launcher to run in one.

A session tong can now declare tmux-socket in mounts: to reach the host tmux server the launcher runs in. This is the groundwork for a tong that opens sibling sessions in tmux windows. The grant equals docker-socket in power: any command on the host as the socket's owner.

  • Mount: binds the socket's parent directory (tmux recreates the socket on restart) at a fixed /run/swarmforge-tmux, always read-only. Read-only protects the directory only; it does not limit what the tong can do.
  • Environment: the tong gets TMUX (rewritten to the in-container path), TMUX_PANE (a default target, not a restriction), SWARMFORGE_SESSION_HANDLE, and SWARMFORGE_WORKSPACE_HOST_PATH. Plain values of these in the definition are replaced, and ${secret:...} references to them are refused.
  • Fails closed, before any container starts: $TMUX unset or malformed; a socket outside tmux's own tmux-<uid> directory; or that directory not passing an on-disk check. The check requires no symlinks, ownership by the launching user, no group/other permissions, and every ancestor root- or user-owned and not writable by others unless sticky.
  • Session tongs only: refused on shared tongs, which outlive the session.
  • Approval prompt: calls the grant out on its own line, as it does the docker socket.

Base automatically changed from teardown-on-hangup to master October 6, 2026 15:08
A tmux socket is host command execution as its owner, as strong as the
docker socket, so the word comes with the narrowest grammar the mount
vocabulary allows: no target, and no write access. tmux-socket and
tmux-socket:ro are accepted and tmux-socket:rw is refused, by one
helper that validation and argv assembly both ask, so the two cannot
disagree. A client only needs connect() on the socket, which a
read-only bind allows; a writable one would let the tong replace the
socket the host's own clients dial.

The mount binds the socket's directory rather than the socket, since
tmux deletes and recreates the socket when its server restarts. It
lands at the fixed /run/swarmforge-tmux, beside rather than under the
secret tmpfs at /run/swarmforge, so validation never depends on the
host environment, and it is reserved so a workspace target cannot
overlap it. The host directory arrives as a keyword, like the docker
socket path; nothing in the pure core reads the environment.
The tong gets the server through TMUX, re-pointed from the launcher's
socket path to the same socket under /run/swarmforge-tmux, so a stock
tmux client reaches it with no flags. TMUX_PANE gives it the caller's
pane as tmux's default target, from which it can work out the window
without the launcher running any tmux command, and
SWARMFORGE_SESSION_HANDLE names the session it serves. A session
tmux-socket tong also gets SWARMFORGE_WORKSPACE_HOST_PATH, as a
session docker-socket broker does.

These describe the host and the session, which a definition cannot
know, so they replace a plain value its env: sets for those names, and
a name the launcher has no value for is dropped rather than left to
the definition. A secret reference on one of them would be exported
by the FIFO wrapper after docker's -e values and win, so validation
refuses it. A tong without the mount gets none of them, and the argv
builder refuses the mount outright on anything but a session tong, so
no caller can hand a shared container a tmux server.

$TMUX is parsed strictly and the launch stops on anything unexpected.
The socket must sit directly in tmux's own per-user directory
(tmux-<uid>): the mount binds the socket's parent, so a socket at an
arbitrary -S path could otherwise hand the tong a home directory. The
path also may not carry a ':' (docker's -v separator), a ',' (tmux
reads the path up to the first one), or whitespace.
The launcher reads its own TMUX and TMUX_PANE into its options and
passes them, with the session handle, to each session tong it starts.
The environment is read only there, in the launcher's entry layer;
the pure core takes the values as arguments.

A launch whose tongs mount tmux-socket stops before anything starts
when the launcher is not in a tmux pane, when TMUX or TMUX_PANE is
malformed or the socket is outside tmux's default directory, or when
a definition makes one of the names the launcher sets a secret
reference, so a bad value never leaves a half-started session to tear
down.

A shared tong may not mount tmux-socket, for the reason it may not
mount the workspace: its container outlives the session and is reused
by later ones, so it would carry one session's tmux server, and its
pinning to that session's pane, into the next. Shared tongs are never
handed the values even if one got that far.

The approval prompt calls tmux socket access out on its own line, as
it does the docker socket. The line says what the grant really is:
the bind covers the whole socket directory, so every tmux server the
user runs there, and through any of them any command on the host;
the mount being read-only does not narrow that.
The $TMUX parse judges only the spelling of the socket path, but the
mount binds the whole directory, and docker follows a symlink in a
bind source to wherever it points. Before anything starts, the
launcher now refuses a directory reached through a symlink, one that
is not a real directory, one owned by another uid, or one open to
group or others (tmux creates it 0700), and a socket path that does
not hold a socket.

Every directory above it must also pass OpenSSH's StrictModes rule:
owned by root or the launcher's uid, and not writable by group or
others unless sticky, as /tmp is. Without that, a TMUX_TMPDIR under a
world-writable, non-sticky directory would let another local user
swap the socket directory for a symlink between the check and
docker's bind.

The check runs on the host side of the launcher, so the pure core
stays free of filesystem access, and run_with_tongs takes it as an
injectable seam so tests can use a $TMUX that names no real socket.
It takes the socket path from the same parse that yields the
directory, so $TMUX is read in one place.
The mounts section gains the third word: what it grants -- every tmux
server in the user's socket directory, not just the launcher's, with
the pane a default target rather than a restriction -- why it is
read-only and what read-only does not limit, why it is session-only,
the requirement that the launcher run in a tmux pane with a socket in
tmux's default directory, the on-disk checks of that directory, and
the environment the tong receives, including that the launcher's
names may not be secret references.

The tongs overview names tmux-socket beside the workspace as a mount a
shared tong may not have. The approval page lists the tmux server
among what the prompt shows, and the environment reference gains the
variables a tmux-socket tong is given and the two the launcher reads
to give them.
validate_tong, tong_mount_specs and workspace_mount_placements each
looped over mounts: with their own copy of the rules. checked_mounts
is now the one walk, so a mount is accepted or refused the same way,
with the same message, wherever it is checked.
run_with_tongs parsed $TMUX and $TMUX_PANE to check them, then
tong_run_argv parsed them again for every tmux-socket tong.
resolve_tmux_server now parses both into a TmuxServer that
run_with_tongs checks and hands down.

run_with_tongs no longer re-checks the secret env that validation
already refused.
The shared-tong rule for workspace and tmux-socket lived in
unsupported_tong_reasons and an argv raise, so `tongs validate`
passed a definition the launch then refused. mount_lifecycle_error
now holds it, and checked_mounts applies it everywhere.
Docstrings and docs restated the same rules in several places: the
approval gate, the shared-mount rule, the tmux env, and "validation
and argv agree". Each is now stated where it lives, and the others
point there.
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.

1 participant