Repository navigation
Expand file tree
/
Copy pathwrite-compose-ssh-agent-socket
More file actions
executable file
·249 lines (222 loc) · 8.79 KB
/
Copy pathwrite-compose-ssh-agent-socket
File metadata and controls
executable file
·249 lines (222 loc) · 8.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
#!/bin/sh
# Run from initializeCommand.sh (before the container starts).
# VS Code, Cursor, and `devcontainer up` invoke this on the host or from an
# outer devcontainer that manages nested devcontainers via docker.sock.
#
# Ensures a host ssh-agent socket (private keys stay on the host) and loads
# default SSH keys when possible. Writes compose.ssh-agent-socket.yaml with
# that socket. Inside a devcontainer the written path is HOST_SSH_AUTH_SOCK
# from the enclosing devcontainer's compose.ssh-agent-socket.yaml.
set -eu
cd "$(dirname "$0")/../../.."
SUPER_PROJECTS_NAME=super_projects
# Name from docker_compose_configuration_customizations/naming/compose.naming.override.yaml when
# that file exists, otherwise compose.naming.yaml in the same folder. The first
# name: line is the Compose name.
_naming_file=.devcontainer/docker_compose_configuration_customizations/naming/compose.naming.yaml
if [ -f .devcontainer/docker_compose_configuration_customizations/naming/compose.naming.override.yaml ]; then
_naming_file=.devcontainer/docker_compose_configuration_customizations/naming/compose.naming.override.yaml
fi
if _line=$(grep -m1 '^name:' "$_naming_file"); then
SUPER_PROJECTS_NAME=$(printf '%s' "${_line#name:}" | tr -d ' "\r')
fi
# WSLg's XDG_RUNTIME_DIR can leave a directory at the socket path; use ~/.cache on WSL.
# Socket filename follows the Compose name.
case "${XDG_RUNTIME_DIR:-}" in
/mnt/wslg/*) MANAGED_AGENT_SOCK="$HOME/.cache/${SUPER_PROJECTS_NAME}-ssh-agent.sock" ;;
*) MANAGED_AGENT_SOCK="${XDG_RUNTIME_DIR:-$HOME/.cache}/${SUPER_PROJECTS_NAME}-ssh-agent.sock" ;;
esac
# Docker bind-mounts create a directory at a missing source path; if compose ever
# ran before the agent socket existed, clear that directory so ssh-agent can bind.
[ -d "$MANAGED_AGENT_SOCK" ] && rm -rf "$MANAGED_AGENT_SOCK"
mkdir -p "$(dirname "$MANAGED_AGENT_SOCK")"
known_hosts_writable() {
[ -e "$HOME/.ssh/known_hosts" ] && [ ! -w "$HOME/.ssh/known_hosts" ] && return 1
return 0
}
prepare_ssh_config_dir() {
mkdir -p "$HOME/.ssh"
touch "$HOME/.ssh/known_hosts"
chmod 700 "$HOME/.ssh" 2>/dev/null || true
}
if known_hosts_writable; then
prepare_ssh_config_dir
else
echo "SSH: $HOME/.ssh/known_hosts is read-only; skipping local ssh config setup" >&2
fi
HOST_OS=$(uname -s)
DEFAULT_KEY_PATHS="
$HOME/.ssh/id_ed25519
$HOME/.ssh/id_rsa
$HOME/.ssh/id_ecdsa
$HOME/.ssh/id_ed25519_sk
$HOME/.ssh/id_ecdsa_sk
"
agent_has_loaded_keys() {
_socket_path=$1
[ -S "$_socket_path" ] || return 1
SSH_AUTH_SOCK="$_socket_path" ssh-add -l >/dev/null 2>&1
}
agent_is_running() {
_socket_path=$1
[ -S "$_socket_path" ] || return 1
_ssh_add_output=$(SSH_AUTH_SOCK="$_socket_path" ssh-add -l 2>&1) && return 0
echo "$_ssh_add_output" | grep -qi 'no identities' && return 0
return 1
}
# Prefer an agent that already has unlocked keys (zero prompt).
SELECTED_AGENT_SOCK=
if [ -n "${SSH_AUTH_SOCK:-}" ] && agent_has_loaded_keys "$SSH_AUTH_SOCK"; then
SELECTED_AGENT_SOCK=$SSH_AUTH_SOCK
fi
if [ -z "$SELECTED_AGENT_SOCK" ]; then
set -- \
"$HOME/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock" \
"$HOME/.1password/agent.sock"
for _onepassword_sock in "$@"; do
if agent_has_loaded_keys "$_onepassword_sock"; then
SELECTED_AGENT_SOCK=$_onepassword_sock
break
fi
done
fi
if [ -z "$SELECTED_AGENT_SOCK" ] && agent_has_loaded_keys "$MANAGED_AGENT_SOCK"; then
SELECTED_AGENT_SOCK=$MANAGED_AGENT_SOCK
fi
# No unlocked keys yet: use our managed socket so loads go to the path we forward.
if [ -z "$SELECTED_AGENT_SOCK" ]; then
if agent_is_running "$MANAGED_AGENT_SOCK"; then
SELECTED_AGENT_SOCK=$MANAGED_AGENT_SOCK
else
# Path is unusable: Docker may have left a directory, or WSL/reboot may have
# left a dead socket. ssh-agent -a cannot bind until the leftover is gone.
if [ -d "$MANAGED_AGENT_SOCK" ]; then
rm -rf "$MANAGED_AGENT_SOCK"
elif [ -e "$MANAGED_AGENT_SOCK" ]; then
rm -f "$MANAGED_AGENT_SOCK"
fi
ssh-agent -a "$MANAGED_AGENT_SOCK" >/dev/null
SELECTED_AGENT_SOCK=$MANAGED_AGENT_SOCK
fi
fi
export SSH_AUTH_SOCK=$SELECTED_AGENT_SOCK
add_key_without_prompt() {
_key_path=$1
[ -f "$_key_path" ] || return 0
if [ "$HOST_OS" = Darwin ]; then
ssh-add --apple-use-keychain "$_key_path" </dev/null >/dev/null 2>&1 || true
else
ssh-add -q "$_key_path" </dev/null >/dev/null 2>&1 || true
fi
}
add_key_with_prompt() {
_key_path=$1
[ -f "$_key_path" ] || return 0
if [ "$HOST_OS" = Darwin ]; then
echo "SSH: unlock $_key_path (macOS Keychain)..." >&2
ssh-add --apple-use-keychain "$_key_path" >&2 || true
else
echo "SSH: unlock $_key_path..." >&2
ssh-add "$_key_path" >&2 || true
fi
}
# 1) Non-interactive / Keychain cache hits
for _key_path in $DEFAULT_KEY_PATHS; do
add_key_without_prompt "$_key_path"
done
# 2) Still empty: prompt as part of initializeCommand (not a separate user-run script)
if ! agent_has_loaded_keys "$SELECTED_AGENT_SOCK"; then
_found_default_key=0
for _key_path in $DEFAULT_KEY_PATHS; do
[ -f "$_key_path" ] || continue
_found_default_key=1
break
done
if [ "$_found_default_key" -eq 1 ]; then
if [ -t 0 ] || [ -t 1 ]; then
echo "SSH: no identities in the host agent yet; unlock keys for this devcontainer..." >&2
for _key_path in $DEFAULT_KEY_PATHS; do
add_key_with_prompt "$_key_path"
done
elif [ "$HOST_OS" = Darwin ]; then
# Cursor/VS Code initializeCommand often has no TTY; Keychain may still present UI.
echo "SSH: attempting Keychain unlock (may show a system prompt)..." >&2
for _key_path in $DEFAULT_KEY_PATHS; do
[ -f "$_key_path" ] || continue
ssh-add --apple-use-keychain "$_key_path" >&2 || true
done
else
_askpass_program=
if command -v ssh-askpass >/dev/null 2>&1; then
_askpass_program=$(command -v ssh-askpass)
elif command -v ksshaskpass >/dev/null 2>&1; then
_askpass_program=$(command -v ksshaskpass)
elif command -v lxqt-openssh-askpass >/dev/null 2>&1; then
_askpass_program=$(command -v lxqt-openssh-askpass)
fi
if [ -n "$_askpass_program" ] && { [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; }; then
echo "SSH: unlocking keys via askpass (system prompt)..." >&2
for _key_path in $DEFAULT_KEY_PATHS; do
[ -f "$_key_path" ] || continue
SSH_ASKPASS=$_askpass_program SSH_ASKPASS_REQUIRE=force ssh-add "$_key_path" </dev/null >&2 || true
done
fi
fi
fi
fi
if agent_has_loaded_keys "$SELECTED_AGENT_SOCK"; then
echo "SSH: host agent ready ($(SSH_AUTH_SOCK=$SELECTED_AGENT_SOCK ssh-add -l 2>/dev/null | wc -l | tr -d ' ') key(s))" >&2
elif [ -n "${SSH_AUTH_SOCK:-}" ]; then
echo "SSH: host agent has no identities yet." >&2
echo " Reopen/rebuild may prompt again, or enable 1Password SSH agent / macOS Keychain." >&2
echo " GitHub HTTPS alternative: gh auth login (inside the container)." >&2
fi
COMPOSE_FILE=.devcontainer/docker_compose_configuration_customizations/ssh_agent_socket/compose.ssh-agent-socket.yaml
ENCLOSING_COMPOSE=../.devcontainer/docker_compose_configuration_customizations/ssh_agent_socket/compose.ssh-agent-socket.yaml
validate_bind_path() {
case "$1" in
/*) ;;
*) echo "ssh agent socket: path must be absolute: $1" >&2; exit 1 ;;
esac
case "$1" in
*:*) echo "ssh agent socket: path must not contain ':': $1" >&2; exit 1 ;;
esac
}
compose_env_value() {
_line=$(grep -m1 -E "^[[:space:]]*${1}:" "$2") || return 1
_line=${_line#*:}
printf '%s' "$_line" | sed 's/^[[:space:]]*//; s/^"//; s/"$//; s/\$\$/$/g'
}
compose_yaml_quote() {
printf '"%s"' "$(printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\$/$$/g')"
}
if [ ! -f /.dockerenv ]; then
COMPOSE_SSH_AUTH_SOCK=$SELECTED_AGENT_SOCK
else
echo "ssh agent socket: resolving Docker host path from $ENCLOSING_COMPOSE" >&2
if [ ! -f "$ENCLOSING_COMPOSE" ]; then
echo "ssh agent socket: expected $ENCLOSING_COMPOSE" >&2
exit 1
fi
COMPOSE_SSH_AUTH_SOCK=$(compose_env_value HOST_SSH_AUTH_SOCK "$ENCLOSING_COMPOSE") || COMPOSE_SSH_AUTH_SOCK=
if [ -z "$COMPOSE_SSH_AUTH_SOCK" ]; then
echo "ssh agent socket: $ENCLOSING_COMPOSE has no HOST_SSH_AUTH_SOCK" >&2
exit 1
fi
fi
validate_bind_path "$COMPOSE_SSH_AUTH_SOCK"
_env=$(compose_yaml_quote "$COMPOSE_SSH_AUTH_SOCK")
_mount=$(compose_yaml_quote "$COMPOSE_SSH_AUTH_SOCK:/ssh-agent.sock")
{
printf '%s\n' \
'# Written by write-compose-ssh-agent-socket. Do not edit.' \
'services:' \
' devcontainer:' \
' environment:' \
" HOST_SSH_AUTH_SOCK: ${_env}" \
' volumes:' \
" - ${_mount}"
} >"$COMPOSE_FILE"
case "$(docker info --format '{{.OperatingSystem}}' 2>/dev/null)" in *Docker\ Desktop*)
[ -S "$COMPOSE_SSH_AUTH_SOCK" ] && docker run --rm -v "$COMPOSE_SSH_AUTH_SOCK:/ssh-agent.sock:ro" alpine true >/dev/null 2>&1 || true
;; esac