Operator-curated, URL-keyed artifact cache for a small lab (CUDA/ROCm/DOCA/firmware)
Project description
withcache
A tiny, operator-curated artifact cache for a small lab, for the big vendor
downloads you re-pull constantly (CUDA, ROCm, DOCA, firmware, drivers), fronted
by transparent curl/wget shims so existing scripts use it with no changes.
Think of it as "ccache for HTTP artifacts, without a proxy."
curl -fsSL https://the/origin/cuda.tar.gz -o cuda.tar.gz # your script, unchanged
└─ curlwithcache shim ─ WITHCACHE_SERVER set?
├─ cached → served from the cache-host (fast, local)
└─ miss/unset/unreachable → runs the real curl, exactly as written
Artifacts are cached by their origin URL as a key; the shim opts in by re-pointing the URL at the cache. No transparent proxy, no TLS interception, no client CA. The URL is a lookup key, not a connection target.
By default a miss is auto-fetched: the request falls through to origin (so
the caller gets its file straight away), and the cache-host pulls the same
artifact in the background, so the next request hits. Run with --curate to
require a human instead, who reviews the miss list in a small web UI and presses
Download (or pre-seeds via Add from URI). Either way the cache-host is the
only box that needs internet egress (and any vendor credentials), and clients
never write to it.
Why not just curl + a caching proxy?
For https:// (i.e. every vendor download) a forward proxy can't cache without
SSL-bump / MITM: curl tunnels TLS end-to-end via CONNECT, so the proxy
only sees ciphertext. The shim sidesteps that entirely by re-pointing the URL
to the cache instead of intercepting the connection. And no proxy offers the
optional operator-curated model (--curate: a miss queue a human approves).
Components
| Path | What it is |
|---|---|
src/withcache/server.py |
The cache-host: blob store + miss table + background download manager + operator UI (Pico.css + HTMX) |
src/withcache/_shim.py |
Shared shim core (find URL → probe → rewrite → exec) |
src/withcache/curlwithcache.py / wgetwithcache.py |
The Python curl / wget shims |
shim/shim.zig |
The native shim: one static binary, both tools via argv[0] |
deploy/Containerfile, deploy/compose.yml |
Single Podman/Docker host deploy |
The cache-host and the Python shims are stdlib-only (no third-party runtime deps); the native shim is a dependency-free static binary.
Install
The cache-host and Python shims (works on any box with Python):
pipx install withcache # or: uv tool install withcache / pip install withcache
# provides: curlwithcache wgetwithcache withcache-server
The native shim (no Python needed, for minimal/distroless boxes; ~200 KB static musl binary). Grab it from the Releases page; one binary serves both tools by the name it's invoked as:
curl -L .../releases/.../withcache-shim-x86_64-linux-musl -o /usr/local/bin/curlwithcache
chmod +x /usr/local/bin/curlwithcache
The Python shim is also the tested oracle and install-time fallback for
platforms without a prebuilt binary; a differential test
asserts the binary and the Python plan() rewrite argv identically.
Deploy the cache-host
export WITHCACHE_ADMIN_PASSWORD=change-me # protects the operator UI
podman compose -f deploy/compose.yml up -d # or: docker compose -f ...
# operator UI: http://withcache-server:3000/
Or without containers:
WITHCACHE_ADMIN_PASSWORD=change-me withcache-server --data-dir ./data --port 3000
Data (blobs + cache.db + session-secret) lives in the /data volume (or
--data-dir). Artifacts are immutable per version, so there's no cache
invalidation. --workers N sets the number of concurrent download workers, and
--curate switches from auto-fetch to operator-approved pulls.
Use the shims (transparent curl / wget)
Every approach is the same two ingredients: (1) point at the cache with
WITHCACHE_SERVER, and (2) make curl/wget resolve to the shim. They differ
only in how widely the system curl/wget is shadowed. Pick the least
invasive one that fits.
Safety: with
WITHCACHE_SERVERunset the shim is a pure pass-through (it justexecs the real tool, zero network/parsing), so even the system-wide setup is harmless wherever the cache isn't configured. Worst case is always "no caching,curlstill works."
These all use command -v curlwithcache, so they work whether you installed the
native binary or the Python launcher (both land under that name).
1. No shadowing: call the shims by name (least invasive)
Nothing is renamed; you opt in per command. Good for trying it out or a script you can edit.
export WITHCACHE_SERVER=http://withcache-server:3000
curlwithcache -fsSL https://the/origin/cuda.tar.gz -o cuda.tar.gz
wgetwithcache https://the/origin/rocm.tar.gz
2. This shell only: shadow curl/wget for the session
Put curl/wget symlinks in a dir and prepend it to PATH in the current
shell. Reversible by just closing the shell.
mkdir -p ~/.withcache/bin
ln -sf "$(command -v curlwithcache)" ~/.withcache/bin/curl
ln -sf "$(command -v wgetwithcache)" ~/.withcache/bin/wget
export WITHCACHE_SERVER=http://withcache-server:3000
export PATH="$HOME/.withcache/bin:$PATH"
hash -r # forget any cached curl/wget location
command -v curl # -> ~/.withcache/bin/curl (verify it's the shim)
curl -fsSL https://the/origin/cuda.tar.gz -o cuda.tar.gz # existing scripts, unchanged
wget https://the/origin/rocm.tar.gz # still saved as rocm.tar.gz
3. Your user: make it the default for your shells (persistent)
Create the symlinks once, then add the two exports to your shell rc. Affects all your future interactive shells; undo by deleting the block.
mkdir -p ~/.withcache/bin
ln -sf "$(command -v curlwithcache)" ~/.withcache/bin/curl
ln -sf "$(command -v wgetwithcache)" ~/.withcache/bin/wget
cat >> ~/.bashrc <<'EOF'
# withcache: transparent curl/wget caching
export WITHCACHE_SERVER=http://withcache-server:3000
export PATH="$HOME/.withcache/bin:$PATH"
EOF
4. One project only: scope it with direnv
Drop an .envrc in a project tree (requires direnv); caching applies only
inside that directory.
# .envrc
export WITHCACHE_SERVER=http://withcache-server:3000
PATH_add ~/.withcache/bin # assumes the symlinks from approach 2/3 exist
Then direnv allow.
5. The whole machine: every user, every shell (most invasive)
Install the shim as curl/wget in /usr/local/bin (ahead of /usr/bin on
the default PATH) and set the server globally. This also catches build tools
and package managers that shell out to curl/wget.
sudo ln -sf "$(command -v curlwithcache)" /usr/local/bin/curl
sudo ln -sf "$(command -v wgetwithcache)" /usr/local/bin/wget
# A login-shell env file (covers interactive logins; daemons started outside a
# login shell won't see it; set WITHCACHE_SERVER in their unit if you need it).
echo 'export WITHCACHE_SERVER=http://withcache-server:3000' \
| sudo tee /etc/profile.d/withcache.sh >/dev/null
On minimal/distroless hosts use the native shim binary here: same symlink, no Python required.
Verify / turn it off
command -v curl # which curl is in effect (the shim, or the real one)
export REAL_CURL=/usr/bin/curl # optional: pin the wrapped tool (also $REAL_WGET)
unset WITHCACHE_SERVER # instantly back to plain curl (pass-through)
rm ~/.withcache/bin/curl ~/.withcache/bin/wget # remove shadowing entirely
How it works: the shim scans for the URL, asks the cache, and execs the real tool:
- Find the real
curl/wgeton$PATH(skipping itself;$REAL_CURL/$REAL_WGEToverride). - With
WITHCACHE_SERVERset, find the URL (thescheme://arg, or--url). - Probe the cache with that same tool (
curl -I/wget --spider).- Hit → re-point only the URL at
http://server/b/<base64(origin)>/<basename>andexecthe real tool (so-o,-O,-L,--retry, … all still apply, and the file is named after the artifact). - Miss / unreachable →
execthe real tool with your arguments untouched (origin); the miss is recorded for the operator.
- Hit → re-point only the URL at
- With no
WITHCACHE_SERVER, it does zero network/parsing, justexecs the real tool.
Notes & limits (all degrade gracefully; worst case is "no caching, curl still works"):
- Needs the wrapped tool present (it shims it). Adds ~Python-startup latency per call.
- URLs hidden in a
-K/-iconfig file or piped via stdin aren't seen → those calls pass through uncached. - Per-tool env override:
CURLWITHCACHE_SERVER/WGETWITHCACHE_SERVERbeatWITHCACHE_SERVER.
Operator UI
http://withcache-server:3000/ (Pico.css + HTMX, bundled offline) shows:
- Misses: auto-fetched by default, or (under
--curate) each with Download (queues a background pull) and Dismiss. - Downloads: live progress bars,
queued/running/completed/cancelled/failed, Cancel, and Clear finished. Downloads run in a background worker pool, not in the request, so large pulls never block, modelled on bty's job managers. - Cached artifacts: URL, size, hits (times served) and misses (times requested before it was cached), SHA-256, fetched-at.
- Add from URI: pre-seed an artifact before anyone misses it.
Auth
Single-tenant session-cookie auth (modelled on bty's approach, env password
instead of PAM). The read path (/blob, /b/…, /healthz) is open so shims
never log in; the operator surface (/, /admin/*) is gated.
| Env var | Purpose |
|---|---|
WITHCACHE_SERVER |
Cache-host URL the shims use |
CURLWITHCACHE_SERVER / WGETWITHCACHE_SERVER |
Per-tool override of the above |
WITHCACHE_ADMIN_PASSWORD |
Operator login password (unset ⇒ UI open, with a warning) |
WITHCACHE_SESSION_SECRET |
Override the persisted cookie-signing key (optional) |
Cache keys & signed URLs
The key is scheme://host/path with the query string dropped by default, so
CDN/presigned URLs (whose tokens change every request) still match by path. Pass
--keep-query to the server for query-sensitive keys. Package-manager repos
(.deb/.rpm) are GPG-signed and verified by the client regardless of
transport, so caching them this way is safe.
Tests
python -m unittest discover -s tests # stdlib only, no test deps
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file withcache-0.2.0.tar.gz.
File metadata
- Download URL: withcache-0.2.0.tar.gz
- Upload date:
- Size: 59.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
20cc5921c4efb42c4af6503ed0f0d243ca591acef0cea03e85ed0b4093cae745
|
|
| MD5 |
27e61cb38c6006942eb6506888678a5a
|
|
| BLAKE2b-256 |
6665905ce00698903d0eba5249a279f96e1f0d0f898cbdf83b90dd0f28613cb8
|
Provenance
The following attestation bundles were made for withcache-0.2.0.tar.gz:
Publisher:
release.yml on safl/withcache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
withcache-0.2.0.tar.gz -
Subject digest:
20cc5921c4efb42c4af6503ed0f0d243ca591acef0cea03e85ed0b4093cae745 - Sigstore transparency entry: 1712330285
- Sigstore integration time:
-
Permalink:
safl/withcache@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/safl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Trigger Event:
push
-
Statement type:
File details
Details for the file withcache-0.2.0-py3-none-musllinux_1_2_x86_64.whl.
File metadata
- Download URL: withcache-0.2.0-py3-none-musllinux_1_2_x86_64.whl
- Upload date:
- Size: 244.9 kB
- Tags: Python 3, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
503a0dcc69be64f809addd58843b0880be6c7134bdf0a440646ef4640ab77646
|
|
| MD5 |
6ea494101b36d475fe42f48728bd7362
|
|
| BLAKE2b-256 |
6952c2e9c4bc86f99ebac45ac322067f7d97fd3852fc9d7861248ec18e9746a8
|
Provenance
The following attestation bundles were made for withcache-0.2.0-py3-none-musllinux_1_2_x86_64.whl:
Publisher:
release.yml on safl/withcache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
withcache-0.2.0-py3-none-musllinux_1_2_x86_64.whl -
Subject digest:
503a0dcc69be64f809addd58843b0880be6c7134bdf0a440646ef4640ab77646 - Sigstore transparency entry: 1712330435
- Sigstore integration time:
-
Permalink:
safl/withcache@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/safl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Trigger Event:
push
-
Statement type:
File details
Details for the file withcache-0.2.0-py3-none-musllinux_1_2_aarch64.whl.
File metadata
- Download URL: withcache-0.2.0-py3-none-musllinux_1_2_aarch64.whl
- Upload date:
- Size: 262.6 kB
- Tags: Python 3, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b459671cb6dc507fca34a265688cbe1f58c30bf2a3f06ed65238e5415a47db59
|
|
| MD5 |
e6df813792f51494146a4633a289b9cb
|
|
| BLAKE2b-256 |
4d4311491129a393c52b9d392a1a42c052c2926f1c37612edf3f418bc99f23a9
|
Provenance
The following attestation bundles were made for withcache-0.2.0-py3-none-musllinux_1_2_aarch64.whl:
Publisher:
release.yml on safl/withcache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
withcache-0.2.0-py3-none-musllinux_1_2_aarch64.whl -
Subject digest:
b459671cb6dc507fca34a265688cbe1f58c30bf2a3f06ed65238e5415a47db59 - Sigstore transparency entry: 1712330518
- Sigstore integration time:
-
Permalink:
safl/withcache@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/safl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Trigger Event:
push
-
Statement type:
File details
Details for the file withcache-0.2.0-py3-none-manylinux_2_17_x86_64.whl.
File metadata
- Download URL: withcache-0.2.0-py3-none-manylinux_2_17_x86_64.whl
- Upload date:
- Size: 244.9 kB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b195b8ff6d56bc0ae9b67a61c4f2ff2195218e0ae2ab706d584ef0b5f9517fd1
|
|
| MD5 |
11977f6b3115779e5fcc870eba9d2d83
|
|
| BLAKE2b-256 |
fc463ebf36ca6a6bdc8f678c2e687bd862073b77ea5bcd11b8aec35239d44b18
|
Provenance
The following attestation bundles were made for withcache-0.2.0-py3-none-manylinux_2_17_x86_64.whl:
Publisher:
release.yml on safl/withcache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
withcache-0.2.0-py3-none-manylinux_2_17_x86_64.whl -
Subject digest:
b195b8ff6d56bc0ae9b67a61c4f2ff2195218e0ae2ab706d584ef0b5f9517fd1 - Sigstore transparency entry: 1712330629
- Sigstore integration time:
-
Permalink:
safl/withcache@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/safl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Trigger Event:
push
-
Statement type:
File details
Details for the file withcache-0.2.0-py3-none-manylinux_2_17_aarch64.whl.
File metadata
- Download URL: withcache-0.2.0-py3-none-manylinux_2_17_aarch64.whl
- Upload date:
- Size: 262.6 kB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f593d16629140155f522cf5b0b0493c69834d832c4a9164068cf4ef38932baa
|
|
| MD5 |
0d9d42c8fe1bbd7e78b120eef0fe3918
|
|
| BLAKE2b-256 |
b6290327eeece968f9ea4de63e79b73736adc0942ffb8932b4cea5a4681fc4f5
|
Provenance
The following attestation bundles were made for withcache-0.2.0-py3-none-manylinux_2_17_aarch64.whl:
Publisher:
release.yml on safl/withcache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
withcache-0.2.0-py3-none-manylinux_2_17_aarch64.whl -
Subject digest:
1f593d16629140155f522cf5b0b0493c69834d832c4a9164068cf4ef38932baa - Sigstore transparency entry: 1712330383
- Sigstore integration time:
-
Permalink:
safl/withcache@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/safl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Trigger Event:
push
-
Statement type:
File details
Details for the file withcache-0.2.0-py3-none-any.whl.
File metadata
- Download URL: withcache-0.2.0-py3-none-any.whl
- Upload date:
- Size: 51.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0063a2cf2b0ddf3c39094520f414e1963f6c694d69a4ed4d9bf0b785bedbc407
|
|
| MD5 |
9b73f928c41abfb72f3797ec0e5403dc
|
|
| BLAKE2b-256 |
1192fc9c3ea4f303f600499b89f6403ee6ae8bd2de428724a9c8ad36fa16d497
|
Provenance
The following attestation bundles were made for withcache-0.2.0-py3-none-any.whl:
Publisher:
release.yml on safl/withcache
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
withcache-0.2.0-py3-none-any.whl -
Subject digest:
0063a2cf2b0ddf3c39094520f414e1963f6c694d69a4ed4d9bf0b785bedbc407 - Sigstore transparency entry: 1712330330
- Sigstore integration time:
-
Permalink:
safl/withcache@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/safl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@74bb0813da59f6b975c38c5e8d68c4c37928319d -
Trigger Event:
push
-
Statement type: