Skip to content

Commit f554d24

Browse files
lroolleclaude
andcommitted
fix(build): harden proxy forwarding and registry fetch fallback
Review fixes for the re-landed proxy work (#407, split from #404): - docker build gets --add-host host.docker.internal:host-gateway whenever proxy args are forwarded: the localhost rewrite was dead on native Linux Engine (Docker Desktop/OrbStack resolve the name implicitly, Engine does not). Verified against a real daemon. - localhost rewrite now catches authenticated proxies: the old patterns anchored on ://127.0.0.1 and missed http://user:pass@127.0.0.1:7890 (confirmed via make -n). Make macro and version-upgrade sed both handle @127.0.0.1/@localhost. - proxy userinfo never printed raw: log_proxy_config (build logs) and version-upgrade's forwarding display redact user:pass@ to ***@. Residual: make's own recipe echo still expands proxy values in a local terminal; not worth silencing every build recipe over. - npm registry fetch restores npm view's soft-fail contract: transient curl failure returns empty and hits load_versions' warn-and-fallback branch instead of aborting under set -euo pipefail (that branch was dead code for npm tools). - tests/version-upgrade.sh: fake curl now serves realistic dist-tags + packument fixtures instead of empty strings (the stale npm view mocks made the suite pass while no builds ran, then die on the missing docker-build.log — the #404 CI red). npm mock is a tripwire: any npm call fails loudly. New scenarios: proxied build asserts rewrite + host-gateway + no credential leak; registry outage asserts warnings, fallback to current, exit 0, no builds. Close #407 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 2e64b42 commit f554d24

7 files changed

Lines changed: 127 additions & 59 deletions

File tree

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# Created by https://www.toptal.com/developers/gitignore/api/node
22
# Edit at https://www.toptal.com/developers/gitignore?templates=node
33

4-
/AGENTS.md
5-
/.cctrace
64
.claude/
75
*.bak
86

Makefile

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ SKIP_BUILD_NETWORK_CHECK ?= 0
4848
# available in all stages without Dockerfile ARG declarations, excluded
4949
# from cache key so proxy changes don't bust layer cache).
5050
# Rewrite localhost proxy addresses: 127.0.0.1/localhost on the host is the
51-
# container's own loopback during build — host.docker.internal reaches the host.
52-
_dproxy = $(subst ://localhost,://host.docker.internal,$(subst ://127.0.0.1,://host.docker.internal,$1))
51+
# container's own loopback during build — host.docker.internal reaches the
52+
# host. The @-patterns catch authenticated proxies (user:pass@127.0.0.1).
53+
_dproxy = $(subst @localhost,@host.docker.internal,$(subst @127.0.0.1,@host.docker.internal,$(subst ://localhost,://host.docker.internal,$(subst ://127.0.0.1,://host.docker.internal,$1))))
5354
PROXY_BUILD_ARGS := \
5455
$(if $(HTTP_PROXY),--build-arg HTTP_PROXY=$(call _dproxy,$(HTTP_PROXY))) \
5556
$(if $(HTTPS_PROXY),--build-arg HTTPS_PROXY=$(call _dproxy,$(HTTPS_PROXY))) \
@@ -58,6 +59,12 @@ PROXY_BUILD_ARGS := \
5859
$(if $(NO_PROXY),--build-arg NO_PROXY=$(NO_PROXY)) \
5960
$(if $(no_proxy),--build-arg no_proxy=$(no_proxy))
6061

62+
# host.docker.internal resolves implicitly on Docker Desktop/OrbStack only;
63+
# native Linux Engine needs the explicit host-gateway mapping during build.
64+
ifneq ($(strip $(HTTP_PROXY)$(HTTPS_PROXY)$(http_proxy)$(https_proxy)),)
65+
PROXY_BUILD_ARGS += --add-host host.docker.internal:host-gateway
66+
endif
67+
6168
DOCKER_BUILD_FLAGS = $(PROXY_BUILD_ARGS) $(DOCKER_BUILD_EXTRA_ARGS)
6269

6370
TOOLCHAIN_BUILD_ARGS := \

scripts/install-agent-tooling.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,12 @@ download_to() {
119119
}
120120

121121
log_proxy_config() {
122+
# Proxy userinfo is credential material and this output lands in build
123+
# logs — redact user:pass@ before printing.
122124
local has_proxy=0
123125
for var in HTTP_PROXY HTTPS_PROXY http_proxy https_proxy NO_PROXY no_proxy; do
124126
if [ -n "${!var:-}" ]; then
125-
log " $var=${!var}"
127+
log " $var=$(printf '%s' "${!var}" | sed -E 's#://[^@/]*@#://***@#')"
126128
has_proxy=1
127129
fi
128130
done

scripts/release-utils.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,10 @@ fetch_latest_version() {
188188

189189
case $type in
190190
npm)
191-
_npm_registry_latest "$source"
191+
# Soft-fail like the gh arms below: a transient registry error
192+
# must reach load_versions' warn-and-fallback path, not abort
193+
# the whole run under set -e.
194+
_npm_registry_latest "$source" || echo ""
192195
;;
193196
github-release)
194197
gh api "repos/$source/releases/latest" --jq '.tag_name' 2>/dev/null || echo ""

scripts/update-version-pins.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ EOF
2121
fetch_npm_version() {
2222
curl -fsSL --max-time 10 \
2323
"https://registry.npmjs.org/-/package/$1/dist-tags" 2>/dev/null | \
24-
sed -n 's/.*"latest":"\([^"]*\)".*/\1/p'
24+
sed -n 's/.*"latest":"\([^"]*\)".*/\1/p' || true
2525
}
2626

2727
fetch_latest_git_tag() {

scripts/version-upgrade.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,20 @@ CORE_IMAGE=${CORE_IMAGE:-ghcr.io/thevibeworks/deva:core}
3232
# Forward host proxy env vars to Docker build stages.
3333
# Rewrite 127.0.0.1/localhost → host.docker.internal so the build
3434
# container reaches the host's proxy instead of its own loopback.
35-
_dproxy() { sed 's|://127\.0\.0\.1|://host.docker.internal|g; s|://localhost|://host.docker.internal|g' <<< "$1"; }
35+
# The @-patterns catch authenticated proxies (user:pass@127.0.0.1).
36+
_dproxy() { sed 's|://127\.0\.0\.1|://host.docker.internal|g; s|://localhost|://host.docker.internal|g; s|@127\.0\.0\.1|@host.docker.internal|g; s|@localhost|@host.docker.internal|g' <<< "$1"; }
37+
# Proxy userinfo is credential material — never print it raw.
38+
_redact_proxy() { sed -E 's#://[^@/]*@#://***@#' <<< "$1"; }
3639
PROXY_ARGS=()
3740
[[ -n ${HTTP_PROXY:-} ]] && PROXY_ARGS+=(--build-arg "HTTP_PROXY=$(_dproxy "$HTTP_PROXY")")
3841
[[ -n ${HTTPS_PROXY:-} ]] && PROXY_ARGS+=(--build-arg "HTTPS_PROXY=$(_dproxy "$HTTPS_PROXY")")
3942
[[ -n ${http_proxy:-} ]] && PROXY_ARGS+=(--build-arg "http_proxy=$(_dproxy "$http_proxy")")
4043
[[ -n ${https_proxy:-} ]] && PROXY_ARGS+=(--build-arg "https_proxy=$(_dproxy "$https_proxy")")
4144
[[ -n ${NO_PROXY:-} ]] && PROXY_ARGS+=(--build-arg "NO_PROXY=$NO_PROXY")
4245
[[ -n ${no_proxy:-} ]] && PROXY_ARGS+=(--build-arg "no_proxy=$no_proxy")
46+
# host.docker.internal resolves implicitly on Docker Desktop/OrbStack only;
47+
# native Linux Engine needs the explicit host-gateway mapping during build.
48+
[[ ${#PROXY_ARGS[@]} -gt 0 ]] && PROXY_ARGS+=(--add-host "host.docker.internal:host-gateway")
4349
RUST_IMAGE=${RUST_IMAGE:-ghcr.io/thevibeworks/deva:rust}
4450
DOCKERFILE=${DOCKERFILE:-Dockerfile}
4551
RUST_DOCKERFILE=${RUST_DOCKERFILE:-Dockerfile.rust}
@@ -215,7 +221,7 @@ main() {
215221
if [[ ${#PROXY_ARGS[@]} -gt 0 ]]; then
216222
echo -e "${DIM}Proxy forwarding to Docker build:${RESET}"
217223
for _pa in "${PROXY_ARGS[@]}"; do
218-
echo -e " ${DIM}${_pa#--build-arg }${RESET}"
224+
echo -e " ${DIM}$(_redact_proxy "${_pa#--build-arg }")${RESET}"
219225
done
220226
echo ""
221227
fi

tests/version-upgrade.sh

Lines changed: 102 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -43,57 +43,13 @@ build)
4343
esac
4444
EOF
4545

46+
# npm must never be called: version resolution goes straight to the
47+
# registry via curl. Tripwire catches regressions back to `npm view`
48+
# (which honors .npmrc registry overrides and can serve stale metadata).
4649
cat >"$FAKE_BIN/npm" <<'EOF'
4750
#!/usr/bin/env bash
48-
set -euo pipefail
49-
50-
if [[ "${1:-}" != "view" ]]; then
51-
echo "unexpected npm invocation: $*" >&2
52-
exit 1
53-
fi
54-
55-
case "${2:-}" in
56-
@anthropic-ai/claude-code)
57-
echo "2.1.87"
58-
;;
59-
@anthropic-ai/claude-code@2.1.87)
60-
echo '{"2.1.87":"2026-03-29T01:40:00Z"}'
61-
;;
62-
@thevibeworks/cctrace)
63-
echo "0.4.0"
64-
;;
65-
@thevibeworks/cctrace@0.4.0)
66-
echo '{"0.4.0":"2026-03-29T01:40:00Z"}'
67-
;;
68-
@openai/codex)
69-
echo "0.117.0"
70-
;;
71-
@openai/codex@0.117.0)
72-
echo '{"0.117.0":"2026-03-26T22:28:00Z"}'
73-
;;
74-
@google/gemini-cli)
75-
echo "0.35.3"
76-
;;
77-
@google/gemini-cli@0.35.3)
78-
echo '{"0.35.3":"2026-03-28T03:17:00Z"}'
79-
;;
80-
@xai-official/grok)
81-
echo "0.2.93"
82-
;;
83-
@xai-official/grok@0.2.93)
84-
echo '{"0.2.93":"2026-07-01T00:00:00Z"}'
85-
;;
86-
playwright)
87-
echo "1.60.0"
88-
;;
89-
playwright@1.60.0)
90-
echo '{"1.60.0":"2026-05-14T08:00:00Z"}'
91-
;;
92-
*)
93-
echo "unexpected npm view args: $*" >&2
94-
exit 1
95-
;;
96-
esac
51+
echo "unexpected npm invocation: $*" >&2
52+
exit 1
9753
EOF
9854

9955
cat >"$FAKE_BIN/gh" <<'EOF'
@@ -134,10 +90,31 @@ repos/microsoft/playwright/releases)
13490
esac
13591
EOF
13692

93+
# Realistic registry fixtures: dist-tags for fetch_latest_version,
94+
# packument (.time) for fetch_version_date. Unknown URLs are a hard error
95+
# so new fetch paths cannot silently no-op like the old empty-string stub.
13796
cat >"$FAKE_BIN/curl" <<'EOF'
13897
#!/usr/bin/env bash
13998
set -euo pipefail
140-
printf ''
99+
url="${!#}"
100+
case "$url" in
101+
*/-/package/@anthropic-ai/claude-code/dist-tags) echo '{"latest":"2.1.87"}' ;;
102+
*/-/package/@thevibeworks/cctrace/dist-tags) echo '{"latest":"0.4.0"}' ;;
103+
*/-/package/@openai/codex/dist-tags) echo '{"latest":"0.117.0"}' ;;
104+
*/-/package/@google/gemini-cli/dist-tags) echo '{"latest":"0.35.3"}' ;;
105+
*/-/package/@xai-official/grok/dist-tags) echo '{"latest":"0.2.93"}' ;;
106+
*/-/package/playwright/dist-tags) echo '{"latest":"1.60.0"}' ;;
107+
*registry.npmjs.org/@anthropic-ai%2fclaude-code) echo '{"time":{"2.1.87":"2026-03-29T01:40:00Z"}}' ;;
108+
*registry.npmjs.org/@thevibeworks%2fcctrace) echo '{"time":{"0.4.0":"2026-03-29T01:40:00Z"}}' ;;
109+
*registry.npmjs.org/@openai%2fcodex) echo '{"time":{"0.117.0":"2026-03-26T22:28:00Z"}}' ;;
110+
*registry.npmjs.org/@google%2fgemini-cli) echo '{"time":{"0.35.3":"2026-03-28T03:17:00Z"}}' ;;
111+
*registry.npmjs.org/@xai-official%2fgrok) echo '{"time":{"0.2.93":"2026-07-01T00:00:00Z"}}' ;;
112+
*registry.npmjs.org/playwright) echo '{"time":{"1.60.0":"2026-05-14T08:00:00Z"}}' ;;
113+
*)
114+
echo "unexpected curl url: $url" >&2
115+
exit 1
116+
;;
117+
esac
141118
EOF
142119

