Skip to content
Closed
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 Pi Calm presentation preference; LOCAL, gitignored, and not inhe
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 "off" opt-out from, or "on" opt-in to, Herdr's default-on disposable single-task visual projection, which is unconfigured-default-on only at or above a Herdr version floor; LOCAL, gitignored; inherited by secondmate homes; see docs/herdr-backend.md "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 Relay watcher cadence; LOCAL, gitignored; source before arming watcher when present
Expand Down
187 changes: 187 additions & 0 deletions bin/fm-gh-ci-fallback.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
#!/usr/bin/env bash
# Preserve no-mistakes' `gh pr checks` contract when GitHub forbids the
# check-runs read but still permits workflow-runs reads.
#
# Usage: fm-gh-ci-fallback.sh <real-gh> pr checks <pr-number-or-url> --repo <owner/repo> --json <fields>
# fm-gh-ci-fallback.sh --supports pr checks <pr-number-or-url> --repo <owner/repo> --json <fields>
#
# Token routing is deliberately least-privilege. The original check-runs call,
# the pull-request head lookup, and the exact-head workflow-runs lookup all use
# the caller's ambient GH_TOKEN/GITHUB_TOKEN unchanged. This script never calls
# fm-gh.sh, never injects config/gh-credential's broader PR-capable credential,
# and never prints a token. It falls back only after the original command reports
# the GraphQL personal-token denial whose error path names a `statusCheckRollup`
# component, at whatever depth GitHub currently reports it; every other result,
# including a denial for any other API, is replayed unchanged.
#
# The fallback is intentionally limited to the two literal argument vectors used
# by the supported no-mistakes CI monitor: a selector, `--repo owner/repository`,
# and `--json name,state,bucket,completedAt` with or without the final `link` field.
# Other `gh pr checks` invocations keep the real gh result, so installing the
# PATH-wide shim does not silently change an interactive command's output format.
#
# A fallback verdict is tied to the PR's exact current head SHA. The PR number
# and owner/repository are strictly validated, the head is read from
# repos/<owner>/<repo>/pulls/<number>, and Actions is queried through
# repos/<owner>/<repo>/actions/runs?head_sha=<exact-sha>. No branch name or local
# HEAD can certify green. All exact-head workflow runs are paginated, then mapped
# into the JSON check shape no-mistakes already consumes. An empty run list emits
# one pending placeholder, so absence of workflow evidence can never certify green.
# The PR head is read again after pagination and any drift refuses the fallback
# result, so a verdict can never describe a head that is no longer current.
# This evidence covers GitHub Actions only; it cannot reconstruct third-party
# check providers hidden behind the forbidden check-runs API.
set -eu

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=bin/fm-pr-lib.sh
. "$SCRIPT_DIR/fm-pr-lib.sh"

fallback_shape_parse() {
[ "$#" -eq 7 ] || return 1
[ "${1:-}" = pr ] && [ "${2:-}" = checks ] || return 1
[ "${4:-}" = --repo ] && [ "${6:-}" = --json ] || return 1
SELECTOR=${3:-}
REPO=${5:-}
JSON_FIELDS=${7:-}

case "$JSON_FIELDS" in
name,state,bucket,completedAt|name,state,bucket,completedAt,link) ;;
*) return 1 ;;
esac

case "$SELECTOR" in
https://github.com/*)
fm_pr_url_parse "$SELECTOR" || return 1
[ "$FM_PR_PROVIDER" = github ] || return 1
[ "$REPO" = "$FM_PR_PATH" ] || return 1
REPO=$FM_PR_PATH
NUMBER=$FM_PR_NUMBER
;;
*)
case "$SELECTOR" in
''|0|*[!0-9]*) return 1 ;;
esac
fm_pr_url_parse "https://github.com/$REPO/pull/$SELECTOR" || return 1
[ "$FM_PR_PROVIDER" = github ] || return 1
REPO=$FM_PR_PATH
NUMBER=$FM_PR_NUMBER
;;
esac
}

if [ "${1:-}" = --supports ]; then
shift
fallback_shape_parse "$@"
exit $?
fi

[ "$#" -ge 5 ] || {
echo "fm-gh-ci-fallback: invalid invocation" >&2
exit 2
}
REAL_GH=$1
shift

