Skip to content

Commit 7f25adc

Browse files
Your Nameclaude
andcommitted
feat(launcher): commit a prebuilt binary via LFS to close the cold-start race
docs/cloud-environment-setup.md documents that on Claude Code on the web, the MCP client dials configured servers concurrently with (not after) SessionStart hooks, so a cold `cargo build -p ci-cli` (~59s) easily outlasts the client's ~7s retry budget — the server gets marked failed for the rest of the session. The only structural fix was previously a Cloud environment Setup Script, which is external config (not stored in this repo) that every user has to paste in manually. This adds a stronger layer that needs no manual setup at all: a prebuilt `ci` binary committed to .ci-bin/x86_64-unknown-linux-musl/ci via Git LFS (already used in this repo for the embedding model weights), rebuilt on every push to main by .github/workflows/prebuild-mcp-binary.yml. Since a git checkout necessarily completes before Claude Code can even read .mcp.json, a binary already present in the checkout has no race to lose. mcp-launcher.sh gets a new Tier 1.5 trying this path (Linux x86_64 only for now), subject to the same is_binary_fresh staleness check as a local target/debug build. Also adds is_lfs_pointer: exec-ing an unresolved LFS pointer stub (git-lfs not installed, smudge filter skipped) does not fail gracefully — bash's ENOEXEC fallback interprets the pointer's text content as a new shell script instead of returning control to the launcher, verified directly against a synthetic stub. Without the check, a missing git-lfs would crash the launcher instead of falling through to the existing tiers. Kept the Setup Script recommendation in the docs as defense-in-depth: this depends on the cloud checkout actually resolving the LFS pointer, which isn't independently verifiable from here, and only covers one platform. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 0c42c76 commit 7f25adc

4 files changed

Lines changed: 139 additions & 9 deletions

File tree

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
crates/ci-core/assets/potion-code-16m/*.safetensors filter=lfs diff=lfs merge=lfs -text
2+
.ci-bin/**/ci filter=lfs diff=lfs merge=lfs -text
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Prebuild MCP binary
2+
3+
# Keeps a ready-to-run `ci` binary committed at .ci-bin/x86_64-unknown-linux-musl/ci
4+
# (via Git LFS) so scripts/mcp-launcher.sh has it the instant a fresh clone
5+
# happens — no compile, no download, no tagged-release requirement. This is
6+
# what closes the Claude-Code-on-the-web cold-start race that a Setup
7+
# Script/SessionStart hook can only race, never structurally win — see
8+
# docs/cloud-environment-setup.md.
9+
#
10+
# `paths-ignore: ['.ci-bin/**']` is load-bearing: without it, this workflow's
11+
# own commit back to main would retrigger itself.
12+
on:
13+
push:
14+
branches: [main]
15+
paths-ignore:
16+
- '.ci-bin/**'
17+
18+
permissions:
19+
contents: write
20+
21+
jobs:
22+
build-and-commit:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
26+
with:
27+
lfs: true
28+
29+
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
30+
with:
31+
targets: x86_64-unknown-linux-musl
32+
33+
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
34+
with:
35+
key: x86_64-unknown-linux-musl-prebuild
36+
37+
- name: Install cross
38+
run: cargo install cross --locked
39+
40+
- name: Build
41+
run: cross build --release --bin ci --target x86_64-unknown-linux-musl
42+
43+
- name: Commit prebuilt binary
44+
run: |
45+
mkdir -p .ci-bin/x86_64-unknown-linux-musl
46+
cp target/x86_64-unknown-linux-musl/release/ci .ci-bin/x86_64-unknown-linux-musl/ci
47+
git config user.name "github-actions[bot]"
48+
git config user.email "github-actions[bot]@users.noreply.github.com"
49+
git add .ci-bin/x86_64-unknown-linux-musl/ci
50+
if git diff --cached --quiet; then
51+
echo "Binary unchanged, nothing to commit"
52+
exit 0
53+
fi
54+
git commit -m "chore(ci-bin): update prebuilt ci binary for $(git rev-parse --short HEAD)"
55+
git push

docs/cloud-environment-setup.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,37 @@ of this repo's setup relied on that hook alone and believed it had fixed
3131
the problem; it hadn't — it had just usually won the race, until a session
3232
where it didn't.
3333

34+
## A stronger layer: a binary already sitting in the checkout
35+
36+
Everything below this point (Setup Script, `MCP_TIMEOUT`) works by trying
37+
to *win* a race against the MCP client's dial attempt. There's a way to
38+
avoid the race entirely: `.ci-bin/x86_64-unknown-linux-musl/ci`, a
39+
prebuilt binary committed to the repo via Git LFS and kept current by
40+
[`.github/workflows/prebuild-mcp-binary.yml`](../.github/workflows/prebuild-mcp-binary.yml)
41+
on every push to `main`. `scripts/mcp-launcher.sh` execs it directly
42+
(subject to the same `is_binary_fresh` staleness check as a local
43+
`target/debug/ci`) — if it's there, there is no compile step for the MCP
44+
dial to race against, because the checkout itself (which necessarily
45+
completes before Claude Code can even read `.mcp.json`) already contains a
46+
working binary.
47+
48+
**This is not a certainty, only a strong improvement**, for two concrete
49+
reasons:
50+
51+
- It depends on the cloud checkout mechanism actually resolving the Git
52+
LFS pointer to real content (running the smudge filter) rather than
53+
leaving a ~130-byte text stub in place. `scripts/mcp-launcher.sh` detects
54+
an unresolved pointer and falls through safely instead of crashing (see
55+
`is_lfs_pointer`'s comment for why that needed an explicit check — a
56+
plain `exec` on a pointer stub does *not* fail gracefully), but a
57+
fallthrough here still means you're back to racing the Setup Script
58+
against a cold build.
59+
- It only covers `x86_64-unknown-linux-musl` today. A different sandbox
60+
architecture falls through to the tiers below, unaffected either way.
61+
62+
Keep the Setup Script below as defense-in-depth regardless — it's the only
63+
one of the two that's a guaranteed fix rather than a "very likely" one.
64+
3465
## The actual fix: a Cloud environment Setup Script
3566

3667
Setup Scripts and `SessionStart` hooks look similar but solve different
@@ -113,6 +144,9 @@ session before any cache exists, or right after the ~7-day cache expiry)
113144

114145
## What's already handled in this repo (defense in depth, not a substitute)
115146

147+
- `.ci-bin/x86_64-unknown-linux-musl/ci` — see the section above. The one
148+
layer that can eliminate the race outright rather than just narrowing it,
149+
when it applies.
116150
- `scripts/mcp-launcher.sh``.mcp.json`'s actual entrypoint now (shared
117151
across every MCP client, not just Claude Code; see
118152
`docs/mcp-client-setup.md`). Execs an already-cached binary directly if

