Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ config/calm Calm presentation preference; LOCAL, gitignored, and not inherit
config/startup-memory-budget primary-authoritative per-home startup-memory budget; LOCAL, gitignored, materialized as 7,500 estimated tokens by locked primary bootstrap and inherited into secondmate homes; see docs/configuration.md "Startup memory budget"
config/herdr-presentation-spaces optional presence flag for Herdr's default-off disposable single-task visual projection; LOCAL, gitignored; inherited by secondmate homes; see docs/herdr-backend.md "Optional presentation spaces"
config/trace-context optional presence flag enabling default-off native W3C trace-context propagation to spawned agents; LOCAL, gitignored; inherited by secondmate homes; see docs/configuration.md "Trace context propagation" and docs/trace-context.md
config/gh-credential optional command prefix injecting a credential authorized for pull-request creation, used by bin/fm-gh.sh; LOCAL, gitignored; absent means GitHub commands run unchanged; see docs/configuration.md "Pull-request credential" and docs/no-mistakes-pr-credential.md
config/cmux-socket-password optional cmux control-socket password; LOCAL, gitignored; read fresh on every cmux CLI call and passed through without ever overriding an operator's own ambient CMUX_SOCKET_PASSWORD when absent (docs/cmux-backend.md "Setup")
config/wedge-alarm optional away-mode wedge-alarm active-alert directives; LOCAL, gitignored; absent means auto (macOS Notification Center when available); see docs/wedge-alarm.md
config/x-mode.env generated X-mode watcher cadence; LOCAL, gitignored; source before arming watcher when present
Expand Down
180 changes: 180 additions & 0 deletions bin/fm-gh-shim-install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#!/usr/bin/env bash
# Install, remove, and verify the `gh` shim that routes pull-request mutations through
# fm-gh.sh. Nothing here runs automatically: installation changes how every process
# using the target PATH resolves gh, so it is always a deliberate, explicit act.
#
# Usage: fm-gh-shim-install.sh --check [--dir <d>] [--path <PATH>]
# fm-gh-shim-install.sh --install --dir <d> [--path <PATH>]
# fm-gh-shim-install.sh --uninstall --dir <d>
#
# --dir <d> directory the shim is installed into, as a symlink named `gh`.
# It must already exist and must precede the real gh on the PATH the
# intercepted process uses.
# --path <PATH> PATH string to evaluate precedence against; defaults to this
# process's own PATH. The no-mistakes daemon resolves its environment
# from the LOGIN shell once at startup, so a check run from an
# unusual shell can report a precedence this daemon does not have.
# See docs/no-mistakes-pr-credential.md.
# --check report installed state, the real gh, and whether --dir wins.
# --check is the default when no mode flag is given.
#
# Exit status is 0 when the requested action succeeded, or when --check finds the shim
# installed and winning; --check exits 1 when the shim is absent or loses precedence,
# so it is usable as a gate.
set -eu

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SHIM_SOURCE="$SCRIPT_DIR/fm-gh-shim.sh"

MODE=--check
DIR=
TARGET_PATH=$PATH

usage() {
sed -n '2,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
}

while [ "$#" -gt 0 ]; do
case "$1" in
--check | --install | --uninstall)
MODE=$1
shift
;;
--dir)
[ "$#" -ge 2 ] || {
echo "fm-gh-shim-install: --dir needs a value" >&2
exit 2
}
DIR=$2
shift 2
;;
--path)
[ "$#" -ge 2 ] || {
echo "fm-gh-shim-install: --path needs a value" >&2
exit 2
}
TARGET_PATH=$2
shift 2
;;
-h | --help)
usage
exit 0
;;
*)
echo "fm-gh-shim-install: unknown argument: $1" >&2
exit 2
;;
esac
done

[ -f "$SHIM_SOURCE" ] || {
echo "fm-gh-shim-install: shim source missing: $SHIM_SOURCE" >&2
exit 1
}

