[zsh] Make ssh loading across platform more robust
This commit is contained in:
130
bin/.local/bin/ssh-agent-doctor
Executable file
130
bin/.local/bin/ssh-agent-doctor
Executable file
@ -0,0 +1,130 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
ssh-agent-doctor: diagnose and select the best SSH_AUTH_SOCK.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ssh-agent-doctor # human-readable report
|
||||||
|
ssh-agent-doctor --export # print: export SSH_AUTH_SOCK='...'
|
||||||
|
ssh-agent-doctor --json # minimal JSON-ish output (no jq required)
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
0 OK (agent reachable)
|
||||||
|
1 Socket found but agent has no identities (reachable)
|
||||||
|
2 No usable socket found OR cannot connect to agent
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
mode="report"
|
||||||
|
case "${1:-}" in
|
||||||
|
--export) mode="export" ;;
|
||||||
|
--json) mode="json" ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
"") ;;
|
||||||
|
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
os="$(uname -s 2>/dev/null || echo unknown)"
|
||||||
|
uid="$(id -u)"
|
||||||
|
user="$(id -un 2>/dev/null || echo unknown)"
|
||||||
|
home="${HOME:-/}"
|
||||||
|
|
||||||
|
linux_sock="/run/user/${uid}/ssh-agent.socket"
|
||||||
|
darwin_sock="${home}/.ssh/agent.sock"
|
||||||
|
env_sock="${SSH_AUTH_SOCK:-}"
|
||||||
|
|
||||||
|
is_sock() { [[ -n "${1:-}" && -S "$1" ]]; }
|
||||||
|
|
||||||
|
pick_sock=""
|
||||||
|
pick_reason=""
|
||||||
|
|
||||||
|
if is_sock "$linux_sock"; then
|
||||||
|
pick_sock="$linux_sock"
|
||||||
|
pick_reason="linux_systemd_default"
|
||||||
|
elif is_sock "$darwin_sock"; then
|
||||||
|
pick_sock="$darwin_sock"
|
||||||
|
pick_reason="darwin_launchd_default"
|
||||||
|
elif is_sock "$env_sock"; then
|
||||||
|
pick_sock="$env_sock"
|
||||||
|
pick_reason="existing_env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
check_agent() {
|
||||||
|
local sock="$1"
|
||||||
|
SSH_AUTH_SOCK="$sock" ssh-add -l >/dev/null 2>&1
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$mode" == "export" ]]; then
|
||||||
|
if [[ -z "$pick_sock" ]]; then
|
||||||
|
echo "echo 'ERROR: No ssh-agent socket found.' >&2"
|
||||||
|
echo "return 2 2>/dev/null || exit 2"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
printf "export SSH_AUTH_SOCK='%s'\n" "$pick_sock"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$mode" == "json" ]]; then
|
||||||
|
if [[ -z "$pick_sock" ]]; then
|
||||||
|
cat <<EOF
|
||||||
|
{"ok":false,"user":"$user","uid":$uid,"os":"$os","picked":"","reason":"","error":"no_socket","candidates":{"linux":"$linux_sock","darwin":"$darwin_sock","env":"$env_sock"}}
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
check_agent "$pick_sock" || rc=$?
|
||||||
|
# ssh-add -l exit codes: 0 has keys, 1 no keys, 2 cannot connect
|
||||||
|
ok=false
|
||||||
|
status="unknown"
|
||||||
|
if [[ $rc -eq 0 ]]; then ok=true; status="has_identities"; fi
|
||||||
|
if [[ $rc -eq 1 ]]; then ok=true; status="no_identities"; fi
|
||||||
|
if [[ $rc -eq 2 ]]; then ok=false; status="cannot_connect"; fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
{"ok":$ok,"user":"$user","uid":$uid,"os":"$os","picked":"$pick_sock","reason":"$pick_reason","ssh_add_l_exit":$rc,"status":"$status","candidates":{"linux":"$linux_sock","darwin":"$darwin_sock","env":"$env_sock"}}
|
||||||
|
EOF
|
||||||
|
exit $rc
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Human report mode
|
||||||
|
echo "ssh-agent-doctor"
|
||||||
|
echo " user: $user (uid $uid)"
|
||||||
|
echo " os: $os"
|
||||||
|
echo
|
||||||
|
echo "Candidates:"
|
||||||
|
printf " linux systemd: %s %s\n" "$linux_sock" "$(is_sock "$linux_sock" && echo '[socket]' || echo '[missing]')"
|
||||||
|
printf " macOS launchd: %s %s\n" "$darwin_sock" "$(is_sock "$darwin_sock" && echo '[socket]' || echo '[missing]')"
|
||||||
|
printf " env SSH_AUTH_SOCK: %s %s\n" "${env_sock:-<unset>}" "$(is_sock "$env_sock" && echo '[socket]' || echo '[not socket]')"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ -z "$pick_sock" ]]; then
|
||||||
|
echo "Result: ❌ No usable SSH agent socket found."
|
||||||
|
echo "Hint: start your agent (systemd user on Linux, launchd agent on macOS) and re-run."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Picked:"
|
||||||
|
echo " SSH_AUTH_SOCK=$pick_sock"
|
||||||
|
echo " reason=$pick_reason"
|
||||||
|
echo
|
||||||
|
|
||||||
|
rc=0
|
||||||
|
check_agent "$pick_sock" || rc=$?
|
||||||
|
|
||||||
|
if [[ $rc -eq 0 ]]; then
|
||||||
|
echo "Agent check: ✅ reachable, has identities"
|
||||||
|
SSH_AUTH_SOCK="$pick_sock" ssh-add -l || true
|
||||||
|
exit 0
|
||||||
|
elif [[ $rc -eq 1 ]]; then
|
||||||
|
echo "Agent check: ✅ reachable, but has no identities loaded"
|
||||||
|
echo "Next: run your pass-based loader (load_keys)."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Agent check: ❌ cannot connect to agent via that socket"
|
||||||
|
echo "This usually means the socket is stale or the agent died."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
32
bin/.local/bin/ssh-agent-env
Executable file
32
bin/.local/bin/ssh-agent-env
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
uid="$(id -u)"
|
||||||
|
|
||||||
|
linux_sock="/run/user/${uid}/ssh-agent.socket"
|
||||||
|
darwin_sock="${HOME}/.ssh/agent.sock"
|
||||||
|
|
||||||
|
pick_sock=""
|
||||||
|
|
||||||
|
# Prefer Linux systemd socket if present
|
||||||
|
if [[ -S "$linux_sock" ]]; then
|
||||||
|
pick_sock="$linux_sock"
|
||||||
|
elif [[ -S "$darwin_sock" ]]; then
|
||||||
|
pick_sock="$darwin_sock"
|
||||||
|
else
|
||||||
|
# Fall back to existing env if it's a socket
|
||||||
|
if [[ -n "${SSH_AUTH_SOCK:-}" && -S "${SSH_AUTH_SOCK}" ]]; then
|
||||||
|
pick_sock="$SSH_AUTH_SOCK"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$pick_sock" ]]; then
|
||||||
|
echo "echo 'ERROR: No ssh-agent socket found.' >&2"
|
||||||
|
echo "echo 'Tried: $linux_sock and $darwin_sock' >&2"
|
||||||
|
echo "return 2 2>/dev/null || exit 2"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
export SSH_AUTH_SOCK='$pick_sock'
|
||||||
|
EOF
|
||||||
@ -8,11 +8,17 @@
|
|||||||
(after! envrc
|
(after! envrc
|
||||||
(envrc-global-mode))
|
(envrc-global-mode))
|
||||||
|
|
||||||
;; Force systemd user ssh-agent socket for all Emacs subprocesses.
|
;; Force user ssh-agent socket for all Emacs subprocesses.
|
||||||
(let* ((xdg (format "/run/user/%d" (user-uid)))
|
(defun my/apply-ssh-agent-env ()
|
||||||
(sock (expand-file-name "ssh-agent.socket" xdg)))
|
"Set SSH_AUTH_SOCK in Emacs using ~/.local/bin/ssh-agent-env."
|
||||||
(setenv "XDG_RUNTIME_DIR" xdg)
|
(let* ((cmd (expand-file-name "~/.local/bin/ssh-agent-env"))
|
||||||
(setenv "SSH_AUTH_SOCK" sock))
|
(out (and (file-executable-p cmd)
|
||||||
|
(string-trim (shell-command-to-string cmd)))))
|
||||||
|
;; out looks like: export SSH_AUTH_SOCK='...'
|
||||||
|
(when (and out (string-match "SSH_AUTH_SOCK='\\([^']+\\)'" out))
|
||||||
|
(setenv "SSH_AUTH_SOCK" (match-string 1 out)))))
|
||||||
|
|
||||||
|
(my/apply-ssh-agent-env)
|
||||||
|
|
||||||
;; load pinentry
|
;; load pinentry
|
||||||
(when (require 'pinentry nil t)
|
(when (require 'pinentry nil t)
|
||||||
@ -448,33 +454,22 @@ Always open the result in `eww`."
|
|||||||
|
|
||||||
;; Bind globally to C-c l
|
;; Bind globally to C-c l
|
||||||
(global-set-key (kbd "C-c l") #'life-scrobble-url)
|
(global-set-key (kbd "C-c l") #'life-scrobble-url)
|
||||||
|
|
||||||
(after! magit
|
(after! magit
|
||||||
(defvar my/ssh-key-injector-script (expand-file-name "~/.local/bin/load_keys"))
|
(defvar my/ssh-key-injector-script (expand-file-name "~/.local/bin/load_keys"))
|
||||||
|
|
||||||
(defun my/systemd-ssh-auth-sock ()
|
|
||||||
(format "/run/user/%d/ssh-agent.socket" (user-uid)))
|
|
||||||
|
|
||||||
(defun my/ssh-add-status ()
|
(defun my/ssh-add-status ()
|
||||||
"ssh-add -l exit code: 0=has keys, 1=no keys, 2=no agent."
|
|
||||||
(call-process "ssh-add" nil nil nil "-l"))
|
(call-process "ssh-add" nil nil nil "-l"))
|
||||||
|
|
||||||
(defun my/run-ssh-key-injector ()
|
(defun my/run-ssh-key-injector ()
|
||||||
|
(my/apply-ssh-agent-env)
|
||||||
(let* ((buf (get-buffer-create "*ssh-key-injector*"))
|
(let* ((buf (get-buffer-create "*ssh-key-injector*"))
|
||||||
(sock (my/systemd-ssh-auth-sock))
|
(process-connection-type t)
|
||||||
(process-connection-type t) ;; PTY for pinentry-curses
|
|
||||||
(process-environment (copy-sequence process-environment)))
|
(process-environment (copy-sequence process-environment)))
|
||||||
(setenv "SSH_AUTH_SOCK" sock)
|
|
||||||
(setenv "XDG_RUNTIME_DIR" (format "/run/user/%d" (user-uid)))
|
|
||||||
|
|
||||||
(unless (file-executable-p my/ssh-key-injector-script)
|
(unless (file-executable-p my/ssh-key-injector-script)
|
||||||
(user-error "SSH injector script not executable: %s" my/ssh-key-injector-script))
|
(user-error "SSH injector script not executable: %s" my/ssh-key-injector-script))
|
||||||
|
|
||||||
(with-current-buffer buf
|
(with-current-buffer buf
|
||||||
(erase-buffer)
|
(erase-buffer)
|
||||||
(insert (format "Emacs SSH_AUTH_SOCK=%s\n" (getenv "SSH_AUTH_SOCK")))
|
(insert (format "Emacs SSH_AUTH_SOCK=%s\n\n" (or (getenv "SSH_AUTH_SOCK") "<unset>"))))
|
||||||
(insert (format "Emacs XDG_RUNTIME_DIR=%s\n\n" (getenv "XDG_RUNTIME_DIR"))))
|
|
||||||
|
|
||||||
(let ((proc (make-process
|
(let ((proc (make-process
|
||||||
:name "ssh-key-injector"
|
:name "ssh-key-injector"
|
||||||
:buffer buf
|
:buffer buf
|
||||||
@ -483,16 +478,15 @@ Always open the result in `eww`."
|
|||||||
:noquery t)))
|
:noquery t)))
|
||||||
(while (process-live-p proc)
|
(while (process-live-p proc)
|
||||||
(accept-process-output proc 0.05))
|
(accept-process-output proc 0.05))
|
||||||
(let ((exit (process-exit-status proc)))
|
(unless (eq (process-exit-status proc) 0)
|
||||||
(unless (eq exit 0)
|
(display-buffer buf)
|
||||||
(display-buffer buf)
|
(user-error "SSH key injection failed. See *ssh-key-injector*.")))))
|
||||||
(user-error "SSH key injection failed (exit %d). See *ssh-key-injector*." exit))))))
|
|
||||||
|
|
||||||
(defun my/ensure-ssh-keys-loaded (&rest _ignore)
|
(defun my/ensure-ssh-keys-loaded (&rest _ignore)
|
||||||
(pcase (my/ssh-add-status)
|
(pcase (my/ssh-add-status)
|
||||||
(0 nil) ;; already has identities
|
(0 nil)
|
||||||
(1 (my/run-ssh-key-injector)) ;; agent reachable but empty
|
(1 (my/run-ssh-key-injector))
|
||||||
(2 (user-error "No reachable ssh-agent. SSH_AUTH_SOCK=%s"
|
(2 (user-error "No reachable ssh-agent (SSH_AUTH_SOCK=%s)"
|
||||||
(or (getenv "SSH_AUTH_SOCK") "<unset>")))
|
(or (getenv "SSH_AUTH_SOCK") "<unset>")))
|
||||||
(_ (my/run-ssh-key-injector))))
|
(_ (my/run-ssh-key-injector))))
|
||||||
|
|
||||||
|
|||||||
25
launchd/Library/LaunchAgents/com.powellc.ssh-agent.service
Normal file
25
launchd/Library/LaunchAgents/com.powellc.ssh-agent.service
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.user.ssh-agent</string>
|
||||||
|
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/bin/ssh-agent</string>
|
||||||
|
<string>-D</string>
|
||||||
|
<string>-a</string>
|
||||||
|
<string>$HOME/.ssh/agent.sock</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -7,6 +7,8 @@ export ZSH="$HOME/.oh-my-zsh"
|
|||||||
ZSH_THEME="robbyrussell"
|
ZSH_THEME="robbyrussell"
|
||||||
|
|
||||||
plugins=(git z fzf asdf direnv emacs yarn aws)
|
plugins=(git z fzf asdf direnv emacs yarn aws)
|
||||||
|
|
||||||
|
eval "$($HOME/.local/bin/ssh-agent-env)"
|
||||||
load_keys &>/dev/null
|
load_keys &>/dev/null
|
||||||
|
|
||||||
source $ZSH/oh-my-zsh.sh
|
source $ZSH/oh-my-zsh.sh
|
||||||
|
|||||||
Reference in New Issue
Block a user