-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbootstrap.sh
More file actions
executable file
·275 lines (251 loc) · 15.5 KB
/
Copy pathbootstrap.sh
File metadata and controls
executable file
·275 lines (251 loc) · 15.5 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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
#!/bin/sh
# abcd plugin bootstrap: provision $CLAUDE_PLUGIN_ROOT/abcd from the latest
# release whenever it is missing (itd-105 / spc-21).
#
# POSIX sh and the base system only: the abcd binary is exactly what is missing
# on a fresh plugin install and after every plugin update (the harness re-clones
# into a fresh commit-stamped cache directory), so nothing here may depend on it.
set -u
plugin_root="${CLAUDE_PLUGIN_ROOT:-}"
[ -n "$plugin_root" ] || exit 0
binary="$plugin_root/abcd"
lock="$plugin_root/.bootstrap.lock"
tmp=""
# The fetch origins are constants of this script, and there is deliberately no
# way to redirect them from the environment. An override here is not a
# convenience, it is a code-execution primitive: the binary AND the
# checksums.txt that verifies it are fetched from the same base, so whoever
# names the origin supplies both the payload and the manifest that "verifies"
# it — verification becomes vacuous — and the binary installed below is then run
# unattended as the Bash shell guard on every tool call. Two successive attempts
# to keep the seam behind a loopback allowlist were both defeated in adversarial
# review (URL userinfo of the form http://127.0.0.1:1@example.invalid slipped
# through the glob, and clearing the transport pin on the override path reopened
# redirect downgrade), so the seam is REMOVED rather than narrowed a third time.
# The tests rewrite these two literals in a throwaway COPY of this file; nothing
# in the shipped script consults the environment to decide where to fetch from.
repo_url="https://github.com/REPPL/abcd-cli"
releases_url="$repo_url/releases"
api_url="https://api.github.com/repos/REPPL/abcd-cli"
# Every fetch below is HTTPS-only, redirects included, unconditionally: there is
# no toggle and no code path that clears it. Without the pin a redirect could
# downgrade the transport (or point at file://) and hand both the payload and the
# manifest that verifies it to whoever can rewrite a response.
#
# The pin is not enough on its own, because curl reads a configuration surface
# this script's argv knows nothing about. Unless -q is its FIRST argument, curl
# loads $CURL_HOME/.curlrc (falling back to $HOME/.curlrc), and one `connect-to`
# or `resolve` line there re-points the connection while the URL above still
# literally reads https://github.com/… — the transport pin still holds, and the
# checksum check becomes vacuous, because the same config supplies the binary AND
# the checksums.txt that "verifies" it. Independent review reproduced exactly
# that. So: -q first on every call below, and the other names curl reads without
# being told to — the proxy variables, which can route both fetches through a
# server of the setter's choosing, and the CA overrides, which are what make such
# a route succeed on TLS — are removed here, before any fetch happens. The cost
# is deliberate and accepted: a machine that can only reach the network through a
# proxy no longer bootstraps automatically, and refuse() already names the
# manual install and build-from-source ways out.
unset HTTPS_PROXY https_proxy HTTP_PROXY http_proxy ALL_PROXY all_proxy CURL_HOME
unset CURL_CA_BUNDLE SSL_CERT_FILE SSL_CERT_DIR
cleanup() {
[ -n "$tmp" ] && rm -rf "$tmp"
rm -rf "$lock"
return 0
}
# safe strips the control characters a message must never carry to a terminal.
# The values interpolated into the messages below are not this script's own text
# — a plugin-root path, a release tag read off a redirect — and a raw escape
# sequence in one of them can recolour, reposition, or visually rewrite what the
# reader is shown. Tab and newline survive; the report is made of them.
safe() {
printf '%s' "$1" | tr -d '\000-\010\013-\037\177'
}
# refuse is the single failure message every failing path shares: what is
# missing, what it costs, and the three ways out. A raw shell error ("No such
# file or directory") must never be the whole story a user gets.
refuse() {
printf 'abcd bootstrap: %s\n\nThe abcd binary is not installed in the plugin root, so the abcd hooks cannot run and the shell-hazard guard is inactive — shell commands run UNGUARDED until it is.\n\nAny one of these fixes it:\n - start a session with network access, and this script retries by itself;\n - install the release binary by hand (%s#install) and copy it to %s;\n - build from source for full trust: go build ./cmd/abcd, then copy the binary to %s.\n' \
"$(safe "$1")" "$repo_url" "$(safe "$binary")" "$(safe "$binary")" >&2
exit 1
}
# notice is a reported condition rather than a fault: the install succeeded, or
# the platform has no released binary. It exits 2 because a SessionStart hook's
# stdout becomes model context while only a NON-ZERO exit puts its stderr in
# front of the human (the same reason `abcd hook session-start` returns 2 for its
# own notices), and because SessionStart treats a non-zero exit as non-blocking:
# the later hooks in this event still run.
notice() {
printf '%s\n' "$(safe "$1")" >&2
exit 2
}
# 1. Fast path: a steady-state session pays one file test and no network. A
# regular file, not merely something with an execute bit — a directory is
# executable too (matching internal/core/ahoy's isExecutableFile).
[ -f "$binary" ] && [ -x "$binary" ] && exit 0
# `mv -f` onto an existing DIRECTORY moves the file INTO it rather than
# replacing it, so a stray directory at $binary would otherwise "succeed"
# every session while the hooks stay broken and .binary-meta lies about it.
if [ -e "$binary" ] && [ ! -f "$binary" ]; then
refuse "$binary exists and is not a regular file, so the release binary cannot be installed there"
fi
# 2. Platform gate. An unsupported platform is a reported condition, not a hook
# fault, so it changes nothing and never blocks — but it is still said out
# loud, because the shell guard stays inactive for as long as it holds.
os=$(uname -s 2>/dev/null | tr '[:upper:]' '[:lower:]')
arch=$(uname -m 2>/dev/null)
case "$arch" in
x86_64 | amd64) arch=amd64 ;;
aarch64 | arm64) arch=arm64 ;;
*) arch='' ;;
esac
case "$os" in
darwin | linux) ;;
*) os='' ;;
esac
if [ -z "$os" ] || [ -z "$arch" ]; then
notice "$(printf 'abcd bootstrap: no abcd binary is released for this platform (%s %s). Released binaries cover darwin and linux on amd64 and arm64 only, so nothing was downloaded and nothing was changed. To run abcd here, build from source: go build ./cmd/abcd, then copy the binary to %s.' \
"$(uname -s 2>/dev/null)" "$(uname -m 2>/dev/null)" "$binary")"
fi
# 3. Concurrency lock. mkdir is atomic on POSIX, so the loser of the race is the
# process whose mkdir fails; it exits quietly rather than racing the winner
# into the same temp dir. A lock older than ten minutes belongs to a run that
# was killed — without breaking it the plugin root stays unprovisionable.
if ! mkdir "$lock" 2>/dev/null; then
if [ -n "$(find "$lock" -maxdepth 0 -mmin +10 2>/dev/null)" ]; then
rm -rf "$lock"
fi
if ! mkdir "$lock" 2>/dev/null; then
# A lock DIRECTORY that exists is the race: another run holds it, and
# staying quiet is right. mkdir failing with no lock directory there is
# something else — a read-only or unwritable plugin root, or a
# non-directory squatting the lock path — and that is a permanent,
# every-session failure the two cases must not share a silent exit with.
[ -d "$lock" ] && exit 0
refuse "the bootstrap lock ($lock) cannot be created and no lock directory is there, so this is not a concurrent run — the plugin root ($plugin_root) is not writable, or something that is not a directory occupies the lock path"
fi
fi
# A signal trap that RETURNS resumes the script (POSIX), which would carry on
# against directories cleanup just deleted and report a checksum mismatch that
# never happened. Terminate explicitly; cleanup is idempotent, so the EXIT trap
# firing again after it is a no-op.
trap cleanup EXIT
trap 'cleanup; exit 1' HUP INT TERM
# SIGKILL runs no trap, so a killed run leaves its PID-stamped temp directory
# behind holding a partially downloaded, UNVERIFIED binary. The lock is held from
# here on, so sweeping them cannot touch a live run's directory.
rm -rf "$plugin_root"/.bootstrap.tmp.* 2>/dev/null
command -v curl >/dev/null 2>&1 ||
refuse 'curl is not available, so the release binary cannot be downloaded'
# 4. Resolve the release TAG before fetching anything from that release, so the
# binary and the manifest that verifies it are pinned to ONE release: a
# release cut between the two downloads would otherwise check a new
# checksums.txt against an old binary and refuse a perfectly good artefact.
# /releases/latest answers 302 -> /releases/tag/<tag>, and %{redirect_url} on
# an UNFOLLOWED request is the only URL shape that survives: %{url_effective}
# after -L lands on the asset CDN (release-assets.githubusercontent.com/...),
# which carries no tag segment at all.
redirect=$(curl -q -fsS --proto '=https' --proto-redir '=https' --max-time 15 -o /dev/null -w '%{redirect_url}' "$releases_url/latest" 2>/dev/null) || redirect=''
release_tag=$(printf '%s\n' "$redirect" | sed -n 's|.*/releases/tag/\([^/?#]*\).*|\1|p')
[ -n "$release_tag" ] ||
refuse 'the latest release tag could not be resolved, so the download cannot be pinned to a single release — there may be no network'
# 5. Download into a temp dir under the plugin root — same filesystem, so the
# install below is a rename and never a half-written binary.
asset="abcd-$os-$arch"
download_url="$releases_url/download/$release_tag"
tmp="$plugin_root/.bootstrap.tmp.$$"
rm -rf "$tmp"
# `mkdir -p` succeeds on a directory that already exists — including one that
# reappeared, symlinked, in the window since the rm -rf above. Plain `mkdir`
# fails on that, turning a same-name race into a refusal instead of a curl
# write through a planted symlink.
mkdir "$tmp" 2>/dev/null ||
refuse "a temporary directory cannot be created in the plugin root ($plugin_root)"
curl -q -fsSL --proto '=https' --proto-redir '=https' --max-time 120 -o "$tmp/$asset" "$download_url/$asset" 2>/dev/null ||
refuse "downloading $asset from release $release_tag failed — there may be no network, or that release may carry no asset for this platform"
curl -q -fsSL --proto '=https' --proto-redir '=https' --max-time 30 -o "$tmp/checksums.txt" "$download_url/checksums.txt" 2>/dev/null ||
refuse "downloading checksums.txt from release $release_tag failed, so the download cannot be verified and is not installed"
# 6. Verification against the same-origin manifest.
line=$(grep " $asset\$" "$tmp/checksums.txt" 2>/dev/null | head -n 1)
[ -n "$line" ] ||
refuse "the release checksums.txt lists no entry for $asset, so the download cannot be verified and is not installed"
printf '%s\n' "$line" > "$tmp/manifest.txt"
if command -v shasum >/dev/null 2>&1; then
verify='shasum -a 256 -c manifest.txt'
elif command -v sha256sum >/dev/null 2>&1; then
verify='sha256sum -c manifest.txt'
else
refuse 'neither shasum nor sha256sum is available, so the download cannot be verified and is not installed'
fi
(cd "$tmp" && $verify) > /dev/null 2>&1 ||
refuse "the downloaded $asset does not match its SHA-256 checksum in the release checksums.txt — the artefact is corrupted or is not the published one, so nothing was installed"
# The hash just verified is RECORDED below, not re-checked anywhere: nothing in
# this repository reads binary_sha256 today. It is provenance for a human (or a
# later verb) answering "is the binary at the guard path the one that was
# verified, or did something replace it" — the fast path deliberately does not
# recompute it, because its whole contract is that a steady-state session pays
# one file test and no more. Do not read this line as an automatic check.
binary_sha256=$(printf '%s\n' "$line" | sed -n 's/^\([0-9a-fA-F]\{64\}\).*/\1/p' | tr 'ABCDEF' 'abcdef')
[ -n "$binary_sha256" ] || binary_sha256=unknown
# The release commit is read from the API when it answers, and left unknown
# otherwise: the meta file is the skew notice's only evidence, so it never
# records a value it did not resolve.
release_sha=unknown
body=$(curl -q -fsSL --proto '=https' --proto-redir '=https' --max-time 15 -H 'Accept: application/vnd.github+json' "$api_url/commits/$release_tag" 2>/dev/null) || body=''
candidate=$(printf '%s\n' "$body" | tr ',' '\n' |
sed -n 's/.*"sha"[[:space:]]*:[[:space:]]*"\([0-9a-f]\{40\}\)".*/\1/p' | head -n 1)
[ -n "$candidate" ] && release_sha="$candidate"
# 7. Install, then record provenance.
chmod 0755 "$tmp/$asset" 2>/dev/null ||
refuse "the downloaded $asset cannot be made executable"
mv -f "$tmp/$asset" "$binary" 2>/dev/null ||
refuse "the verified $asset cannot be installed at $binary"
# plugin_sha is the harness's commit stamp: the plugin cache directory is named
# for the source commit it was cloned from. Anything else is not a commit and is
# recorded as such.
#
# That naming is a WARRANT this repository takes from itd-105 and cannot verify
# against the real harness. If it ever stops holding, every basename fails the
# 40-hex gate, plugin_sha is permanently `unknown`, and the version-skew notice
# goes silent forever with nothing anywhere to look at. So the RAW basename is
# recorded beside the gated value: it is never compared and never rendered, it
# exists so that "why has the skew notice never fired" has an answer in the file
# rather than only in this comment. Control characters are stripped (a directory
# name may contain a newline, which would forge a key=value line) and the value
# is capped, so a pathological name cannot push .binary-meta past the guarded
# read size and silence the notice by a different route.
plugin_root_basename=$(basename "$plugin_root" | tr -d '\000-\037' | cut -c1-120)
plugin_sha="$plugin_root_basename"
case "$plugin_sha" in
*[!0-9a-f]*) plugin_sha=unknown ;;
esac
[ "${#plugin_sha}" -eq 40 ] || plugin_sha=unknown
# Written into the temp dir and renamed in, on the same filesystem, for the same
# reason the binary is: a crash mid-write would otherwise leave a truncated value
# that still parses, and a skew notice rendered off a truncated commit is a lie.
meta_path="$plugin_root/.binary-meta"
meta_note=''
if [ -e "$meta_path" ] && [ ! -f "$meta_path" ]; then
# The same `mv -f` hazard the binary path refuses above, and quieter here: a
# DIRECTORY at this path swallows the record as .binary-meta/binary-meta,
# where nothing reads it, while the notice below would claim provenance was
# recorded. The install itself is genuine and is not undone for this — it is
# reported instead, in the words the other write failure uses.
meta_note=' (the .binary-meta provenance record could not be written because that path exists and is not a regular file, so version-skew reporting stays silent for this plugin root)'
else
{
printf 'release_tag=%s\n' "$release_tag"
printf 'release_sha=%s\n' "$release_sha"
printf 'binary_sha256=%s\n' "$binary_sha256"
printf 'fetched_at=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
printf 'plugin_sha=%s\n' "$plugin_sha"
printf 'plugin_root_basename=%s\n' "$plugin_root_basename"
} > "$tmp/binary-meta" 2>/dev/null &&
mv -f "$tmp/binary-meta" "$meta_path" 2>/dev/null ||
meta_note=' (the .binary-meta provenance record could not be written, so version-skew reporting stays silent for this plugin root)'
fi
# The one place PATH setup is suggested; the symlink itself stays owned by ahoy.
# This prints once per plugin root, because every later session takes the fast
# path above.
notice "$(printf 'abcd bootstrap: installed the checksum-verified abcd binary (release %s) into the plugin root, so the abcd hooks are live for this session. For the abcd command in your own terminal, run `abcd ahoy install` once.%s' \
"$release_tag" "$meta_note")"