# first_gh_on_path <path> [skip_dir]: echo the first executable gh on <path>,
# optionally ignoring one directory.
first_gh_on_path() {
local path_value=$1 skip=${2:-} entry resolved
local IFS=:
for entry in $path_value; do
[ -n "$entry" ] || entry=.
[ -f "$entry/gh" ] && [ -x "$entry/gh" ] || continue
resolved=$(cd "$entry" 2> /dev/null && pwd) || continue
if [ -n "$skip" ] && [ "$resolved" = "$skip" ]; then
continue
fi
printf '%s\n' "$resolved/gh"
return 0
done
return 1
}

resolve_dir() {
cd "$1" 2> /dev/null && pwd
}

owns_link() {
[ -L "$1" ] && [ "$(readlink "$1")" = "$SHIM_SOURCE" ]
}

case "$MODE" in
--install)
[ -n "$DIR" ] || {
echo "fm-gh-shim-install: --install needs --dir" >&2
exit 2
}
dir_abs=$(resolve_dir "$DIR") || {
echo "fm-gh-shim-install: --dir does not exist: $DIR" >&2
exit 1
}
link="$dir_abs/gh"
if { [ -e "$link" ] || [ -L "$link" ]; } && ! owns_link "$link"; then
echo "fm-gh-shim-install: refusing to replace $link because this installer does not own it; remove it yourself first" >&2
exit 1
fi
real_gh=$(first_gh_on_path "$TARGET_PATH" "$dir_abs") || {
echo "fm-gh-shim-install: no real gh on PATH outside $dir_abs; refusing to install a shim that cannot delegate" >&2
exit 1
}
ln -sf "$SHIM_SOURCE" "$link"
printf 'installed: %s -> %s\n' "$link" "$SHIM_SOURCE"
printf 'delegates to: %s\n' "$real_gh"
winner=$(first_gh_on_path "$TARGET_PATH") || winner=
if [ "$winner" != "$link" ]; then
printf 'WARNING: %s does not win on the evaluated PATH (first gh is %s)\n' \
"$link" "${winner:-none}" >&2
fi
;;
--uninstall)
[ -n "$DIR" ] || {
echo "fm-gh-shim-install: --uninstall needs --dir" >&2
exit 2
}
dir_abs=$(resolve_dir "$DIR") || {
echo "fm-gh-shim-install: --dir does not exist: $DIR" >&2
exit 1
}
link="$dir_abs/gh"
if [ ! -L "$link" ]; then
printf 'not installed: %s\n' "$link"
exit 0
fi
# Only remove a link this script owns, so an unrelated gh symlink survives.
if ! owns_link "$link"; then
echo "fm-gh-shim-install: $link does not point at $SHIM_SOURCE; leaving it alone" >&2
exit 1
fi
rm -f "$link"
printf 'removed: %s\n' "$link"
;;
--check)
status=0
if [ -n "$DIR" ]; then
dir_abs=$(resolve_dir "$DIR") || {
echo "fm-gh-shim-install: --dir does not exist: $DIR" >&2
exit 1
}
link="$dir_abs/gh"
if owns_link "$link"; then
printf 'shim: installed at %s\n' "$link"
else
printf 'shim: not installed at %s\n' "$link"
status=1
fi
real_gh=$(first_gh_on_path "$TARGET_PATH" "$dir_abs") || real_gh=
printf 'real gh: %s\n' "${real_gh:-none found}"
fi
winner=$(first_gh_on_path "$TARGET_PATH") || winner=
printf 'first gh on evaluated PATH: %s\n' "${winner:-none found}"
if [ -n "$DIR" ]; then
if [ "$winner" = "$dir_abs/gh" ]; then
printf 'precedence: %s wins\n' "$dir_abs"
else
printf 'precedence: %s does NOT win\n' "$dir_abs"
status=1
fi
fi
exit "$status"
;;
esac
97 changes: 97 additions & 0 deletions bin/fm-gh-shim.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# A `gh` shim that routes pull-request mutations through fm-gh.sh and passes every
# other invocation straight to the real gh.
#
# Install it as a SYMLINK named `gh` in a directory that precedes the real gh on the
# PATH of the process you need to intercept; bin/fm-gh-shim-install.sh owns
# installation, removal, and the precedence check. The symlink is required: the shim
# locates fm-gh.sh relative to its own real path, so a copy placed outside bin/ cannot
# find the wrapper. Read docs/no-mistakes-pr-credential.md before installing, because
# this shim is scoped to a PATH, not to a repository or a worktree, so every process
# resolving gh through that directory is affected.
#
# Routed invocations, chosen because these are the two calls a fine-grained token is
# forbidden from and the pipeline's PR step makes both:
# gh pr create ...
# gh pr edit ...
# The shape must be exactly `pr` as the first argument and `create` or `edit` as the
# second. Every other invocation, including any other `pr` subcommand, execs the real
# gh unchanged, so the shim's default is current behavior.
set -eu

