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:
@@ -46,6 +46,13 @@ function serverConf() {
|
||||
return confCache.value;
|
||||
}
|
||||
|
||||
let TMUX_MEMBER = "/opt/agenthub/libexec/tmux-member";
|
||||
/** Test seam: where tmux-member is looked for. */
|
||||
function __setTmuxMember(p) {
|
||||
TMUX_MEMBER = p;
|
||||
confCache = { at: 0, path: null, value: null };
|
||||
}
|
||||
|
||||
function readConf(file) {
|
||||
let text;
|
||||
try {
|
||||
@@ -56,16 +63,28 @@ function readConf(file) {
|
||||
const get = (k) => new RegExp(`^${k}=(.*)$`, "m").exec(text)?.[1]?.trim() || "";
|
||||
const usersRoot = get("USERS_ROOT");
|
||||
if (!path.isAbsolute(usersRoot)) return null;
|
||||
// A member's tmux runs through tmux-member once install-system put it there
|
||||
// (and in sudoers): it picks their RUNNING server's own binary, so a tmux
|
||||
// upgrade never mixes a new client with an old server. Before that, TMUX_BIN.
|
||||
let tmuxBin = get("TMUX_BIN");
|
||||
try {
|
||||
fs.accessSync(TMUX_MEMBER, fs.constants.X_OK);
|
||||
if (path.isAbsolute(tmuxBin)) tmuxBin = TMUX_MEMBER;
|
||||
} catch {
|
||||
/* not installed yet */
|
||||
}
|
||||
return {
|
||||
hostUser: get("HOST_USER"),
|
||||
hostUid: Number(get("HOST_UID")),
|
||||
usersRoot,
|
||||
port: Number(get("PORT")) || null,
|
||||
// The installed tmux (a new member server starts on it).
|
||||
tmuxInstalled: path.isAbsolute(get("TMUX_BIN")) ? get("TMUX_BIN") : null,
|
||||
// The absolute paths sudoers lists (sudo would resolve a bare name on its
|
||||
// own secure_path, which may not be them).
|
||||
bins: Object.fromEntries(
|
||||
[
|
||||
["tmux", get("TMUX_BIN")],
|
||||
["tmux", tmuxBin],
|
||||
["git", get("GIT_BIN")],
|
||||
["gh", get("GH_BIN")],
|
||||
].filter(([, v]) => path.isAbsolute(v))
|
||||
@@ -154,6 +173,7 @@ function __resetConfCache() {
|
||||
|
||||
module.exports = {
|
||||
__resetConfCache,
|
||||
__setTmuxMember,
|
||||
confPath,
|
||||
LINUX_NAME_RE,
|
||||
serverConf,
|
||||
|
||||
Reference in New Issue
Block a user