From cdcce4305cd0e6a632d66e21bc62cc8e114224f0 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 4 Nov 2026 12:50:40 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A4=96=20fix:=20verify-xum=20names=20?= =?UTF-8?q?the=20umask=20fix,=20fails=20fast=20on=20a=20lost=20leader,=20k?= =?UTF-8?q?eeps=20the=20guard=20from=20agent-browser?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under umask 002 git makes a clone's folder, .git and .git/worktrees/ 0775, so every command refused without saying what to do. The git-dir refusals now name the folder and the fix (chmod g-w,o-w on it, or a checkout made under umask 022), and SKILL.md says so next to the rule. A leader that cannot write its id to state.env exits before make, but launch waited for the whole deadline. The wait loop now stops when the leader is gone and no process carries the run's id, and says why. From launch_failed, cleanup runs agent-browser with the launch guard on fd 9. Those calls now close fd 9, so an agent-browser daemon started there cannot hold the flock and block every later launch. --- _Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh` • Cost: `$186.72`_ --- .xum/skills/verify-xum/SKILL.md | 8 +++- .xum/skills/verify-xum/scripts/verify-xum.sh | 47 ++++++++++++++------ 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/.xum/skills/verify-xum/SKILL.md b/.xum/skills/verify-xum/SKILL.md index 455ead811fd..ffaa533adc4 100644 --- a/.xum/skills/verify-xum/SKILL.md +++ b/.xum/skills/verify-xum/SKILL.md @@ -41,8 +41,12 @@ The script runs on Linux only: it needs `setsid`, `flock`, `ss` and `/proc`. Eve refuses a checkout whose git dir (or, for a worktree, its common git dir) is not yours or is writable by group or others, and one with a folder above that git dir that is not yours or root's, or that group or others can write without the sticky bit: the launch lock and the -newest-run link live there. Use a checkout of your own, not a shared repository. Bun on PATH must be the version in `package.json` -`packageManager`. Launch refuses another Bun. +newest-run link live there. Use a checkout of your own, not a shared repository. Under umask 002 +(the default on Debian and Ubuntu, which give each user a private group), git makes a clone's +folder, `.git` and `.git/worktrees/` 0775, so every command refuses. Run +`chmod g-w,o-w` on the folder the message names (it names one at a time), or use a checkout made +under umask 022. Bun on PATH must be the version in `package.json` `packageManager`. Launch +refuses another Bun. ```bash .xum/skills/verify-xum/scripts/verify-xum.sh launch diff --git a/.xum/skills/verify-xum/scripts/verify-xum.sh b/.xum/skills/verify-xum/scripts/verify-xum.sh index 54bca5e62c3..70d54911383 100755 --- a/.xum/skills/verify-xum/scripts/verify-xum.sh +++ b/.xum/skills/verify-xum/scripts/verify-xum.sh @@ -87,26 +87,26 @@ group_is_ours() { # A run's state.env is sourced, so the run folder and the home above it must be real folders (no # symlinks), owned by this user and writable by nobody else. -require_private_dir() { - local dir=$1 mode +require_private_dir() { # require_private_dir [] + local dir=$1 fix=${2:-} mode [[ -d $dir && ! -L $dir ]] || die "$dir is not a real folder" [[ $(stat -c %u "$dir") == "$(id -u)" ]] || die "$dir is not owned by user $(id -u)" mode=$(stat -c %a "$dir") - (((8#$mode & 8#022) == 0)) || die "$dir is writable by group or others (mode $mode)" + (((8#$mode & 8#022) == 0)) || die "$dir is writable by group or others (mode $mode)${fix:+: ${fix//@DIR@/$dir}}" } # Every folder above a run home (or a git dir) must be owned by root or this user and writable by # nobody else, unless it is sticky like /tmp (as ssh's StrictModes checks): another account could # otherwise rename the checked folder away and put its own in its place. Takes a physical path. -require_safe_ancestors() { # require_safe_ancestors [] - local dir=$1 what=${2:-the run home} owner mode +require_safe_ancestors() { # require_safe_ancestors [] [] + local dir=$1 what=${2:-the run home} fix=${3:-} owner mode while [[ $dir != / ]]; do dir=$(dirname "$dir") owner=$(stat -c %u "$dir") mode=$(stat -c %a "$dir") [[ $owner == 0 || $owner == "$(id -u)" ]] || die "$dir, above $what, is owned by user $owner" (((8#$mode & 8#022) == 0 || (8#$mode & 8#1000) != 0)) \ - || die "$dir, above $what, is writable by group or others and not sticky (mode $mode)" + || die "$dir, above $what, is writable by group or others and not sticky (mode $mode)${fix:+: ${fix//@DIR@/$dir}}" done } @@ -419,8 +419,13 @@ launch_failed() { cmd_cleanup "$VERIFY_XUM_RUN" >&2 || true } +# The process group id that the new group's leader appended to state.env, if it has. +recorded_pgid() { + sed -n 's/^export VERIFY_XUM_PGID=\([0-9][0-9]*\)$/\1/p' "$VERIFY_XUM_RUN/state.env" | tail -n 1 +} + cmd_launch() { - local t0 backend_port vite_port log existing token + local t0 backend_port vite_port log existing token leader need git curl node bun make setsid flock ss realpath agent-browser check_bun LAUNCH_DEADLINE=$((SECONDS + READY_TIMEOUT)) @@ -480,9 +485,18 @@ cmd_launch() { "$VERIFY_XUM_RUN/state.env" "${env_without_live_xum[@]}" \ make -C "$CHECKOUT" dev-server-sandbox DEV_SERVER_SANDBOX_ARGS="--clean-providers --clean-projects" \ >"$log" 2>&1 /dev/null && ! grep -qszx "VERIFY_XUM_SERVER_OF=$VERIFY_XUM_RUN_ID" /proc/[0-9]*/environ; then + VERIFY_XUM_PGID=$(recorded_pgid) + [[ -n $VERIFY_XUM_PGID ]] && break + die "the server's process group exited before it wrote its id to $VERIFY_XUM_RUN/state.env (a read-only file or a full disk?)" + fi sleep 0.1 done release_launch_guard @@ -743,9 +757,11 @@ cmd_cleanup() { local i # Read the token before the group stops: the backend then deletes server.lock. REDACT_TOKEN=$(run_token) - # agent-browser `close` on a session that does not exist starts that session first. - if [[ $(agent-browser session list 2>/dev/null) == *"$AGENT_BROWSER_SESSION"* ]]; then - agent-browser --session "$AGENT_BROWSER_SESSION" close >/dev/null 2>&1 || true + # agent-browser `close` on a session that does not exist starts that session first. From + # launch_failed this runs with the launch guard on fd 9: an agent-browser daemon started here + # must not inherit it, or it holds the flock and blocks every later launch. + if [[ $(agent-browser session list 2>/dev/null 9>&-) == *"$AGENT_BROWSER_SESSION"* ]]; then + agent-browser --session "$AGENT_BROWSER_SESSION" close >/dev/null 2>&1 9>&- || true fi if group_is_ours "$VERIFY_XUM_PGID" "$VERIFY_XUM_RUN_ID"; then kill -TERM -- "-$VERIFY_XUM_PGID" 2>/dev/null || true @@ -779,12 +795,15 @@ cmd_cleanup() { # of this user or that group or others can write: the rule for the run home and the folders # above it. A writable folder above would let another account rename the whole git dir away and # put its own in its place. The checks on the guard's open fd stay as a second line. +# Under umask 002 (the default for Debian's and Ubuntu's per-user groups) git makes a clone's +# folder, .git and .git/worktrees/ 0775, so the refusal says how to fix the folder it names. +GIT_DIR_FIX="run chmod g-w,o-w @DIR@ (git makes its folders group-writable under umask 002), or use a checkout made under umask 022" require_private_git_dirs() { local dir for dir in "$CHECKOUT_GIT_DIR" "$(git -C "$CHECKOUT" rev-parse --path-format=absolute --git-common-dir)"; do dir=$(cd -P -- "$dir" && pwd -P) - require_private_dir "$dir" - require_safe_ancestors "$dir" "the git dir $dir" + require_private_dir "$dir" "$GIT_DIR_FIX" + require_safe_ancestors "$dir" "the git dir $dir" "$GIT_DIR_FIX" done } require_private_git_dirs From 8dc5c63a5a6443bb7cc4f1f6c31f6a6fc85f835e Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Wed, 4 Nov 2026 12:57:44 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A4=96=20fix:=20verify-xum=20quotes?= =?UTF-8?q?=20the=20folder=20in=20its=20chmod=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The umask fix printed the folder as is, so a folder with a space or a glob character gave a command that split or expanded the path. The fix now quotes the folder with printf %q, so the command can be copied as printed. --- _Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh` • Cost: `$186.72`_ --- .xum/skills/verify-xum/scripts/verify-xum.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.xum/skills/verify-xum/scripts/verify-xum.sh b/.xum/skills/verify-xum/scripts/verify-xum.sh index 70d54911383..98f24d9c6e6 100755 --- a/.xum/skills/verify-xum/scripts/verify-xum.sh +++ b/.xum/skills/verify-xum/scripts/verify-xum.sh @@ -85,6 +85,12 @@ group_is_ours() { return 1 } +# ": " with @DIR@ replaced by the folder, quoted so the command can be copied as printed (a +# folder can hold spaces or glob characters). Nothing without a fix. +refusal_fix() { # refusal_fix + if [[ -n $1 ]]; then printf ': %s' "${1//@DIR@/$(printf %q "$2")}"; fi +} + # A run's state.env is sourced, so the run folder and the home above it must be real folders (no # symlinks), owned by this user and writable by nobody else. require_private_dir() { # require_private_dir [] @@ -92,7 +98,7 @@ require_private_dir() { # require_private_dir [ [ Date: Wed, 4 Nov 2026 14:35:01 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A4=96=20fix:=20verify-xum=20inserts?= =?UTF-8?q?=20the=20quoted=20folder=20without=20pattern=20replacement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bash 5.2's patsub_replacement reads & and \ in the replacement of ${var//pat/rep} again, so the printf %q quoting of a folder named amp&ersand or back\slash was undone and the printed chmod command ran shell syntax or missed the folder. The fix text is now split at @DIR@ and the folder is printed with %q between the two parts. --- _Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh` • Cost: `$186.72`_ --- .xum/skills/verify-xum/scripts/verify-xum.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.xum/skills/verify-xum/scripts/verify-xum.sh b/.xum/skills/verify-xum/scripts/verify-xum.sh index 98f24d9c6e6..9364fe55fff 100755 --- a/.xum/skills/verify-xum/scripts/verify-xum.sh +++ b/.xum/skills/verify-xum/scripts/verify-xum.sh @@ -86,9 +86,12 @@ group_is_ours() { } # ": " with @DIR@ replaced by the folder, quoted so the command can be copied as printed (a -# folder can hold spaces or glob characters). Nothing without a fix. +# folder can hold spaces, glob characters, & or \). Nothing without a fix. The fix is split at +# @DIR@ instead of ${1//@DIR@/...}: bash 5.2's patsub_replacement reads & and \ in that +# replacement again and undoes the quoting. refusal_fix() { # refusal_fix - if [[ -n $1 ]]; then printf ': %s' "${1//@DIR@/$(printf %q "$2")}"; fi + [[ -n $1 ]] || return 0 + printf ': %s%q%s' "${1%%@DIR@*}" "$2" "${1#*@DIR@}" } # A run's state.env is sourced, so the run folder and the home above it must be real folders (no