Do not prefer a dialog on a machine with nowhere to draw it

config/bash/shell exported SSH_ASKPASS=ksshaskpass with
SSH_ASKPASS_REQUIRE=prefer on every machine. `prefer` means ssh and git reach
for that dialog BEFORE the terminal prompt, so on a headless server the
prompt that would have worked was skipped in favour of a KDE window that
cannot open:

  error: /usr/bin/ksshaskpass died of signal 6
  fatal: could not read Username for 'https://git.gbrown.org'

A push from a server failed to ASK for a username rather than failing to
authenticate. The shell environment is the one half of Panama a server does
install, so nothing downstream was going to catch this.

The export is now conditional on a session existing to draw in, and on the
binary being there to run. The headless branch also CLEARS both variables
rather than merely declining to set them: an askpass inherited from a parent
that had a display, or from a login predating this guard, is exactly as
unusable and a guard that only withholds leaves it in place. A fresh login
shell on the repaired server still reported prefer until it cleared them.

A desktop is unchanged; a server, a serial console and a plain SSH session
fall back to the terminal prompt, which works.

tests/setup/headless-askpass-contract pins every branch, the inherited case
included, and fails against the previous code and against a guard that only
withholds.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
gibandClaude Opus 5 committed 2026-10-01 17:50:14 -04:00
1 parent a493fa5584
commit a664cf68f5
4 files changed
+102 -3

No files matched your search

+1 -1
View File
@@ -250,7 +250,7 @@ docs/ Settings reference, and the design specs behind the work
## Tests
191 of them, under `tests/`. `tests/contracts.manifest` classifies every
192 of them, under `tests/`. `tests/contracts.manifest` classifies every
contract by the capabilities it needs. Run the hermetic set, or grant a
specific external capability when automation needs it:
+17 -2
View File
@@ -16,8 +16,23 @@ unset PANAMA_ENV
# Editor used by CLI
export EDITOR="nvim"
export SUDO_EDITOR="$EDITOR"
export SSH_ASKPASS=/usr/bin/ksshaskpass
export SSH_ASKPASS_REQUIRE=prefer
# A graphical askpass, and only where there is a session to draw it in.
# SSH_ASKPASS_REQUIRE=prefer means ssh and git reach for the dialog BEFORE the
# terminal prompt, so on a headless machine the prompt that would have worked
# is skipped in favour of one that cannot open: `git push` then fails reading
# a username rather than asking for it. The desktop keeps the dialog; a
# server, a serial console and a plain SSH session fall back to the terminal.
if [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ] && [ -x /usr/bin/ksshaskpass ]; then
export SSH_ASKPASS=/usr/bin/ksshaskpass
export SSH_ASKPASS_REQUIRE=prefer
else
# Cleared, not merely left unset. An askpass inherited from a parent that
# did have a display -- an agent's shell, a forwarded environment, a login
# that predates this guard -- is exactly as unusable here as one this file
# set itself, and declining to add it does nothing about a value already in
# the environment.
unset SSH_ASKPASS SSH_ASKPASS_REQUIRE
fi
# Define Paths
export CARGO_PATH="$HOME/.cargo"
+1
View File
@@ -261,6 +261,7 @@ hermetic tests/setup/firefox-chrome-contract
hermetic tests/setup/hardware-contract
# Reads the host hardware predicates after exercising fixture sysfs trees.
live-host tests/setup/hardware-predicates-contract
hermetic tests/setup/headless-askpass-contract
hermetic tests/setup/hooks-contract
hermetic tests/setup/interview-contract
hermetic tests/setup/launch-or-focus-contract
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# The graphical askpass, and the machines that have no graphics.
#
# config/bash/shell used to export SSH_ASKPASS=ksshaskpass with
# SSH_ASKPASS_REQUIRE=prefer unconditionally. `prefer` means ssh and git reach
# for that dialog BEFORE the terminal prompt, so on a server the prompt that
# would have worked was skipped in favour of a KDE window that cannot open:
#
# error: /usr/bin/ksshaskpass died of signal 6
# fatal: could not read Username for 'https://...'
#
# A `git push` from a headless box failed to ASK for a username rather than
# failing to authenticate -- and the shell environment is the one half of
# Panama a server does install, so nothing else was going to catch it.
#
# Pinned here: headless sessions never prefer a dialog, and a desktop with the
# binary present still gets one.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
shell_file="$repo_dir/config/bash/shell"
findings=()
note() { findings+=("$1"); }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/home"
# Sources the shell environment in a sandbox and reports one variable from it.
# HOME is empty so the personal env file is not read, and the session is
# non-interactive with no SSH_CONNECTION so the tmux hand-off cannot fire.
askpass_state() {
env -u DISPLAY -u WAYLAND_DISPLAY -u SSH_CONNECTION -u SSH_ASKPASS \
-u SSH_ASKPASS_REQUIRE "$@" \
HOME="$work/home" PANAMA_PATH="$repo_dir" \
bash -c 'source "$1" >/dev/null 2>&1
printf "%s|%s" "${SSH_ASKPASS:-}" "${SSH_ASKPASS_REQUIRE:-}"' \
_ "$shell_file"
}
# ── Headless is the case that broke ──────────────────────────────────────────
got="$(askpass_state)"
[[ "$got" == "|" ]] \
|| note "a headless session still prefers a graphical askpass (got '$got')"
got="$(askpass_state SSH_CONNECTION='10.0.0.1 22 10.0.0.2 22')"
[[ "$got" == "|" ]] \
|| note "an SSH session still prefers a graphical askpass (got '$got')"
# The case the first version of this contract missed: declining to SET the
# variables does nothing about ones already in the environment, inherited from
# a parent that had a display or from a login predating the guard. A fresh
# login shell on the repaired server still reported prefer until the headless
# branch cleared them outright, so the clearing is what gets pinned.
got="$(askpass_state SSH_ASKPASS=/usr/bin/ksshaskpass SSH_ASKPASS_REQUIRE=prefer)"
[[ "$got" == "|" ]] \
|| note "an inherited askpass survived a headless session (got '$got')"
# ── A desktop still gets its dialog ──────────────────────────────────────────
# Only assertable where the binary exists; saying so beats asserting that a
# machine without KDE installed fails to configure KDE's dialog.
if [[ -x /usr/bin/ksshaskpass ]]; then
got="$(askpass_state WAYLAND_DISPLAY=wayland-0)"
[[ "$got" == "/usr/bin/ksshaskpass|prefer" ]] \
|| note "a Wayland session does not prefer the graphical askpass (got '$got')"
got="$(askpass_state DISPLAY=:0)"
[[ "$got" == "/usr/bin/ksshaskpass|prefer" ]] \
|| note "an X session does not prefer the graphical askpass (got '$got')"
else
printf 'headless askpass contract: ksshaskpass is not installed, so the desktop half was not verified\n' >&2
fi
# ── Report ───────────────────────────────────────────────────────────────────
if (( ${#findings[@]} > 0 )); then
printf 'headless askpass contract FAILED:\n' >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'headless askpass contract passed\n'