Harness pool: quarantine and scheduled refresh
The developer harness pool ($XDG_DATA_HOME/wraptool/harness-pool) installs a coding harness once and shares it across every project (see Getting started and Dev Containers for the mount mechanics). This page covers its update-safety story — quarantine — and the one piece of upkeep it needs from you: a periodic wraptool harness refresh.
Why quarantine, and why refresh matters
wraptool harness update does not just grab the newest release. Before a candidate version can be selected it must clear two independent age checks (design/harness/unified-harness-pool.md §9):
now - published_at >= minimum_release_age # registry publish time
now - first_seen_at >= minimum_observation_age # this pool's own clock
minimum_release_age entirely
minimum_release_age only means something measured against a real upstream publication timestamp. Some sources don’t have one: the agy (Antigravity) archive channel carries no publish-history field at all, and occasionally an npm package is missing its registry time metadata too. For exactly these candidates the pool synthesizes published_at from first_seen_at (its own first-refresh clock read) instead — and, because that synthesized value isn’t a real publish timestamp, pool.Eligible (and the list quarantine countdown) skip the minimum_release_age check entirely for them. Their only quarantine is minimum_observation_age, measured against the real first_seen_at.
The shipped default for minimum_observation_age is 0h. Put together: for agy today, out of the box, that is zero quarantine — a freshly refreshed candidate is eligible immediately, with no age check of any kind standing between it and install/update. This is deliberate (a source with no publish history would otherwise hit an unconditional wall on every first install — see internal/pool/eligibility.go’s PublishedAtSynthesized doc comment for the full ruling), but it means the scheduled-refresh habit this page argues for is not just about making minimum_observation_age count sooner for npm-sourced harnesses — for a synthesized-timestamp source like agy it is the entire quarantine story. Set a nonzero minimum_observation_age if you want any time-based protection at all there.
minimum_release_age (default seven days, policy set --minimum-release-age) protects against a drive-by release published on compromised maintainer credentials: a version pulled or reported during its quarantine window is never downloaded. It only needs the registry’s own publish timestamp, so it works even against a pool that has never run before.
minimum_observation_age (default 0h, opt-in) is different: it is not about when upstream published a release, it is about how long this pool has known the release exists.
# Require 48h of this pool's own observation before a candidate is eligible —
# the same syntax as minimum-release-age above (--harness scopes it).
wraptool harness policy set --minimum-observation-age 48hfirst_seen_at is recorded the first time refresh observes a given (source, package, version) triple, and it is write-once — repeated refreshes never move it forward or back. A pool that never runs refresh never records a first_seen_at, which means minimum_observation_age never starts counting for anything new: the countdown can only begin once metadata has actually been fetched at least once. If you set minimum_observation_age above zero and only ever refresh right before an update, you get the friction (a release looks freshly observed and stays quarantined) without the benefit (an attacker who compromises a release right after you observed it still slips through on your very next update). Refreshing on a schedule — independent of when you actually intend to update — is what makes the observation window mean anything.
Metadata-only refresh is exactly the right shape for a timer: it takes the pool’s advisory lock briefly (§6.5), talks only to the registry (no container/backend, no npm/docker/guix involved for an npm-sourced harness’s metadata, and none at all for the archive-sourced ones), and never downloads a package payload or changes what is active. Running it daily or weekly costs nothing at update time and only helps.
# Metadata only for every harness the manifest already lists; safe for a
# timer, and required for minimum_observation_age to ever start counting.
wraptool harness refreshWith no harness argument this only touches harnesses the pool manifest already lists (i.e. that wraptool harness install has recorded intent for) — nothing to refresh before a first install.
The release source (omp)
Oh My Pi (omp) comes from a third kind of pool source, the release source. It acquires one binary per platform from a GitHub release and locks it by the release’s SHA256SUMS.txt. No guix, docker, or podman is involved.
- Versions and publish times come from the npm packument of
@oh-my-pi/pi-coding-agent, sominimum_release_ageapplies against a real publish timestamp.ompreleases several times a day, and the default seven days keeps the pool behind upstream. - The lock is the checksum file. A platform missing from it is left out of the lock. A release with no checksum file, or no Linux asset, cannot be locked: the error says the assets may not be published yet, or that the release predates checksum publication. Releases before 17.2.10 have binaries but no checksum file, so
--constraintandrollback --tocannot reach them. - Downloads cover only the platform being realized, and each is checked against its SHA-256 before it enters the cache. A download aborts after 60 seconds without receiving data, but has no overall time limit, so a slow link still completes. An aborted download restarts from zero.
gckeeps a cache object while the lock names it.
The checksum file and the binary share an origin, so the lock proves the download matches the release, not that the release is genuine. Quarantine mitigates a bad release. It does not prevent one.
A pool harness must not update itself
Quarantine and the lock only hold while the harness runs the binary the pool built. A harness with its own self-updater can step around both. It fetches whatever upstream calls latest, with no quarantine, and puts it wherever it can write.
This happened with claude in a Dev Container. The pool’s runtime/ mount is read-only, so the updater could not replace the pool binary. Instead it ran npm install -g and the copy landed in the node feature’s writable npm prefix. That directory comes before the pool’s runtime/bin on PATH, which is appended so the pool never shadows a project tool. So the next claude started the unlocked copy.
The wraptool Dev Container feature and the Guix runtime set DISABLE_AUTOUPDATER=1, which turns off claude’s updater. Another harness needs its own switch. Whether omp updates itself is still open (see design/harness/omp-release-source.md §9). To check which binary a container runs, compare command -v claude with /opt/wraptool-harness/runtime/bin.
Scheduling it
Linux: a systemd user timer
Two unit files under ~/.config/systemd/user/:
~/.config/systemd/user/wraptool-harness-refresh.service:
[Unit]
Description=Refresh wraptool harness pool candidate metadata
[Service]
Type=oneshot
ExecStart=%h/go/bin/wraptool harness refresh(Adjust ExecStart to wherever your wraptool binary actually lives — %h expands to your home directory.)
~/.config/systemd/user/wraptool-harness-refresh.timer:
[Unit]
Description=Run wraptool harness refresh daily
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.targetEnable it:
systemctl --user daemon-reload
systemctl --user enable --now wraptool-harness-refresh.timerPersistent=true catches up a missed run (laptop suspended overnight) on the next login rather than silently skipping it. A weekly cadence (OnCalendar=weekly) is just as reasonable if minimum_observation_age is short or unset — the point is regularity, not frequency.
macOS: launchd
~/Library/LaunchAgents/com.wraptool.harness-refresh.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.wraptool.harness-refresh</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/wraptool</string>
<string>harness</string>
<string>refresh</string>
</array>
<key>StartInterval</key>
<integer>86400</integer>
<key>RunAtLoad</key>
<false/>
</dict>
</plist>(Adjust the ProgramArguments path to wherever wraptool is installed — which wraptool on the machine you’re loading this on.) Load it with the modern launchctl bootstrap form (targets your per-user GUI domain explicitly, unlike the older, ambiguous load):
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.wraptool.harness-refresh.plistOn an older macOS without bootstrap, fall back to launchctl load ~/Library/LaunchAgents/com.wraptool.harness-refresh.plist.
StartInterval is in seconds (86400 = daily); launchd runs the job roughly on that cadence and catches up a missed run once the machine wakes, similar in spirit to systemd’s Persistent=true.
What’s already shipped versus what’s still deliberately missing
The rest of §16 Phase 5 (“quarantine automation”) in design/harness/unified-harness-pool.md is implemented, not just planned:
- Local observations and release-age eligibility (item 2):
refreshrecords first-seen metadata inobservations.json;wraptool harness list’sELIGIBLE/QUARANTINEDcolumns andupdate/installthemselves enforce bothminimum_release_ageandminimum_observation_agebefore a candidate is selected. - Bypass audit events and candidate warnings (item 3):
--allow-young --reason '...'oninstall/updateexplicitly bypasses quarantine, prints the candidate’s age, and appends anallow_youngevent to the pool-localaudit.jsonl— it is never implied by any other flag.wraptool harness listshows a live countdown (e.g.2.1.221 (3d left)) for anything still quarantined, so the wait is visible before you reach for the bypass. - The bundled Node runtime is a locked input too (
design/harness/node-runtime-tracking.md): for an npm-sourced harness,refreshalso fetchesnodejs.org/dist/index.jsonfor the harness’s pinned Node line and records each patch exactly like a package candidate — the sameminimum_release_age/minimum_observation_agequarantine applies, with no bypass: there is no--allow-youngequivalent for node, so a quarantined line is a hard, actionable error until it clears policy naturally.updatelocks the newest eligible observed patch and folds it intorecipe_revision, so a routine node security release flows through the same refresh/update self-heal a package version bump already gets — no code change needed for a patch, only for moving to a new major line.
Item 1 (this page) is scheduling guidance — refresh itself has run the observation clock since Phase 1; a systemd timer or launchd job is the missing piece that makes minimum_observation_age actually protective instead of accidentally decorative.
Item 4, optional upstream signature/transparency adapters, remains deliberately unimplemented. Quarantine’s time delay is a real, working mitigation on its own (§9) — it complements digest/integrity verification, exact dependency locking, no-network realization, and rollback — but it does not require or wait on any particular registry’s signing scheme. If a source you use gains verifiable signatures or a transparency log, that is a future addition, not a prerequisite for anything documented here.