Always current

kapsl has no upgrade command, by design. There is nothing to upgrade: a tool named without a pinned digest is expected to stay current, and the machinery that keeps it current is the same machinery that keeps it patched. This page is that machinery — three clocks, one heal loop, and the trade you make when you opt out.

The three clocks

Three independent freshness windows run in the background of every invocation. All three are configurable in kapsl.toml; the defaults are what is shown:

ClockDefaultWhat it moves
image_max_age_secs7 daysA cached image on a floating tag (python:3.14, rg:latest) older than this is re-pulled before a run. Best-effort: a failed refresh falls back to the cached image — the scan gate, not the freshness window, is the security backstop.
index_max_age_secs24 hoursA background check for a new tool catalogue. A conditional GET: a 304 changes nothing, and the payload is downloaded only when the pointer's version moved.
security_scan_max_age_secs24 hoursHow long a passing scan verdict is trusted. A rebuilt image gets a new digest and re-scans immediately; the window applies to the same bytes.

Two details worth knowing. First, the clocks are best-effort by construction — a network hiccup never blocks a run; it delays a refresh by a window. Second, one path tightens the catalogue window deliberately: when the tool you asked for is not in the cached catalogue, the refresh probe runs on a 30-minute window instead of 24 hours, because in that one case a stale catalogue is a failed run, not just an old one. The most likely cause of a miss is a tool published after your last check, and that is the case worth paying for.

The catalogue carries no versions at all — no release lines, no dates (The catalogue) — so "which bytes did python run as" is answered by the index site and the registry, not by the document the client resolves against. The line table a tool publishes is visible per tool:

kapsl --info python
  ■ INFO       python

▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔
   The Python programming language interpreter
 
   IMAGE        ghcr.io/kapsl-sh/python:latest
   digest       sha256:619e…b645   59 MB · 1 layers
 
   LINES        RESOLVES TO
 @latest      3.14.7            in use
   @3.14        3.14.7
   @3.13        3.13.15
   @3.12        3.12.14
   @3.11        3.11.16
   @3.10        3.10.21
                lines float — kapsl carries security updates onto them
                pin one with python@<version> · a digest never moves
 
   CAPS          rw 
   SECCOMP      net-listen (no class declared)  (tier: default)
 
────────────────────────────────────────────────────────────────────────────────
   RUN   kapsl python      SHIM   kapsl -i python

Every line above is a floating tag: @3.12 does not mean "3.12.14 forever", it means "the newest 3.12.x build", and it advances as upstream security updates land and the line is rebuilt. That is what keeps a maintained line patched without anyone running anything.

The CVE-heal loop

The clocks and the scanner are not two systems. The scan fails on a floating tag — CRITICAL findings, or the verdict you declined — and kapsl does not just block: it gives the tag one chance to heal. The re-pull happens, and if the tag moved, the new bytes are re-scanned, because the rebuild may have fixed the vulnerability upstream. If the tag had not moved — same digest, same bytes — the block stands: re-scanning the same image would only re-find the same CVEs. Digest-pinned and locally-built images skip the retry entirely; a refresh cannot change what they refer to.

So the sequence for a tool with a newly-fixed critical CVE is: block → re-pull → clean → run. Nobody ran an upgrade. The fix arrived with the image that the freshness machinery was already tracking, and the scan is what confirmed it.

Why this is security, not convenience

In a pip install --upgrade-or-never world, the update is a human decision with a schedule, and the time between "upstream shipped the patch" and "your box got it" is the supply-chain attack window — and the transitive dependencies your tool pulled are patched by someone else's upgrade, if any. The pipeline above removes the schedule: the same mechanism that keeps the tag current is the one that rebuilds on upstream fixes and re-scans the result before anything runs. Current is the default state, and the scan gate is the check that "current" actually means "clean".

That is also why pinning is stated as a trade, not a feature. A digest pin (python@sha256:… in your configuration) is opting out of currency: bit-for-bit reproducibility, yes — and no floating, no re-pull, no rebuilds carrying security updates onto it. The pin is exactly the bytes you named, forever, and the scan still runs on it every window. For reproducibility that is the point, and it is CI's job, not your laptop's: CI/CD covers the pinning workflow — digest pin in the pipeline for bit-for-bit runs, floating tags for "current and clean".

Old version lines age deliberately

A version line upstream stops maintaining is not deleted and not quietly dropped: the index resolves each project's lines from upstream's own support calendar, and a line past end-of-life is reported, never rebuilt, never deleted. Its tag keeps resolving, kept so you can reproduce a bug or test against several versions of a tool — not maintained as a distribution channel. Reach for an old line when you need that version, and expect it to be vulnerable: the scan still runs on old lines by default, and its findings stand in for the security updates the line no longer gets. A test setup that accepts that risk opts out per run with --skip-scan (or globally, at the cost of the whole gate — see Scanned before it runs).

What you pinWhat movesWhat you give up
Nothing (kapsl python)The line floats, the line's build floatsNothing — this is the default
A version line ([email protected])Builds within the line float, carrying security updatesNewer lines — 3.12 does not become 3.13
A digest (python@sha256:…)NothingCurrency entirely — no re-pull, no rebuilds, no patches