feat(server-s3): a member's tmux follows tmux upgrades — tmux-member + Users → Members' tmux
A member's tmux server keeps the version it started with until their last pane closes, but the dashboard asked it with TMUX_BIN, the newest tmux: a 3.8 client on a 3.6 server spun it at 100% CPU (three members needed a manual kill on 2026-10-15). - /opt/agenthub/libexec/tmux-member (install-system, in sudoers): runs a member's tmux call with the binary of THEIR running server (/proc/<pid>/exe, found from the socket in /proc/net/unix), else TMUX_BIN. It runs as the member and execs only a process they own. sudoArgv uses it once installed (linux-accounts readConf), TMUX_BIN until then. - Users → Members' tmux (GET /api/users/tmux): each member server's version beside the installed one, sessions, agents working. An admin restarts one (POST /api/users/:id/tmux-restart) or every idle outdated one (POST /api/users/tmux-restart-idle), with their password again, audited per member; a server that will not stop answers 409 ETMUXSTUCK naming its pid for root. - server-s3-system.test.js: `delete` is a provision action since Delete user, so the "unknown action" case now uses `purge`. Operators: run `sudo agenthub server install-system` once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
# /opt/agenthub/libexec/member-hook Claude hook forwarder for members
|
||||
# /opt/agenthub/libexec/write-as writes in a member's home AS the member (sudo -u)
|
||||
# /opt/agenthub/libexec/pane-launch starts a member's pane from its launch file
|
||||
# /opt/agenthub/libexec/tmux-member a member's tmux, with their running server's own binary
|
||||
# /etc/sudoers.d/agenthub checked with visudo before it is installed
|
||||
# /etc/claude-code/managed-settings.json the member hooks (never over a foreign file)
|
||||
# /proc hidepid=invisible,gid=agenthub-proc (now and in /etc/fstab)
|
||||
@@ -105,6 +106,7 @@ chmod 0644 /etc/agenthub/server.conf
|
||||
install -m 0755 -o root -g root "$ROOT/scripts/server/agenthub-provision.sh" /opt/agenthub/sbin/agenthub-provision
|
||||
install -m 0755 -o root -g root "$ROOT/scripts/server/write-as.sh" /opt/agenthub/libexec/write-as
|
||||
install -m 0755 -o root -g root "$ROOT/scripts/server/pane-launch.sh" /opt/agenthub/libexec/pane-launch
|
||||
install -m 0755 -o root -g root "$ROOT/scripts/server/tmux-member.sh" /opt/agenthub/libexec/tmux-member
|
||||
|
||||
# ── node + the hook handler, for members (host-owned: the dashboard refreshes them) ──
|
||||
NODE_VER=$("$NODE" -p process.version)
|
||||
@@ -138,7 +140,7 @@ visudo -cf "$TMP_SUDOERS" >/dev/null 2>&1 && NOTTY="Defaults:$HOST !requiretty"
|
||||
{
|
||||
echo "# AgentHub server mode (install-system). Members are never in sudoers."
|
||||
[ -z "$NOTTY" ] || echo "$NOTTY"
|
||||
echo "$HOST ALL=(%agenthub-members) NOPASSWD:SETENV: $TMUX_BIN, $GIT_BIN${GH_BIN:+, $GH_BIN}, /opt/agenthub/libexec/write-as, /opt/agenthub/tools/bin/claude"
|
||||
echo "$HOST ALL=(%agenthub-members) NOPASSWD:SETENV: $TMUX_BIN, /opt/agenthub/libexec/tmux-member, $GIT_BIN${GH_BIN:+, $GH_BIN}, /opt/agenthub/libexec/write-as, /opt/agenthub/tools/bin/claude"
|
||||
echo "$HOST ALL=(root) NOPASSWD: /opt/agenthub/sbin/agenthub-provision"
|
||||
for a in start stop restart enable disable; do
|
||||
[ "$a" = start ] || [ "$a" = stop ] || [ "$a" = restart ] && extra=" --no-block" || extra=""
|
||||
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
# @file tmux-member.sh
|
||||
# @description Server mode, one Linux user per account (S3): installed as
|
||||
# /opt/agenthub/libexec/tmux-member, the one tmux the host may run AS a
|
||||
# member (sudoers). Called as `tmux-member -S <socket> <tmux args…>`, it runs
|
||||
# the args with the binary of the tmux server already listening on that
|
||||
# socket (`/proc/<pid>/exe`, which still runs after an upgrade replaced the
|
||||
# file), else with TMUX_BIN from /etc/agenthub/server.conf (no server yet:
|
||||
# this call starts one, on the installed version). A client must match its
|
||||
# server: a tmux 3.8 client asking a 3.6 server kept it at 100% CPU.
|
||||
#
|
||||
# It runs as the member and execs only a process THEY own (under hidepid they
|
||||
# see no other), so it can run nothing they could not run themselves.
|
||||
# @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
set -u
|
||||
bin=$(sed -n 's/^TMUX_BIN=//p' /etc/agenthub/server.conf 2>/dev/null | head -n 1)
|
||||
[[ "$bin" == /* ]] || { echo "tmux-member: no TMUX_BIN in /etc/agenthub/server.conf" >&2; exit 2; }
|
||||
if [ "${1:-}" = -S ] && [ -n "${2:-}" ]; then
|
||||
# /proc/net/unix: Num RefCount Protocol Flags Type St Inode Path; 00010000 = listening.
|
||||
inode=$(awk -v s="$2" '$4 == "00010000" && $8 == s { print $7; exit }' /proc/net/unix 2>/dev/null)
|
||||
if [ -n "$inode" ]; then
|
||||
for d in /proc/[0-9]*; do
|
||||
[ -O "$d" ] || continue
|
||||
read -r comm <"$d/comm" 2>/dev/null || continue
|
||||
[[ "$comm" == tmux* ]] || continue
|
||||
for fd in "$d"/fd/*; do
|
||||
if [ "$(readlink "$fd" 2>/dev/null)" = "socket:[$inode]" ] && [ -x "$d/exe" ]; then
|
||||
exec "$d/exe" "$@"
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
fi
|
||||
exec "$bin" "$@"
|
||||
Reference in New Issue
Block a user