Initial pre-release
tmux-party: share a tmux session with people on the same UNIX host. Single-file POSIX shell (party) with a filesystem + tmux server-access trust model. See README.md.
This commit is contained in:
commit
6be0ac1877
20 changed files with 3869 additions and 0 deletions
435
party.1
Normal file
435
party.1
Normal file
|
|
@ -0,0 +1,435 @@
|
|||
.\" party - share a tmux session with the people you already work with
|
||||
.Dd April 30, 2026
|
||||
.Dt PARTY 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm party
|
||||
.Nd share a tmux session with the people you already work with
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Cm host
|
||||
.Op Ar name
|
||||
.Op Fl -group Ar group
|
||||
.Nm
|
||||
.Cm close
|
||||
.Op Ar name
|
||||
.Nm
|
||||
.Cm join
|
||||
.Op Ar name
|
||||
.Op Fl -passive
|
||||
.Nm
|
||||
.Cm leave
|
||||
.Nm
|
||||
.Cm invite
|
||||
.Ar user
|
||||
.Op Fl r
|
||||
.Nm
|
||||
.Cm voice
|
||||
.Ar user
|
||||
.Nm
|
||||
.Cm mute
|
||||
.Ar user
|
||||
.Nm
|
||||
.Cm kick
|
||||
.Ar user
|
||||
.Nm
|
||||
.Cm detach
|
||||
.Ar user
|
||||
.Nm
|
||||
.Cm role
|
||||
.Op Cm active | passive | switch
|
||||
.Nm
|
||||
.Cm list
|
||||
.Nm
|
||||
.Cm who
|
||||
.Op Fl -short
|
||||
.Nm
|
||||
.Cm status
|
||||
.Sh DESCRIPTION
|
||||
.Nm
|
||||
hosts a live
|
||||
.Xr tmux 1
|
||||
session for other users on the same UNIX host, or joins one
|
||||
someone else is hosting, with the ergonomics of
|
||||
.Ql screen -x
|
||||
and none of the copy/paste friction.
|
||||
Use it to pair-program, mentor, debug an outage together, or demo a
|
||||
workflow live: same terminal, no screen-sharing software, no
|
||||
intermediary servers.
|
||||
.Pp
|
||||
Each party runs on its own dedicated
|
||||
.Xr tmux 1
|
||||
server, distinct from the host's personal
|
||||
.Xr tmux 1 .
|
||||
This keeps
|
||||
.Xr tmux 1
|
||||
.Cm server-access
|
||||
scoped: inviting someone to your party does not expose your other
|
||||
sessions.
|
||||
Two gates protect each party.
|
||||
The
|
||||
.Em filesystem gate
|
||||
is universal across every supported platform: the per-party directory
|
||||
is
|
||||
.Xr chgrp 1 Ns 'd
|
||||
to
|
||||
.Ev TMUX_PARTY_GROUP
|
||||
at mode 0750 so only group members can traverse into it; the socket
|
||||
itself is
|
||||
.Ql g+rw .
|
||||
The
|
||||
.Em auth gate
|
||||
is
|
||||
.Xr tmux 1
|
||||
.Cm server-access :
|
||||
a user who passes the FS gate but is not on the allowlist is rejected
|
||||
at the protocol layer.
|
||||
Both gates must pass.
|
||||
.Pp
|
||||
.Nm
|
||||
is designed for small, mutually trusted groups: a hacklab, a tech
|
||||
team, a circle of friends, not strangers across the internet.
|
||||
The auth gate is authoritative; the FS gate is defense in depth.
|
||||
.Sh SUBCOMMANDS
|
||||
.Bl -tag -width Ds
|
||||
.It Cm host Op Ar name Op Fl -group Ar group
|
||||
Spawn a dedicated party
|
||||
.Xr tmux 1
|
||||
server.
|
||||
With no
|
||||
.Ar name ,
|
||||
a random one is generated.
|
||||
Only the host is on the allowlist; invite others explicitly with
|
||||
.Cm invite .
|
||||
Group precedence:
|
||||
.Fl -group
|
||||
flag >
|
||||
.Ev TMUX_PARTY_GROUP
|
||||
> the default
|
||||
.Ql party .
|
||||
.It Cm close Op Ar name
|
||||
Tear down the party server and remove its per-party directory.
|
||||
With no
|
||||
.Ar name ,
|
||||
closes the only party the caller hosts.
|
||||
Host-only.
|
||||
.It Cm join Op Ar name Op Fl -passive
|
||||
Join a party.
|
||||
With no
|
||||
.Ar name
|
||||
and exactly one party visible, auto-attaches; otherwise presents a
|
||||
numbered picker.
|
||||
By default joins in
|
||||
.Em active
|
||||
mode (your own session group with an independent window cursor).
|
||||
.Fl -passive
|
||||
attaches read-only to the host's session (mirrored view).
|
||||
The read-only boundary is set at attach time and is not toggled by
|
||||
later
|
||||
.Cm role
|
||||
changes.
|
||||
.It Cm leave
|
||||
Detach from the current party and clean up the per-guest session.
|
||||
.It Cm invite Ar user Op Fl r
|
||||
Add
|
||||
.Ar user
|
||||
to the allowlist as read/write.
|
||||
.Fl r
|
||||
or
|
||||
.Fl -read-only
|
||||
invites as a watcher.
|
||||
Best-effort
|
||||
.Xr write 1
|
||||
ping is sent if available.
|
||||
Host-only.
|
||||
.It Cm voice Ar user
|
||||
Promote
|
||||
.Ar user
|
||||
to read/write.
|
||||
Alias:
|
||||
.Cm rw .
|
||||
Host-only.
|
||||
.It Cm mute Ar user
|
||||
Demote
|
||||
.Ar user
|
||||
to read-only.
|
||||
Alias:
|
||||
.Cm ro .
|
||||
Host-only.
|
||||
.It Cm kick Ar user
|
||||
Revoke
|
||||
.Ar user Ns 's
|
||||
invite, disconnect their client, and kill their guest session.
|
||||
Host-only.
|
||||
.It Cm detach Ar user
|
||||
Disconnect
|
||||
.Ar user Ns 's
|
||||
client and kill their guest session, but keep them on the allowlist.
|
||||
Host-only.
|
||||
.It Cm role Op Cm active | passive | switch
|
||||
Flip your clients between the guest session and the host session.
|
||||
With no argument, prints the current role for each of your clients.
|
||||
Session-level only: the read-only boundary set at attach time
|
||||
.Po see
|
||||
.Cm join Fl -passive
|
||||
.Pc
|
||||
is not toggled by role changes
|
||||
.Po
|
||||
.Xr tmux 1
|
||||
.Cm switch-client
|
||||
has no read-only flag
|
||||
.Pc .
|
||||
.It Cm list
|
||||
List live parties on this host from the roster.
|
||||
.It Cm who Op Fl -short
|
||||
Show invited and attached users for the current party.
|
||||
.Fl -short
|
||||
emits a compact form suitable for a status-line widget.
|
||||
.It Cm status
|
||||
Show the caller's own state: hosting, attached, or idle.
|
||||
.El
|
||||
.Sh ENVIRONMENT
|
||||
.Bl -tag -width "PARTY_SOCKET_DIR"
|
||||
.It Ev TMUX_PARTY_GROUP
|
||||
Shared system group used for socket access.
|
||||
Default
|
||||
.Ql party .
|
||||
Override to reuse an existing group like
|
||||
.Ql wheel ,
|
||||
.Ql users ,
|
||||
or
|
||||
.Ql staff ,
|
||||
or pass
|
||||
.Fl -group Ar name
|
||||
to
|
||||
.Cm host .
|
||||
.It Ev PARTY_SOCKET_DIR
|
||||
Where each party's per-party private directory (and its socket and
|
||||
roster) is created.
|
||||
Default
|
||||
.Pa /tmp .
|
||||
.It Ev PARTY_TMUX
|
||||
.Xr tmux 1
|
||||
binary to use.
|
||||
Default
|
||||
.Ql tmux .
|
||||
Override if
|
||||
.Xr tmux 1
|
||||
\(>= 3.3 lives at a non-standard path.
|
||||
.El
|
||||
.Sh FILES
|
||||
.Bl -tag -width Ds
|
||||
.It Pa ${PARTY_SOCKET_DIR}/party-${USER}:${NAME}.d/
|
||||
Per-party private directory, mode 0750, group
|
||||
.Ev TMUX_PARTY_GROUP .
|
||||
.It Pa ${PARTY_SOCKET_DIR}/party-${USER}:${NAME}.d/sock
|
||||
The
|
||||
.Xr tmux 1
|
||||
socket, mode
|
||||
.Ql g+rw .
|
||||
.It Pa ${PARTY_SOCKET_DIR}/party-${USER}:${NAME}.d/roster
|
||||
Bookkeeping:
|
||||
.Va HOST_USER ,
|
||||
.Va PARTY_NAME ,
|
||||
.Va SOCKET ,
|
||||
.Va SERVER_PID ,
|
||||
.Va GROUP ,
|
||||
.Va CREATED .
|
||||
Security-relevant fields are re-derived from the directory itself on
|
||||
every read, not trusted from the file.
|
||||
.It Pa ${PARTY_SOCKET_DIR}/party-${USER}:${NAME}.d/.party-notify
|
||||
Auto-generated helper that fans
|
||||
.Cm display-message
|
||||
out to every attached client on join/leave.
|
||||
.El
|
||||
.Sh EXIT STATUS
|
||||
.Bl -tag -width Ds
|
||||
.It 0
|
||||
Success.
|
||||
.It 1
|
||||
Operational failure (no such party, not a member of the group, socket
|
||||
gone, ambiguous match, etc.).
|
||||
.It 2
|
||||
Usage error (unknown subcommand or flag, missing argument, invalid
|
||||
party name).
|
||||
.It 13
|
||||
.Cm join :
|
||||
caller is not on the allowlist for the named party.
|
||||
.El
|
||||
.Sh EXAMPLES
|
||||
One-time setup, as root:
|
||||
.Bd -literal -offset indent
|
||||
groupadd party
|
||||
usermod -aG party alice
|
||||
usermod -aG party bob
|
||||
.Ed
|
||||
.Pp
|
||||
Have alice and bob log out and back in so the new group takes effect.
|
||||
.Pp
|
||||
Host a party named
|
||||
.Ql debug-the-deploy
|
||||
and invite alice as a watcher and bob as a co-driver:
|
||||
.Bd -literal -offset indent
|
||||
$ party host debug-the-deploy
|
||||
$ party invite alice -r
|
||||
$ party invite bob
|
||||
.Ed
|
||||
.Pp
|
||||
On bob's side, join the only running party on this host:
|
||||
.Bd -literal -offset indent
|
||||
$ party join
|
||||
.Ed
|
||||
.Pp
|
||||
On alice's side, join read-only:
|
||||
.Bd -literal -offset indent
|
||||
$ party join debug-the-deploy --passive
|
||||
.Ed
|
||||
.Pp
|
||||
Reuse an existing group instead of creating a new one:
|
||||
.Bd -literal -offset indent
|
||||
$ party host debug-the-deploy --group staff
|
||||
.Ed
|
||||
.Sh SECURITY
|
||||
The OS group is the perimeter,
|
||||
.Xr tmux 1 Ns 's
|
||||
.Cm server-access
|
||||
allowlist is the per-user filter.
|
||||
Both are universal across the supported platforms (Linux, FreeBSD,
|
||||
NetBSD, OpenBSD, illumos, Solaris, macOS), so the mechanism is one
|
||||
liner:
|
||||
.Xr chgrp 1
|
||||
+
|
||||
.Xr chmod 1 .
|
||||
No ACL syscalls are invoked.
|
||||
.Pp
|
||||
Trust is anchored in unforgeable primitives:
|
||||
.Xr mkdir 2
|
||||
and
|
||||
.Xr bind 2
|
||||
set the owner to the calling effective UID, and
|
||||
.Xr chown 1
|
||||
requires root.
|
||||
A group member who plants a forged directory cannot fake its
|
||||
ownership, cannot redirect the socket path, and cannot forge a socket
|
||||
elsewhere.
|
||||
.Pp
|
||||
.Nm
|
||||
is designed for small, mutually trusted groups: a hacklab, a tech
|
||||
team, a circle of friends, not strangers across the internet.
|
||||
It assumes you already know and trust the people you are adding to
|
||||
the group; it does not try to be a public access-control system.
|
||||
.Sh CAVEATS
|
||||
On filesystems with NFSv4-style inherited ACLs (macOS HFS+/APFS,
|
||||
FreeBSD/illumos ZFS), an ACE on
|
||||
.Ev PARTY_SOCKET_DIR
|
||||
can override the mode bits
|
||||
.Nm
|
||||
sets, widening access (e.g.\& an inherited
|
||||
.Ql everyone@:rx
|
||||
survives our
|
||||
.Ql chmod 0750 )
|
||||
or narrowing it (e.g.\& a
|
||||
.Ql deny everyone:
|
||||
blocks group members our
|
||||
.Xr chgrp 1
|
||||
tried to admit).
|
||||
The script does not invoke
|
||||
.Xr getfacl 1
|
||||
or
|
||||
.Xr getextattr 8
|
||||
to verify effective access; re-introducing per-platform ACL plumbing
|
||||
would unwind the simplification this design commits to.
|
||||
The FS gate is therefore best-effort on those filesystems; the
|
||||
auth gate
|
||||
.Po
|
||||
.Xr tmux 1
|
||||
.Cm server-access
|
||||
.Pc
|
||||
remains authoritative regardless.
|
||||
The default
|
||||
.Ev PARTY_SOCKET_DIR Ns = Ns Pa /tmp
|
||||
rarely has ACL trouble in practice; if you deploy on a custom dir
|
||||
with restrictive inherited ACLs, expect to handle traversal at the
|
||||
ACL layer for your intended group members.
|
||||
Strangers stay out via
|
||||
.Cm server-access
|
||||
either way.
|
||||
.Pp
|
||||
.Cm server-access
|
||||
decides who may
|
||||
.Em attach ,
|
||||
not who can see that a party
|
||||
.Em exists .
|
||||
Hiding a party's existence, meaning the host, its name, group, and
|
||||
creation time recorded in the
|
||||
.Pa roster ,
|
||||
falls to the FS gate, not the auth gate: on an ACL-shadowed dir where the
|
||||
mode bits do not hold, a non-member may read that metadata even though
|
||||
.Cm server-access
|
||||
still blocks the join.
|
||||
The
|
||||
.Cm list
|
||||
subcommand re-checks group membership before printing, as defense in
|
||||
depth, but treat discovery as confidential only insofar as the filesystem
|
||||
enforces it.
|
||||
.Pp
|
||||
An active
|
||||
.Cm join
|
||||
lands you in your own
|
||||
.Ql __party_guest_<you>
|
||||
session on the shared server.
|
||||
Any write-capable invitee can create sessions there, including one named
|
||||
for someone else; if they pre-create yours, your next active
|
||||
.Cm join
|
||||
attaches to it.
|
||||
This is inside the trust boundary by design, not a defended one;
|
||||
invitees are people you already trust.
|
||||
If you do not trust an invitee that far, invite them read-only
|
||||
.Pq Fl r ,
|
||||
which cannot create sessions, or not at all.
|
||||
.Pp
|
||||
.Nm
|
||||
requires
|
||||
.Xr tmux 1
|
||||
\(>= 3.3 for
|
||||
.Cm server-access .
|
||||
Older
|
||||
.Xr tmux 1
|
||||
versions are refused at host time rather than degrading to the FS
|
||||
gate alone.
|
||||
.Pp
|
||||
The
|
||||
.Cm role
|
||||
subcommand can flip a client between the guest session and the host
|
||||
session, but it cannot change the read-only state of an attached
|
||||
client;
|
||||
.Xr tmux 1
|
||||
.Cm switch-client
|
||||
has no equivalent of
|
||||
.Cm attach Fl r .
|
||||
The watcher boundary is set at
|
||||
.Cm join Fl -passive
|
||||
time and is fixed for the life of that client.
|
||||
.Sh SEE ALSO
|
||||
.Xr tmux 1 ,
|
||||
.Xr screen 1 ,
|
||||
.Xr write 1 ,
|
||||
.Xr chgrp 1 ,
|
||||
.Xr chmod 1
|
||||
.Sh HISTORY
|
||||
.Nm
|
||||
was written for the UNIX Social Club
|
||||
.Pq Lk https://club.unix.rocks/ ,
|
||||
inspired by many years of collective working sessions using
|
||||
.Ql screen -x
|
||||
across collectives.
|
||||
.Sh AUTHORS
|
||||
.Nm
|
||||
was written by
|
||||
.An veg
|
||||
and
|
||||
.An kol3rby .
|
||||
See the
|
||||
.Pa party
|
||||
script header for additional credits.
|
||||
Issues and contributions welcome!
|
||||
Loading…
Add table
Add a link
Reference in a new issue