# FM_GH_SHIM_ACTIVE is a hard recursion stop: if fm-gh.sh's credential prefix itself
# resolves gh through this shim, the second entry passes straight through rather than
# routing again.
ROUTE=no
if [ "${FM_GH_SHIM_ACTIVE:-}" != "1" ] && [ "${1:-}" = "pr" ]; then
case "${2:-}" in
create | edit) ROUTE=yes ;;
esac
fi

SHIM_PATH="${BASH_SOURCE[0]}"
SHIM_DIR="$(cd "$(dirname "$SHIM_PATH")" && pwd)"

# resolve_path <path>: echo an absolute, symlink-resolved path, or the input when it
# cannot be resolved. Used only to compare candidates against this shim.
resolve_path() {
local target=$1
if command -v realpath > /dev/null 2>&1; then
realpath "$target" 2> /dev/null || printf '%s\n' "$target"
else
local dir base
dir=$(cd "$(dirname "$target")" 2> /dev/null && pwd) || {
printf '%s\n' "$target"
return 0
}
base=$(basename "$target")
while [ -L "$dir/$base" ]; do
local link
link=$(readlink "$dir/$base") || break
case "$link" in
/*) dir=$(cd "$(dirname "$link")" && pwd) ;;
*) dir=$(cd "$dir" && cd "$(dirname "$link")" && pwd) ;;
esac
base=$(basename "$link")
done
printf '%s\n' "$dir/$base"
fi
}

SHIM_REAL=$(resolve_path "$SHIM_PATH")
# SHIM_DIR is where the shim was INVOKED from and is what must be skipped when
# searching PATH for the real gh. SHIM_SRC_DIR is where the shim's own file actually
# lives, which is the repo's bin/ when installed as a symlink, and is what locates
# fm-gh.sh. The two differ for every real installation, so neither can serve both roles.
SHIM_SRC_DIR="$(cd "$(dirname "$SHIM_REAL")" && pwd)"

# find_real_gh: echo the first executable named gh on PATH that is not this shim.
# Directory identity alone is not enough, because the shim may be installed under a
# path that resolves to a directory already on PATH, so each candidate is compared
# against the shim's own resolved path as well.
find_real_gh() {
local entry candidate
local IFS=:
for entry in $PATH; do
[ -n "$entry" ] || entry=.
candidate="$entry/gh"
[ -f "$candidate" ] && [ -x "$candidate" ] || continue
[ "$(cd "$entry" 2> /dev/null && pwd)" = "$SHIM_DIR" ] && continue
[ "$(resolve_path "$candidate")" = "$SHIM_REAL" ] && continue
printf '%s\n' "$candidate"
return 0
done
return 1
}

REAL_GH=$(find_real_gh) || {
echo "fm-gh-shim: no real gh found on PATH beyond the shim directory ($SHIM_DIR)" >&2
exit 127
}

if [ "$ROUTE" = "yes" ]; then
export FM_GH_SHIM_ACTIVE=1
exec "${FM_GH_SHIM_WRAPPER:-$SHIM_SRC_DIR/fm-gh.sh}" "$REAL_GH" "$@"
fi

exec "$REAL_GH" "$@"
96 changes: 96 additions & 0 deletions bin/fm-gh.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Run one GitHub command with this home's PR-capable credential injected.
#
# Why this exists: a GitHub fine-grained personal access token is forbidden from the
# GraphQL createPullRequest mutation ("Resource not accessible by personal access
# token"), so `gh pr create` fails while ordinary REST calls succeed. When the ambient
# environment exports such a token, every tool that shells out to `gh` inherits the
# failure. This wrapper runs one command with a credential this home has configured
# for that mutation, and never prints the credential.
#
# Usage: fm-gh.sh <command> [args...] e.g. fm-gh.sh gh pr create --fill
# fm-gh.sh --check report the configured prefix and resolved identity
#
# Configuration: config/gh-credential (LOCAL, gitignored; see docs/configuration.md).
# The file holds ONE command prefix line that injects the credential and then runs its
# trailing arguments, for example:
#
# cred run GITHUB_TOKEN=vault/path --
#
# `gh` gives GH_TOKEN precedence over GITHUB_TOKEN. When a prefix is configured, this
# wrapper removes both ambient variables before starting the prefix so only the token
# that prefix injects can reach `gh`. The unconfigured pass-through changes nothing.
#
# Only the first non-empty, non-comment line is read. The line is split on whitespace
# and executed directly, NOT through a shell, so quoting, globbing, redirection, and
# pipelines in that line are not interpreted. The prefix must end with whatever token
# its own runner needs before the command to run.
#
# When config/gh-credential is absent or blank this wrapper is a transparent
# pass-through: it execs the command unchanged, so a home with no special credential
# behaves exactly as if it had called the command directly.
set -eu

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FM_ROOT="${FM_ROOT_OVERRIDE:-$(cd "$SCRIPT_DIR/.." && pwd)}"
FM_HOME="${FM_HOME:-${FM_ROOT_OVERRIDE:-$FM_ROOT}}"
CONFIG="${FM_CONFIG_OVERRIDE:-$FM_HOME/config}"
CREDENTIAL_FILE="$CONFIG/gh-credential"

# read_prefix: echo the configured prefix line, or nothing when unconfigured.
read_prefix() {
[ -f "$CREDENTIAL_FILE" ] || return 0
local line
while IFS= read -r line || [ -n "$line" ]; do
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"
case "$line" in
'' | '#'*) continue ;;
esac
printf '%s\n' "$line"
return 0
done < "$CREDENTIAL_FILE"
}

PREFIX_LINE=$(read_prefix)
PREFIX=()
[ -n "$PREFIX_LINE" ] && read -r -a PREFIX <<< "$PREFIX_LINE"
# Expand the prefix as ${PREFIX[@]+"${PREFIX[@]}"} rather than "${PREFIX[@]}": under
# `set -u`, bash 3.2 (the system bash macOS still ships) treats an empty array's
# expansion as an unbound variable, which would break the unconfigured pass-through
# path that every home without config/gh-credential takes.

if [ "${1:-}" = "--check" ]; then
if [ -n "$PREFIX_LINE" ]; then
printf 'credential prefix: configured (%s)\n' "$CREDENTIAL_FILE"
else
printf 'credential prefix: none (%s absent or blank); commands run unchanged\n' \
"$CREDENTIAL_FILE"
fi
# Report the identity the wrapper actually resolves. This is the decisive fact when
# a PR lands under an unexpected account; it does not, and cannot, prove that the
# credential is authorized for createPullRequest without opening a real pull request.
if [ -n "$PREFIX_LINE" ]; then
identity=$(env -u GH_TOKEN -u GITHUB_TOKEN \
${PREFIX[@]+"${PREFIX[@]}"} gh api user --jq .login 2> /dev/null) || identity=
else
identity=$(gh api user --jq .login 2> /dev/null) || identity=
fi
if [ -n "$identity" ]; then
printf 'identity: %s\n' "$identity"
else
printf 'identity: unresolved (gh api user failed)\n' >&2
exit 1
fi
exit 0
fi

[ "$#" -ge 1 ] || {
echo "usage: fm-gh.sh <command> [args...] | --check" >&2
exit 2
}

if [ -n "$PREFIX_LINE" ]; then
exec env -u GH_TOKEN -u GITHUB_TOKEN ${PREFIX[@]+"${PREFIX[@]}"} "$@"
fi
exec "$@"
Loading
Loading