feat: auto-select the gating group; 'party' becomes the fallback
Bare 'party host' now walks staff, users, wheel, party and gates on the first group the caller belongs to, announcing the pick. Boxes where people already share a system group need no setup; the curated 'party' group remains the fallback and the explicit --group / TMUX_PARTY_GROUP paths are unchanged. Caller membership is the only probe (id -nG, portable everywhere); whether a guest is a member stays an invite-time warning, where it can actually be answered.
This commit is contained in:
parent
22606ea55d
commit
7770289d85
6 changed files with 165 additions and 22 deletions
|
|
@ -12,7 +12,7 @@ Built for small, mutually trusted groups: a hacklab, a tech team, a circle of fr
|
|||
|
||||
- POSIX shell (`/bin/sh`)
|
||||
- `tmux` ≥ 3.3 (for `server-access`) on `$PATH`, or named via `PARTY_TMUX`
|
||||
- A shared system group (default name `party`, override via `TMUX_PARTY_GROUP`). Every host and guest must be a member.
|
||||
- A shared system group. A party is gated on the first of `staff`, `users`, `wheel`, `party` the host belongs to (`--group <name>` or `TMUX_PARTY_GROUP` always win). Boxes where people already share one of those groups need no setup; creating a dedicated `party` group is the fallback recipe. Every host and guest must be a member of whichever group ends up gating the party.
|
||||
- `write(1)`, optional: used to ping invited guests. Silently skipped when absent.
|
||||
|
||||
## Install
|
||||
|
|
@ -70,7 +70,8 @@ Party metadata (host, server pid, group, creation time) lives in a `roster` file
|
|||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `TMUX_PARTY_GROUP` | `party` | Shared group for socket access. Override to reuse an existing group (`wheel`, `users`, `staff`), or pass `--group <name>` to `party host`. |
|
||||
| `TMUX_PARTY_GROUP` | auto-selected | Shared group for socket access. When unset, `party host` auto-selects (see `PARTY_GROUP_CANDIDATES`); pin an existing group here, or pass `--group <name>` to `party host`. |
|
||||
| `PARTY_GROUP_CANDIDATES` | `staff users wheel party` | Space-separated candidate list `party host` walks when `TMUX_PARTY_GROUP` is unset; gates on the first the host belongs to. |
|
||||
| `PARTY_SOCKET_DIR` | `/tmp` | Where each party's private directory (socket + roster) is created. No whitespace in the path. |
|
||||
| `PARTY_TMUX` | `tmux` | tmux binary to use. Override if tmux ≥ 3.3 lives at a non-standard path. |
|
||||
|
||||
|
|
|
|||
|
|
@ -26,11 +26,6 @@ Direction, not promises. Two rules govern everything below (README
|
|||
|
||||
## Later
|
||||
|
||||
- **Group ergonomics:** the default `party` group needs root and a
|
||||
relog before anyone's first party. On boxes where everyone already
|
||||
shares a group, `host` could offer (or default to) the caller's
|
||||
primary group when `party` doesn't exist. Fits the trust model;
|
||||
costs a little existence-confidentiality.
|
||||
- **Matrix:** live validation on NetBSD and DragonFly; one
|
||||
interactive attach/role/switch pass per release (automated coverage
|
||||
is all non-interactive).
|
||||
|
|
|
|||
66
party
66
party
|
|
@ -16,9 +16,25 @@ PARTY_VERSION=0.1.0
|
|||
# `env -i` invocations). Every cmd_* path references it, so guarantee it
|
||||
# now via id -un rather than crashing later under set -eu.
|
||||
: "${USER:=$(id -un)}"
|
||||
: "${TMUX_PARTY_GROUP:=party}"
|
||||
# Group default. Remember whether the caller supplied TMUX_PARTY_GROUP
|
||||
# explicitly: cmd_host auto-selects a candidate group only when it was
|
||||
# not (an empty value counts as unset; an empty group name gates
|
||||
# nothing).
|
||||
if [ -n "${TMUX_PARTY_GROUP:-}" ]; then
|
||||
TMUX_PARTY_GROUP_EXPLICIT=1
|
||||
else
|
||||
TMUX_PARTY_GROUP_EXPLICIT=0
|
||||
TMUX_PARTY_GROUP=party
|
||||
fi
|
||||
: "${PARTY_SOCKET_DIR:=/tmp}"
|
||||
: "${PARTY_TMUX:=tmux}"
|
||||
# Candidate groups for hosting when none was named: the admin
|
||||
# convention ('staff', also every macOS user's primary group), the
|
||||
# classic shared-users group, the BSD admin group, then the
|
||||
# recommended custom group as the fallback. Caller membership is the
|
||||
# only test; whether *guests* are members is checked where it can
|
||||
# actually be answered, at invite time (cmd_invite warns).
|
||||
: "${PARTY_GROUP_CANDIDATES:=staff users wheel party}"
|
||||
|
||||
# Usage
|
||||
# =====
|
||||
|
|
@ -110,10 +126,28 @@ user_exists() {
|
|||
id -u "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Boolean: is user $1 a member of group $2 (supplementary or primary)?
|
||||
# id -nG is the one portable membership probe: no getent on macOS, no
|
||||
# dscl anywhere else, and it answers primary-GID membership too.
|
||||
user_in_group() {
|
||||
id -nG "$1" 2>/dev/null | tr ' ' '\n' | grep -qx "$2"
|
||||
}
|
||||
|
||||
# Boolean: is the named user in TMUX_PARTY_GROUP?
|
||||
user_in_party_group() {
|
||||
user="$1"
|
||||
id -nG "$user" 2>/dev/null | tr ' ' '\n' | grep -qx "$TMUX_PARTY_GROUP"
|
||||
user_in_group "$1" "$TMUX_PARTY_GROUP"
|
||||
}
|
||||
|
||||
# Print the first PARTY_GROUP_CANDIDATES entry the caller belongs to;
|
||||
# print nothing and return 1 when the caller is in none of them.
|
||||
pick_host_group() {
|
||||
for _pg_g in $PARTY_GROUP_CANDIDATES; do
|
||||
if user_in_group "$USER" "$_pg_g"; then
|
||||
printf '%s\n' "$_pg_g"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Soft assertion: caller is in TMUX_PARTY_GROUP. Fails loudly with
|
||||
|
|
@ -132,6 +166,9 @@ One-time setup (as root), your OS may differ:
|
|||
OpenBSD: groupadd $TMUX_PARTY_GROUP && usermod -G $TMUX_PARTY_GROUP $USER
|
||||
illumos: groupadd $TMUX_PARTY_GROUP && usermod -G $TMUX_PARTY_GROUP $USER
|
||||
Then log out and back in.
|
||||
|
||||
Or gate on any existing group you are already in:
|
||||
party host --group <group>
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -612,9 +649,10 @@ Usage: party host [name] [--group <name>]
|
|||
name Party name (default: random).
|
||||
--group <g> Group used to gate this party. The host must be a
|
||||
member; only members can discover or join. Precedence:
|
||||
--group flag > $TMUX_PARTY_GROUP env > default 'party'.
|
||||
Pick any existing group (e.g. 'staff', 'users', 'wheel');
|
||||
no install-time setup needed.
|
||||
--group flag > $TMUX_PARTY_GROUP env > auto-selection:
|
||||
the first of staff, users, wheel, party the host
|
||||
belongs to (candidate list overridable via
|
||||
$PARTY_GROUP_CANDIDATES).
|
||||
EOF
|
||||
exit 0 ;;
|
||||
-*)
|
||||
|
|
@ -626,14 +664,24 @@ EOF
|
|||
esac
|
||||
done
|
||||
|
||||
# Group precedence: --group flag > $TMUX_PARTY_GROUP env > default
|
||||
# 'party' (set by the env-defaults block at the top of the file).
|
||||
# The chosen group becomes the per-party perimeter: chgrp on the
|
||||
# Group precedence: --group flag > $TMUX_PARTY_GROUP env >
|
||||
# auto-selection over PARTY_GROUP_CANDIDATES ('party' is the last
|
||||
# candidate, a fallback rather than a hardcoded default). The
|
||||
# chosen group becomes the per-party perimeter: chgrp on the
|
||||
# private dir, on the socket, and on the roster. No shared install
|
||||
# directory, pick any existing group the host is in.
|
||||
if [ -n "$group_arg" ]; then
|
||||
TMUX_PARTY_GROUP="$group_arg"
|
||||
export TMUX_PARTY_GROUP
|
||||
elif [ "$TMUX_PARTY_GROUP_EXPLICIT" = 0 ]; then
|
||||
auto_group=$(pick_host_group || true)
|
||||
if [ -n "$auto_group" ]; then
|
||||
TMUX_PARTY_GROUP="$auto_group"
|
||||
export TMUX_PARTY_GROUP
|
||||
echo "party host: gating on group '$TMUX_PARTY_GROUP' (auto-selected; --group overrides)."
|
||||
fi
|
||||
# In none of the candidates: keep the 'party' default so
|
||||
# require_caller_in_group prints its remediation message.
|
||||
fi
|
||||
|
||||
require_caller_in_group
|
||||
|
|
|
|||
20
party.1
20
party.1
|
|
@ -1,5 +1,5 @@
|
|||
.\" party - share a tmux session with the people you already work with
|
||||
.Dd April 30, 2026
|
||||
.Dd July 11, 2026
|
||||
.Dt PARTY 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
|
@ -121,8 +121,12 @@ Group precedence:
|
|||
.Fl -group
|
||||
flag >
|
||||
.Ev TMUX_PARTY_GROUP
|
||||
> the default
|
||||
.Ql party .
|
||||
> auto-selection, which walks
|
||||
.Ql staff ,
|
||||
.Ql users ,
|
||||
.Ql wheel ,
|
||||
.Ql party
|
||||
and gates on the first the host belongs to, announcing the pick.
|
||||
.It Cm close Op Ar name
|
||||
Tear down the party server and remove its per-party directory.
|
||||
With no
|
||||
|
|
@ -232,9 +236,9 @@ Show the caller's own state: hosting, attached, or idle.
|
|||
.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
|
||||
When unset, the group is auto-selected at host time; see
|
||||
.Cm host .
|
||||
Override to pin an existing group like
|
||||
.Ql wheel ,
|
||||
.Ql users ,
|
||||
or
|
||||
|
|
@ -243,6 +247,10 @@ or pass
|
|||
.Fl -group Ar name
|
||||
to
|
||||
.Cm host .
|
||||
.It Ev PARTY_GROUP_CANDIDATES
|
||||
Space-separated candidate list for host-time group auto-selection.
|
||||
Default:
|
||||
.Dq staff users wheel party .
|
||||
.It Ev PARTY_SOCKET_DIR
|
||||
Where each party's per-party private directory (and its socket and
|
||||
roster) is created.
|
||||
|
|
|
|||
85
tests/45-group-select.bats
Normal file
85
tests/45-group-select.bats
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env bats
|
||||
#
|
||||
# Group auto-selection. When neither --group nor TMUX_PARTY_GROUP names
|
||||
# a group, cmd_host walks PARTY_GROUP_CANDIDATES and gates on the first
|
||||
# group the caller belongs to; 'party' is the last candidate, a
|
||||
# fallback rather than a hardcoded default.
|
||||
|
||||
load 'helpers'
|
||||
|
||||
setup() {
|
||||
setup_party_sandbox
|
||||
load_party_lib
|
||||
}
|
||||
|
||||
teardown() { teardown_party_sandbox; }
|
||||
|
||||
# Put a stub id(1) on PATH whose -nG answer is $1; every other
|
||||
# invocation falls through to the real id.
|
||||
stub_id_groups() {
|
||||
mkdir -p "$PARTY_TMP/bin"
|
||||
real_id=$(command -v id)
|
||||
{ printf '#!/bin/sh\n'
|
||||
printf 'case "$1" in\n'
|
||||
printf ' -nG) echo "%s" ;;\n' "$1"
|
||||
printf ' *) exec %s "$@" ;;\n' "$real_id"
|
||||
printf 'esac\n'
|
||||
} > "$PARTY_TMP/bin/id"
|
||||
chmod +x "$PARTY_TMP/bin/id"
|
||||
PATH="$PARTY_TMP/bin:$PATH"
|
||||
}
|
||||
|
||||
@test "pick_host_group prefers staff over the party fallback" {
|
||||
stub_id_groups "$USER staff party"
|
||||
[ "$(pick_host_group)" = staff ]
|
||||
}
|
||||
|
||||
@test "pick_host_group walks candidates in order: users beats wheel" {
|
||||
stub_id_groups "wheel users"
|
||||
[ "$(pick_host_group)" = users ]
|
||||
}
|
||||
|
||||
@test "pick_host_group falls back to party" {
|
||||
stub_id_groups "$USER dialout party"
|
||||
[ "$(pick_host_group)" = party ]
|
||||
}
|
||||
|
||||
@test "pick_host_group fails when the caller is in no candidate" {
|
||||
stub_id_groups "$USER dialout"
|
||||
run pick_host_group
|
||||
[ "$status" -ne 0 ]
|
||||
[ -z "$output" ]
|
||||
}
|
||||
|
||||
@test "PARTY_GROUP_CANDIDATES env overrides the candidate list" {
|
||||
stub_id_groups "$USER randomgrp"
|
||||
PARTY_GROUP_CANDIDATES="randomgrp party" \
|
||||
run pick_host_group
|
||||
[ "$output" = randomgrp ]
|
||||
}
|
||||
|
||||
@test "host auto-selects the caller's group, announces it, records it" {
|
||||
unset TMUX_PARTY_GROUP
|
||||
export PARTY_GROUP_CANDIDATES="$(id -gn) party"
|
||||
run "$PARTY_BIN" host autosel
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"gating on group '$(id -gn)'"* ]]
|
||||
grep -qx "GROUP=$(id -gn)" \
|
||||
"$PARTY_SOCKET_DIR/party-$USER:autosel.d/roster"
|
||||
}
|
||||
|
||||
@test "explicit TMUX_PARTY_GROUP disables auto-selection" {
|
||||
export TMUX_PARTY_GROUP="$(id -gn)"
|
||||
run "$PARTY_BIN" host explicit-env
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" != *"auto-selected"* ]]
|
||||
}
|
||||
|
||||
@test "host in no candidate group gets the remediation message" {
|
||||
unset TMUX_PARTY_GROUP
|
||||
export PARTY_GROUP_CANDIDATES="no-such-group-xyzzy"
|
||||
run "$PARTY_BIN" host nogroup
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"not in the 'party' group"* ]]
|
||||
[[ "$output" == *"--group"* ]]
|
||||
}
|
||||
|
|
@ -19,6 +19,12 @@ setup_party_sandbox() {
|
|||
# zsh with clean env). party uses $USER under set -eu, so guarantee it.
|
||||
export USER="${USER:-$(id -un)}"
|
||||
|
||||
# Pin the gating group. cmd_host auto-selects a candidate group when
|
||||
# TMUX_PARTY_GROUP is unset, which would make bare `party host` calls
|
||||
# environment-dependent; tests keep the historical explicit default.
|
||||
# Suites that test auto-selection unset this themselves.
|
||||
export TMUX_PARTY_GROUP="${TMUX_PARTY_GROUP:-party}"
|
||||
|
||||
export PARTY_TMP="$BATS_TEST_TMPDIR/party"
|
||||
export PARTY_SOCKET_DIR="$PARTY_TMP/sockets"
|
||||
mkdir -p "$PARTY_SOCKET_DIR"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue