diff --git a/CLAUDE.md b/CLAUDE.md index 06cd7ce..543c223 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -238,6 +238,21 @@ Admins can **View as** a sub-user from the top-right of the home layout (`Profil `hermes_requires <0.20.0` on a "verified" 401 that came from a hand-rolled `hermes serve` run rather than `hermes webui`. 0.20.0 works. Verify against the real start path before writing a version constraint. +5. **The agent image build clones two third-party repos over the public internet, + unauthenticated.** `agent/Dockerfile` fetches `NousResearch/hermes-agent` and + `nesquena/hermes-webui` at build time. GitHub rate-limits unauthenticated + requests *per source IP*, and Actions runners share pooled egress addresses, so + `docker compose build` intermittently died on `fatal: unable to access ...: The + requested URL returned error: 429` (exit 128) — 2 of 15 runs, always on branches + whose diff had nothing to do with the agent. Both clones now retry 5x with + backoff, and still print FATAL and exit 1 on exhaustion so a genuinely dead + upstream cannot yield an image with no runtime in it. Two lessons worth keeping: + a CI failure that is *intermittent and unrelated to the diff* is a network or + rate-limit signature, not a code defect — read the log before bisecting the + branch; and the webui clone's pre-existing `|| git clone` fallback looked like + resilience but only ever handled a *moved ref*, re-issuing the identical refused + request against a 429. A fallback that fails the same way as the thing it backs + up is not a fallback. ### Verification Anti-Patterns (do not repeat) diff --git a/agent/Dockerfile b/agent/Dockerfile index 48006e2..1f59bcb 100644 --- a/agent/Dockerfile +++ b/agent/Dockerfile @@ -52,8 +52,24 @@ ARG AGENT_RUNTIME_REF=v2026.8.18 ARG AGENT_API_REPO=https://github.com/nesquena/hermes-webui.git ARG AGENT_API_REF=v0.52.76 -# Runtime engine (Python agent) -RUN git clone --depth 1 --branch "${AGENT_RUNTIME_REF}" "${AGENT_RUNTIME_REPO}" runtime \ +# Runtime engine (Python agent). +# +# The clone retries with backoff. github.com rate-limits *unauthenticated* +# requests per source IP, and CI runners share pooled egress addresses, so a +# cold clone here intermittently returns HTTP 429 and took the whole image +# build down with it (`fatal: unable to access ...: error: 429`, exit 128 -- +# 2 of 15 `docker compose build` runs). This is not CI-only defensiveness: a +# self-host user behind a shared NAT hits the identical refusal. Retry is the +# right layer precisely because the condition is transient; the loop still +# exits non-zero and loudly on the last attempt rather than continuing with no +# runtime cloned. +RUN n=0; rm -rf runtime; \ + until git clone --depth 1 --branch "${AGENT_RUNTIME_REF}" "${AGENT_RUNTIME_REPO}" runtime; do \ + n=$((n+1)); \ + if [ "$n" -ge 5 ]; then echo "FATAL: clone of ${AGENT_RUNTIME_REPO} failed after 5 attempts" >&2; exit 1; fi; \ + echo "clone attempt $n failed (HTTP 429 / transient network); retrying in $((n*15))s" >&2; \ + rm -rf runtime; sleep $((n*15)); \ + done \ && cd runtime \ && UV_NO_CONFIG=1 uv sync \ && if [ -d .venv ] && [ ! -d venv ]; then ln -sfn .venv venv; fi \ @@ -64,8 +80,18 @@ RUN git clone --depth 1 --branch "${AGENT_RUNTIME_REF}" "${AGENT_RUNTIME_REPO}" # HTTP API surface the frontend / backend talk to (:8787). # Default branch is master (not main). Fall back to HEAD if the ref moves. -RUN (git clone --depth 1 --branch "${AGENT_API_REF}" "${AGENT_API_REPO}" api \ - || git clone --depth 1 "${AGENT_API_REPO}" api) \ +# +# Same 429 retry as the runtime clone above. Note the inner `||` fallback +# handles only a *moved ref*; on a rate-limit it just re-issues the same +# refused request, so it needed the outer retry to be any use at all. +RUN n=0; rm -rf api; \ + until (git clone --depth 1 --branch "${AGENT_API_REF}" "${AGENT_API_REPO}" api \ + || git clone --depth 1 "${AGENT_API_REPO}" api); do \ + n=$((n+1)); \ + if [ "$n" -ge 5 ]; then echo "FATAL: clone of ${AGENT_API_REPO} failed after 5 attempts" >&2; exit 1; fi; \ + echo "clone attempt $n failed (HTTP 429 / transient network); retrying in $((n*15))s" >&2; \ + rm -rf api; sleep $((n*15)); \ + done \ && cd api \ && if [ -f requirements.txt ]; then \ UV_NO_CONFIG=1 uv pip install --python /opt/deepsql-agent/runtime/venv/bin/python -r requirements.txt; \