143120
chmod +x "$FAKE_BIN/docker" "$FAKE_BIN/npm" "$FAKE_BIN/gh" "$FAKE_BIN/curl"
@@ -205,3 +182,78 @@ do
205182
exit 1
206183
}
207184
done
185+
186+
# ───── proxied build: localhost rewrite + host-gateway + redacted logs ─────
187+
PROXY_BUILD_LOG="$TMP_ROOT/docker-build-proxy.log"
188+
proxy_out="$(PATH="$FAKE_BIN:$PATH" \
189+
DOCKER_BUILD_LOG="$PROXY_BUILD_LOG" \
190+
AUTO_YES=1 \
191+
HTTP_PROXY="http://user:secret@127.0.0.1:7890" \
192+
HTTPS_PROXY="http://localhost:7890" \
193+
CHECK_IMAGE="ghcr.io/thevibeworks/deva:rust" \
194+
BUILD_IMAGE="ghcr.io/thevibeworks/deva:latest" \
195+
CORE_IMAGE="ghcr.io/thevibeworks/deva:core" \
196+
RUST_IMAGE="ghcr.io/thevibeworks/deva:rust" \
197+
GO_VERSION="1.26.2" \
198+
CCTRACE_VERSION="0.4.0" \
199+
PLAYWRIGHT_VERSION="1.60.0" \
200+
"$REPO_ROOT/scripts/version-upgrade.sh" 2>&1)"
201+
202+
proxy_build="$(sed -n '1p' "$PROXY_BUILD_LOG")"
203+
for expected in \
204+
"--build-arg HTTP_PROXY=http://user:secret@host.docker.internal:7890" \
205+
"--build-arg HTTPS_PROXY=http://host.docker.internal:7890" \
206+
"--add-host host.docker.internal:host-gateway"
207+
do
208+
[[ "$proxy_build" == *"$expected"* ]] || {
209+
echo "proxied build missing expected arg: $expected" >&2
210+
echo "$proxy_build" >&2
211+
exit 1
212+
}
213+
done
214+
if grep -F -- "user:secret@" <<<"$proxy_out" >/dev/null; then
215+
echo "proxy credentials leaked into script output" >&2
216+
exit 1
217+
fi
218+
if ! grep -F -- "://***@" <<<"$proxy_out" >/dev/null; then
219+
echo "expected redacted proxy URL in script output" >&2
220+
exit 1
221+
fi
222+
223+
# ───── registry outage: warn + fall back to current, do not abort ─────
224+
cat >"$FAKE_BIN/curl" <<'EOF'
225+
#!/usr/bin/env bash
226+
exit 22
227+
EOF
228+
chmod +x "$FAKE_BIN/curl"
229+
230+
OUTAGE_BUILD_LOG="$TMP_ROOT/docker-build-outage.log"
231+
if ! outage_out="$(PATH="$FAKE_BIN:$PATH" \
232+
DOCKER_BUILD_LOG="$OUTAGE_BUILD_LOG" \
233+
AUTO_YES=1 \
234+
CHECK_IMAGE="ghcr.io/thevibeworks/deva:rust" \
235+
BUILD_IMAGE="ghcr.io/thevibeworks/deva:latest" \
236+
CORE_IMAGE="ghcr.io/thevibeworks/deva:core" \
237+
RUST_IMAGE="ghcr.io/thevibeworks/deva:rust" \
238+
GO_VERSION="1.26.2" \
239+
CCTRACE_VERSION="0.4.0" \
240+
PLAYWRIGHT_VERSION="1.60.0" \
241+
"$REPO_ROOT/scripts/version-upgrade.sh" 2>&1)"; then
242+
echo "registry outage must degrade to warnings, not abort the run" >&2
243+
echo "$outage_out" >&2
244+
exit 1
245+
fi
246+
if ! grep -F -- "Failed to fetch latest" <<<"$outage_out" >/dev/null; then
247+
echo "expected fetch-failure warnings during outage" >&2
248+
echo "$outage_out" >&2
249+
exit 1
250+
fi
251+
if ! grep -F -- "All versions up-to-date" <<<"$outage_out" >/dev/null; then
252+
echo "expected up-to-date fallback conclusion during outage" >&2
253+
echo "$outage_out" >&2
254+
exit 1
255+
fi
256+
if [[ -s "$OUTAGE_BUILD_LOG" ]]; then
257+
echo "no builds should run during a registry outage" >&2
258+
exit 1
259+
fi

0 commit comments

Comments
 (0)