mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-10-29 06:05:13 +08:00
107 lines
4.8 KiB
Bash
Executable File
107 lines
4.8 KiB
Bash
Executable File
#!/bin/sh
|
|
# Detach a T2 gate (owlab, npm run live, bench, a computed-style diff) so the session never waits on
|
|
# it. Prints the run-id and the log path before the command has produced a byte, and exits 0 whether
|
|
# or not the command later fails: the exit status lives in <run-id>.status, read by hand.
|
|
#
|
|
# Logs go to ../tmp/ beside the checkout, resolved from this script rather than from $PWD, so a run
|
|
# started from a subdirectory still lands in the one scratch directory CLAUDE.md sanctions.
|
|
#
|
|
# tools/bg.sh npm run live -- --all
|
|
# tail -n 40 ../tmp/<run-id>.log
|
|
set -eu
|
|
|
|
if [ $# -eq 0 ]; then
|
|
echo "usage: tools/bg.sh <command> [args...]" >&2
|
|
exit 2
|
|
fi
|
|
|
|
# `Bash(tools/bg.sh:*)` is allow-listed, and the permission engine matches the first word only: it
|
|
# cannot see an `ssh` inside `tools/bg.sh sh -c '…'`, which is the form every T2 gate uses. Without
|
|
# this the wrapper launders any command past the `ask` fence on the hardware router and on
|
|
# publishing. The fence has to live here, where the whole command line is visible.
|
|
#
|
|
# This is a guardrail against the wrapper laundering a command past a permission rule, not a
|
|
# sandbox: it reads the text of the command, so it can be fooled by anything the text itself does
|
|
# not reveal (an alias, a script that shells out, a binary named to match nothing here). What it
|
|
# does catch is scanned as words, not substrings, so `sshd` or a path ending in `-ssh` does not
|
|
# trip it. Backslashes are deleted (not folded to a separator) BEFORE the quoting and shell
|
|
# operators are folded to spaces, so `s\sh` collapses to the `ssh` it will actually become when the
|
|
# shell reassembles it, rather than scanning as two harmless words.
|
|
SCAN=$(printf ' %s ' "$*" | tr -d '\\' | tr '\n\t"`;&|()<>'"'"'' ' ' | tr -s ' ')
|
|
set -f # $SCAN is split into words, never globbed: a run may carry a literal `*`
|
|
prev=''
|
|
for word in $SCAN; do
|
|
# Path forms (`/usr/bin/ssh`, `./ssh`) walk past a bare-word match, so the same names are
|
|
# checked with a `*/name` form too; `*dev-sync.sh` already had this shape and is the model.
|
|
case "$word" in
|
|
ssh | scp | sftp | rsync | */ssh | */scp | */sftp | */rsync | *dev-sync.sh)
|
|
echo "refused: '$word' goes through the human, not through a detached wrapper." >&2
|
|
echo "tools/bg.sh is allow-listed, so a command run through it never reaches the 'ask' rule that guards this one. Run it in the foreground." >&2
|
|
exit 2 ;;
|
|
esac
|
|
# Publishing is `git`/`gh` plus a subcommand, and a path defeats a literal `git push`
|
|
# substring match the same way it defeats the ssh check above; the basename of the PREVIOUS
|
|
# word is what a shell would actually run, so that is what is compared here.
|
|
case "${prev##*/}" in
|
|
git)
|
|
case "$word" in
|
|
push | commit | tag)
|
|
echo "refused: committing, pushing and publishing are given per action by the human (CLAUDE.md), and a detached run cannot be given anything." >&2
|
|
exit 2 ;;
|
|
esac ;;
|
|
gh)
|
|
case "$word" in
|
|
pr | release | issue)
|
|
echo "refused: committing, pushing and publishing are given per action by the human (CLAUDE.md), and a detached run cannot be given anything." >&2
|
|
exit 2 ;;
|
|
esac ;;
|
|
esac
|
|
prev=$word
|
|
done
|
|
set +f
|
|
|
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
|
TMP=$(dirname -- "$ROOT")/tmp
|
|
mkdir -p "$TMP"
|
|
|
|
# The slug is the first two words of the command with everything but [A-Za-z0-9] folded to a dash,
|
|
# so `npm run live` and `owlab test` are distinguishable in a directory listing without opening one.
|
|
SLUG=$(printf '%s %s' "$1" "${2-}" | tr -c 'A-Za-z0-9' '-' | sed 's/--*/-/g; s/^-//; s/-$//')
|
|
RUN="$(date +%Y%m%d-%H%M%S)-${SLUG:-cmd}-$$"
|
|
LOG="$TMP/$RUN.log"
|
|
STATUS="$TMP/$RUN.status"
|
|
PIDF="$TMP/$RUN.pid"
|
|
|
|
{
|
|
printf '# run-id: %s\n' "$RUN"
|
|
printf '# started: %s\n' "$(date -Is 2>/dev/null || date)"
|
|
printf '# cwd: %s\n' "$(pwd)"
|
|
printf '# command: %s\n\n' "$*"
|
|
} >"$LOG"
|
|
|
|
# setsid detaches from the session's process group, so the run survives the shell that started it
|
|
# and a Ctrl-C in the session does not reach it. Not every image has it; nohup alone is the fallback.
|
|
RUNNER=''
|
|
command -v setsid >/dev/null 2>&1 && RUNNER='setsid'
|
|
|
|
# $0 is the log, $1 the status file and $2 the pid file; the shift is what makes "$@" the caller's
|
|
# command alone. The pid written is this inner shell's own, which lives exactly as long as the run:
|
|
# $! would name setsid instead, which exits immediately and would read as a dead run.
|
|
# shellcheck disable=SC2086 # RUNNER is deliberately word-split: empty means "no wrapper".
|
|
nohup $RUNNER sh -c '
|
|
st_file=$1
|
|
pid_file=$2
|
|
shift 2
|
|
printf "%s\n" "$$" >"$pid_file"
|
|
"$@" >>"$0" 2>&1
|
|
st=$?
|
|
printf "\n# exit: %s\n" "$st" >>"$0"
|
|
printf "%s\n" "$st" >"$st_file"
|
|
exit $st
|
|
' "$LOG" "$STATUS" "$PIDF" "$@" >/dev/null 2>&1 &
|
|
|
|
printf 'run-id: %s\n' "$RUN"
|
|
printf 'log: %s\n' "$LOG"
|
|
printf 'status: %s (absent while running)\n' "$STATUS"
|
|
printf 'read: tail -n 40 %s\n' "$LOG"
|