Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions claude/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,10 @@ runs:
# plugins, and the run all execute as the sandbox user. Cache the proxy's
# mitmproxy download (a sizeable dependency) in an isolated uv cache dir,
# keyed on the pinned version. First run populates; every run after restores
# in seconds instead of a cold ~20s resolve. Scoped to the proxy's uv; a
# skill that installs its own uv gets that uv's default cache, untouched.
# instead of pulling ~100MB from PyPI again (measured 2026-08-09 on a
# GitHub-hosted runner: 2.2s cold, 0.5s restored — the saving is the reach,
# more than the seconds). Scoped to the proxy's uv; a skill that installs
# its own uv gets that uv's default cache, untouched.
- name: Cache mitmproxy
uses: actions/cache@v4
with:
Expand Down
76 changes: 76 additions & 0 deletions generator/tests/test_shared_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -1691,3 +1691,79 @@ def test_install_codex_cli_installs_first_try_without_sleeping(
)
assert _sleeps(codex_cli_env) == [], _sleeps(codex_cli_env)
assert "codex-cli 0.131.0-alpha.22" in result.stdout, result.stdout


# ---------------------------------------------------------------------------
# proxy/setup-sandbox.sh — the mitmproxy cache warm, the reach that isn't an
# installer
# ---------------------------------------------------------------------------

SETUP_SANDBOX = REPO_ROOT / "proxy" / "setup-sandbox.sh"


def _logical_lines(path: Path) -> list[str]:
"""Script lines with backslash continuations folded, so a call reads as one."""
out: list[str] = []
buf = ""
for raw in path.read_text().splitlines():
stripped = raw.strip()
if stripped.endswith("\\"):
buf += stripped[:-1].rstrip() + " "
continue
out.append(buf + stripped)
buf = ""
if buf:
out.append(buf)
return out


def test_setup_sandbox_resolves_the_shared_retry_window() -> None:
"""The lib path has to resolve from ACTION_PATH the way the action passes it.

The action points ACTION_PATH at the repo root, and the script runs under
`set -euo pipefail`, so a path that doesn't resolve doesn't degrade to an
unretried warm — it exits the step, and then every run is lost, not only
the ones that hit a blip. Sourcing the script's own line (matched by
content) is what makes this a check on the shipped path rather than on a
copy of it.
"""
sources = [
line
for line in _logical_lines(SETUP_SANDBOX)
if line.startswith(". ") and "lib/retry.sh" in line
]
assert len(sources) == 1, sources

result = subprocess.run(
["bash", "-c", f"set -euo pipefail; {sources[0]}; type -t retry_install"],
env={"PATH": "/usr/bin:/bin", "ACTION_PATH": str(REPO_ROOT)},
capture_output=True,
text=True,
)

assert result.returncode == 0, result.stderr
assert result.stdout.strip() == "function", result.stdout


def test_setup_sandbox_warms_the_mitmproxy_cache_through_the_window() -> None:
"""The warm resolves mitmproxy from PyPI on a miss, so it rides the window.

A miss is scheduled rather than rare — the actions/cache key is scoped to
`mitmproxy_version`, so every bump misses on each workflow's first run —
and the warm sits three steps ahead of the agent, so a PyPI blip that isn't
ridden out costs the whole run, the failure the window exists for.

Asserted at the call site instead of by running the script: setup-sandbox.sh
creates the sandbox user, chowns the workspace and installs a CA before it
reaches this line, so the behavioural suites the three installers get don't
transfer. What the window itself does is covered there.
"""
warms = [
line
for line in _logical_lines(SETUP_SANDBOX)
if "mitmdump --version" in line and not line.startswith("#")
]
assert len(warms) == 1, warms
assert warms[0].startswith("retry_install "), (
f"the cache warm reaches PyPI outside the retry window: {warms[0]}"
)
26 changes: 24 additions & 2 deletions proxy/setup-sandbox.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ if [ -z "${MITMPROXY_VERSION:-}" ]; then
exit 1
fi

# The window the pre-agent third-party reaches share; the mitmproxy cache warm
# below is one of them. Sourced up here rather than at the point of use so a
# wrong path fails now, ahead of the user creation and the workspace chown.
# shellcheck source=../shared/steps/lib/retry.sh
. "${ACTION_PATH}/shared/steps/lib/retry.sh"

# 1. Non-sudo sandbox user. -m gives it /home/tend-sandbox (0755, so the
# runner can still read the session logs it writes).
if ! id "$SANDBOX" >/dev/null 2>&1; then
Expand Down Expand Up @@ -309,14 +315,30 @@ chmod 700 "$CONFDIR"
# Warm the uvx cache first so the backgrounded launch starts immediately and
# the readiness wait below measures startup, not a cold dependency resolve.
# Pinned + UV_CACHE_DIR (set by the action) point at the actions/cache-backed
# dir, so this is a fast restore after the first run.
# dir.
#
# On a miss this resolves and downloads mitmproxy from PyPI, which makes it the
# proxy's own third-party reach, three steps ahead of the agent — so a blip
# that isn't ridden out costs the whole run. The miss is scheduled rather than
# rare: the cache key is version-scoped, so every mitmproxy_version bump misses
# on each workflow's first run, which is when a lost run is most expensive (it
# reddens the bump PR itself). Hence retry_install.
#
# The window covers the backgrounded launch below too: it shares UV_CACHE_DIR,
# and a cache warmed here serves that same `--from` with no network at all
# (checked with `uvx --offline`), so PyPI is off the launch path once this line
# succeeds. Measured on a GitHub-hosted runner, 2026-08-09: 2.2s cold (43
# packages, ~100MB) and 0.5s restored — far enough inside the 60s timeout that
# this shares the lib's window rather than needing one of its own.
#
# $TEND_UV_DIR holds tend's own pinned uv (shared/steps/install-proxy-uv.sh),
# addressed absolutely rather than through PATH: the binary that launches the
# credential-holding process is tend's, not whatever the adopter's `setup:`
# left on PATH.
MITMPROXY="mitmproxy==${MITMPROXY_VERSION}"
UVX="${TEND_UV_DIR}/uvx"
"$UVX" --from "$MITMPROXY" mitmdump --version >/dev/null
retry_install "mitmproxy ${MITMPROXY_VERSION}" \
"'$UVX' --from '$MITMPROXY' mitmdump --version >/dev/null"
log "starting proxy"
# The --allow-hosts regex scopes which hosts mitmproxy TLS-intercepts. It must
# cover every host the addon injects into — keep it in sync with the
Expand Down
13 changes: 7 additions & 6 deletions shared/steps/lib/retry.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#!/usr/bin/env bash
# The retry window the pre-agent installers share: install-claude-binary.sh,
# install-proxy-uv.sh and install-codex-cli.sh each reach a third party — two
# CDNs and the npm registry — before the agent step exists, so a blip that
# exhausts their retries costs the whole run: the step goes red having done
# none of the work the trigger asked for. The lost run is what justifies a
# window this wide, independent of how the failure is reported afterwards.
# The retry window the pre-agent third-party reaches share: three installers —
# install-claude-binary.sh, install-proxy-uv.sh and install-codex-cli.sh (two
# CDNs and the npm registry) — plus proxy/setup-sandbox.sh's mitmproxy cache
# warm (PyPI). Each runs before the agent step exists, so a blip that exhausts
# its retries costs the whole run: the step goes red having done none of the
# work the trigger asked for. The lost run is what justifies a window this
# wide, independent of how the failure is reported afterwards.
#
# Sourced, not executed.

Expand Down
Loading