ORIGINAL_OUT=$(mktemp "${TMPDIR:-/tmp}/fm-gh-ci-original-out.XXXXXX")
ORIGINAL_ERR=$(mktemp "${TMPDIR:-/tmp}/fm-gh-ci-original-err.XXXXXX")
FALLBACK_OUT=$(mktemp "${TMPDIR:-/tmp}/fm-gh-ci-fallback-out.XXXXXX")
# shellcheck disable=SC2329 # Registered by the EXIT trap below.
cleanup() {
rm -f -- "$ORIGINAL_OUT" "$ORIGINAL_ERR" "$FALLBACK_OUT"
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM

ORIGINAL_STATUS=0
"$REAL_GH" "$@" > "$ORIGINAL_OUT" 2> "$ORIGINAL_ERR" || ORIGINAL_STATUS=$?
if [ "$ORIGINAL_STATUS" -eq 0 ]; then
cat "$ORIGINAL_OUT"
cat "$ORIGINAL_ERR" >&2
exit 0
fi

replay_original() {
cat "$ORIGINAL_OUT"
cat "$ORIGINAL_ERR" >&2
exit "$ORIGINAL_STATUS"
}

# Requiring GitHub's exact denial sentence together with a whole
# `statusCheckRollup` component inside its GraphQL error path keeps a changed gh
# failure from being reinterpreted as CI state, while tolerating the deeper
# property paths GitHub appends to that same denial as its check-runs selection
# set evolves. A component match is deliberate: a path that merely starts with
# those characters, and any denial for another API, still replays unchanged.
DENIAL_PATTERN='GraphQL: Resource not accessible by personal access token \(([A-Za-z0-9_]+\.)*statusCheckRollup(\.[A-Za-z0-9_]+)*\)'
if ! grep -Eq "$DENIAL_PATTERN" "$ORIGINAL_OUT" "$ORIGINAL_ERR"; then
replay_original
fi

fallback_shape_parse "$@" || replay_original

read_exact_pr_head() {
local head
head=$("$REAL_GH" api -X GET "repos/$REPO/pulls/$NUMBER" --jq .head.sha 2>/dev/null) || return 1
head=${head//$'\r'/}
head=${head//$'\n'/}
fm_pr_head_valid "$head" || return 1
printf '%s\n' "$head"
}

PR_HEAD=
if ! PR_HEAD=$(read_exact_pr_head); then
echo "fm-gh-ci-fallback: exact PR head lookup failed; preserving the original gh pr checks failure" >&2
replay_original
fi

# gh's built-in jq evaluator applies this after --paginate --slurp has wrapped
# every Actions response page in one outer array. Producing one object per
# workflow run lets the existing no-mistakes classifier reach green, red,
# cancelled, skipped, and pending verdicts without any upstream patch.
# shellcheck disable=SC2016 # This is a literal jq program; its $ names belong to jq.
RUNS_JQ='[
.[].workflow_runs[]
| .status as $status
| .conclusion as $conclusion
| {
name: (.name // "GitHub Actions workflow"),
state: (if $status != "completed" then ($status // "pending") else ($conclusion // "pending") end),
bucket: (
if $status != "completed" or $conclusion == null then "pending"
elif $conclusion == "success" then "pass"
elif $conclusion == "cancelled" then "cancel"
elif ($conclusion == "skipped" or $conclusion == "neutral" or $conclusion == "stale") then "skipping"
elif ($conclusion == "failure" or $conclusion == "timed_out" or $conclusion == "action_required" or $conclusion == "startup_failure") then "fail"
else "pending"
end
),
completedAt: (if $status == "completed" then (.updated_at // "") else "" end),
link: (.html_url // "")
}
] | if length == 0 then [{
name: "GitHub Actions workflows",
state: "pending",
bucket: "pending",
completedAt: "",
link: ""
}] else . end'

RUNS_ENDPOINT="repos/$REPO/actions/runs?head_sha=$PR_HEAD&per_page=100"
if "$REAL_GH" api -X GET "$RUNS_ENDPOINT" --paginate --slurp --jq "$RUNS_JQ" \
> "$FALLBACK_OUT" 2>/dev/null; then
PR_HEAD_AFTER=
if ! PR_HEAD_AFTER=$(read_exact_pr_head); then
echo "fm-gh-ci-fallback: exact PR head recheck failed; preserving the original gh pr checks failure" >&2
replay_original
fi
if [ "$PR_HEAD_AFTER" != "$PR_HEAD" ]; then
echo "fm-gh-ci-fallback: PR head changed during workflow-runs lookup; preserving the original gh pr checks failure" >&2
replay_original
fi
cat "$FALLBACK_OUT"
exit 0
fi

echo "fm-gh-ci-fallback: exact-head workflow-runs lookup failed; preserving the original gh pr checks failure" >&2
replay_original
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
Loading