scripts/mcp-launcher.sh

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,38 @@
66
#
77
# Resolution order (first usable binary wins, no exceptions):
88
# 1. Fast path — an already-usable binary: $CI_MCP_BIN override, a
9-
# cached verified download, or a local dev build
10-
# (target/release/ci, target/debug/ci) — the dev build
11-
# candidates are only trusted if `is_binary_fresh` says
12-
# they're at least as new as every source file (see that
13-
# function's comment for the incident this guards
14-
# against: a stale target/debug/ci silently served an
15-
# entire MCP session because nothing checked it against
16-
# the checked-out source before exec'ing it).
9+
# cached verified download, a local dev build
10+
# (target/release/ci, target/debug/ci), or a prebuilt
11+
# binary committed to .ci-bin/ via Git LFS — the dev
12+
# build and .ci-bin candidates are only trusted if
13+
# `is_binary_fresh` says they're at least as new as
14+
# every source file (see that function's comment for
15+
# the incident this guards against: a stale
16+
# target/debug/ci silently served an entire MCP session
17+
# because nothing checked it against the checked-out
18+
# source before exec'ing it).
1719
# $CI_MCP_BIN and the cached download are NOT freshness-
1820
# checked here — one is an explicit override (the caller
1921
# is asserting "use exactly this"), the other is an
2022
# immutable, checksum-verified artifact for an exact
2123
# tagged commit (its own consistency check is the tag
2224
# match + `--version` check in download_and_verify).
25+
# .ci-bin/ closes the exact cold-start race
26+
# docs/cloud-environment-setup.md documents for Claude
27+
# Code on the web: a Setup Script or SessionStart hook
28+
# can only race a cold `cargo build` against the MCP
29+
# client's dial attempt, but a binary already sitting in
30+
# the checkout the instant it's cloned has no race to
31+
# lose. Built by .github/workflows/prebuild-mcp-binary.yml
32+
# on every push to main; may lag HEAD by one commit,
33+
# which `is_binary_fresh` catches the same way it catches
34+
# a stale local dev build. Only covers
35+
# x86_64-unknown-linux-musl today. Also guarded against
36+
# an unresolved Git LFS pointer (git-lfs not installed,
37+
# or a checkout that skipped the smudge filter) — see
38+
# `is_lfs_pointer`'s comment for why that specific
39+
# failure mode needs an explicit check instead of
40+
# falling through on its own.
2341
# 2. Verified download — Linux x86_64/aarch64 only, and only when HEAD is
2442
# exactly a released git tag (never guesses a version).
2543
# Downloads the matching GitHub Release asset, verifies
@@ -89,9 +107,25 @@ is_binary_fresh() {
89107
[ -z "$newer" ]
90108
}
91109

110+
# True if `bin` looks like an unresolved Git LFS pointer stub rather than
111+
# real binary content — happens when `git lfs pull`/the smudge filter never
112+
# ran during checkout (e.g. git-lfs not installed in the environment). A
113+
# real `ci` binary is tens of MB; an LFS pointer is a ~130-byte text file
114+
# starting with this exact line. This matters because `exec`-ing one does
115+
# NOT fail gracefully: the kernel's ENOEXEC (no shebang, not an ELF) makes
116+
# bash fall back to interpreting the file's *text content* as a new shell
117+
# script, which runs (and errors on "version: command not found") INSTEAD
118+
# OF returning control to this script — verified directly against a
119+
# synthetic pointer stub, not assumed. Without this check, an unresolved
120+
# .ci-bin/ pointer would crash the whole launcher instead of falling
121+
# through to the next tier.
122+
is_lfs_pointer() {
123+
[ "$(head -c 7 -- "$1" 2>/dev/null)" = "version" ]
124+
}
125+
92126
try_exec_if_fresh() {
93127
local bin="$1"
94-
if [ -x "$bin" ]; then
128+
if [ -x "$bin" ] && ! is_lfs_pointer "$bin"; then
95129
if is_binary_fresh "$bin"; then
96130
exec "$bin" "${serve_args[@]}"
97131
else
@@ -110,6 +144,12 @@ cache_key="${resolved_tag:-$workspace_version}"
110144
try_exec_if_fresh "target/release/ci"
111145
try_exec_if_fresh "target/debug/ci"
112146

147+
# ---- Tier 1.5: prebuilt binary committed via Git LFS (see header) ----
148+
# Only the one platform .github/workflows/prebuild-mcp-binary.yml builds.
149+
if [ "$(uname -s)" = "Linux" ] && [ "$(uname -m)" = "x86_64" ]; then
150+
try_exec_if_fresh ".ci-bin/x86_64-unknown-linux-musl/ci"
151+
fi
152+
113153
# ---- Tier 2: verified download (Linux only, tagged commit only unless opted in) ----
114154
download_and_verify() {
115155
local os arch target_triple tag asset_name asset_url sums_url tmp_dir downloaded_version cache_dir

0 commit comments

Comments
 (0)