2026-09-11 07:01:30 +00:00
|
|
|
|
# pkgwatch — declarative package-update watcher/publisher (working name)
|
|
|
|
|
|
|
|
|
|
|
|
Status: design draft, pre-PoC. Captures the design discussion as of 2026-09-11.
|
|
|
|
|
|
|
|
|
|
|
|
## Problem
|
|
|
|
|
|
|
|
|
|
|
|
Software not packaged by the distro (Arch/Manjaro here) usually gets installed
|
|
|
|
|
|
one of a few ways:
|
|
|
|
|
|
|
|
|
|
|
|
- `curl | sh` from the vendor's own install script — the classic "trust me,
|
|
|
|
|
|
bro." The script and any checksum it embeds share a trust boundary, so it
|
|
|
|
|
|
verifies nothing beyond transport corruption.
|
|
|
|
|
|
- Manual download + manual checksum/signature verification, redone by hand
|
|
|
|
|
|
every time you want to update. Tedious enough that people stop doing it.
|
|
|
|
|
|
- A distro package (pacman `extra`, AUR) — trustworthy, but version-lagged
|
|
|
|
|
|
behind upstream, and someone else has to maintain the PKGBUILD.
|
|
|
|
|
|
|
|
|
|
|
|
There's no local, low-effort way to say "here's how to fetch and verify
|
|
|
|
|
|
package X" once, and have that declaration stay live — checked periodically,
|
|
|
|
|
|
re-verified on every new upstream release, and fed into a normal
|
|
|
|
|
|
`pacman`-based workflow without hand-editing a PKGBUILD each time.
|
|
|
|
|
|
|
2026-09-11 07:10:06 +00:00
|
|
|
|
## Scope
|
|
|
|
|
|
|
|
|
|
|
|
This is a personal tool for a small, curated list of non-critical packages
|
|
|
|
|
|
— not a general-purpose supply-chain-security framework. The concrete
|
|
|
|
|
|
motivating case: software (especially fast-moving AI/ML tooling) that
|
|
|
|
|
|
Manjaro's `extra` repo lags weeks behind upstream on, where raw AUR or a
|
|
|
|
|
|
vendor's curl|sh script are the only faster alternatives today.
|
|
|
|
|
|
|
|
|
|
|
|
**Goal**: a middle ground — fresher than Manjaro's lag, with real
|
|
|
|
|
|
verification where the vendor actually offers something to check, safer
|
|
|
|
|
|
than blindly piping an install script to `sh`.
|
|
|
|
|
|
|
|
|
|
|
|
**Non-goals**:
|
|
|
|
|
|
|
|
|
|
|
|
- Defending against a fully compromised vendor signing/release pipeline.
|
|
|
|
|
|
If upstream's CI or signing key is itself compromised, pkgwatch cannot
|
|
|
|
|
|
and does not try to catch that. Tiers 1–3 (below) raise the bar from
|
|
|
|
|
|
"trust the domain" to "trust the vendor's actual release process"; they
|
|
|
|
|
|
are not a guarantee against that process being subverted.
|
|
|
|
|
|
- Defending against a malicious downgrade specifically. A compromised
|
|
|
|
|
|
version-check source reporting an older version as "latest" is a
|
|
|
|
|
|
downstream/distro-security problem, out of scope here. (See "post-build
|
|
|
|
|
|
version check" below for a related but distinct sanity check — it is
|
|
|
|
|
|
not a security control.)
|
|
|
|
|
|
- Supporting an adversarial or multi-user config. The tracked-package list
|
|
|
|
|
|
is curated by the one person running the daemon on their own machine;
|
|
|
|
|
|
config/state file integrity relies on normal filesystem permissions, not
|
|
|
|
|
|
a hardened trust boundary. Key-pinning friction on rotation (see tiers
|
|
|
|
|
|
below) is acceptable, even desirable, at this scale — it's a personal,
|
|
|
|
|
|
low-volume list, not something that needs to scale painlessly.
|
|
|
|
|
|
|
2026-09-11 07:01:30 +00:00
|
|
|
|
## Vision
|
|
|
|
|
|
|
|
|
|
|
|
A Rust binary, run as a systemd service (service + timer, periodic not
|
|
|
|
|
|
persistent), that:
|
|
|
|
|
|
|
|
|
|
|
|
1. Reads a declarative config of tracked packages — where to check for new
|
|
|
|
|
|
versions, how to fetch the artifact, and how to verify it.
|
|
|
|
|
|
2. On each run, checks each tracked package for a new upstream version.
|
|
|
|
|
|
3. If a new version is found, fetches the artifact and runs the verification
|
|
|
|
|
|
method declared for that package.
|
|
|
|
|
|
4. If verification succeeds (per the package's trust tier — see below),
|
|
|
|
|
|
updates/generates a local PKGBUILD (bump `pkgver`, refresh
|
|
|
|
|
|
`sha256sums`/signature reference) and rebuilds it into a local pacman
|
|
|
|
|
|
repo via `makepkg` + `repo-add`.
|
|
|
|
|
|
5. The next `pacman -Syu` (with the local repo configured) picks up the new
|
|
|
|
|
|
version normally — no separate tooling needed on the consuming side.
|
|
|
|
|
|
|
|
|
|
|
|
This is conceptually `nvchecker` (version checking) + `updpkgsums` (checksum
|
|
|
|
|
|
refresh) + `repo-add` (local repo publishing), fused into one daemon with a
|
|
|
|
|
|
single declarative source of truth, plus an explicit, surfaced trust model
|
|
|
|
|
|
that those tools don't provide.
|
|
|
|
|
|
|
|
|
|
|
|
## Verification trust tiers
|
|
|
|
|
|
|
2026-09-11 07:10:06 +00:00
|
|
|
|
Per the Scope above, the goal is to raise the bar above curl|sh where the
|
|
|
|
|
|
vendor gives us something to check — not to build airtight supply-chain
|
|
|
|
|
|
defense. The central design problem within that goal: from the install UX,
|
|
|
|
|
|
a cryptographically strong
|
2026-09-11 07:01:30 +00:00
|
|
|
|
verification and a "trust-me-bro" same-domain checksum look identical. The
|
|
|
|
|
|
tool's job is to make that difference legible instead of laundering every
|
|
|
|
|
|
package into an undifferentiated "verified" bucket.
|
|
|
|
|
|
|
|
|
|
|
|
Tiers, strongest to weakest:
|
|
|
|
|
|
|
|
|
|
|
|
1. **Pinned-key signature** — GPG/minisign/sigstore-cosign, where the
|
|
|
|
|
|
signing key's fingerprint is pinned in *our* config (not fetched fresh
|
|
|
|
|
|
from the vendor each time). Proves authorship, independent of the
|
|
|
|
|
|
artifact's own hosting.
|
|
|
|
|
|
2. **Build provenance attestation** — GitHub/GitLab attestations, SLSA
|
|
|
|
|
|
provenance. Ties the artifact to a specific CI run and source commit.
|
|
|
|
|
|
Strong, but only as trustworthy as that CI pipeline.
|
|
|
|
|
|
3. **Registry-native signing** — crates.io, PyPI trusted publishing, npm
|
|
|
|
|
|
provenance. Similar strength, scoped to that registry's trust model.
|
|
|
|
|
|
4. **Same-origin checksum file** — a `.sha256`/`.sha256sum` served next to
|
|
|
|
|
|
the artifact by the vendor. Proves transport integrity only. If the
|
|
|
|
|
|
vendor's server or account is compromised, the attacker controls the
|
|
|
|
|
|
artifact and the "verification" in the same move.
|
|
|
|
|
|
5. **Checksum embedded in an install script** — the classic curl|sh case.
|
|
|
|
|
|
The verifier and the thing being verified share a trust boundary.
|
|
|
|
|
|
6. **Nothing** — bare TLS to a domain, no checksum or signature at all.
|
|
|
|
|
|
|
|
|
|
|
|
### Automation posture per tier
|
|
|
|
|
|
|
|
|
|
|
|
This is the load-bearing decision, not just a cosmetic label:
|
|
|
|
|
|
|
|
|
|
|
|
- **Tiers 1–3**: a passing verification is a real trust signal. Safe to
|
|
|
|
|
|
auto-bump, auto-verify, auto-publish unattended.
|
|
|
|
|
|
- **Tiers 4–6**: a passing "verification" only proves internal consistency
|
|
|
|
|
|
of one origin (the checksum and the artifact agree), which tells you
|
|
|
|
|
|
nothing about whether that origin was compromised. For these tiers the
|
|
|
|
|
|
tool should **not** treat a pass as "verified, ship it." Instead: treat a
|
|
|
|
|
|
version/hash *change* as a flag-for-human-review event. The value pkgwatch
|
|
|
|
|
|
adds at these tiers is diff-and-alert (notice something changed, surface
|
|
|
|
|
|
the new hash for a human to look at), not verify-and-trust.
|
|
|
|
|
|
|
2026-09-11 07:10:06 +00:00
|
|
|
|
The post-build version sanity check (see Architecture below) runs
|
|
|
|
|
|
regardless of tier — it's a correctness gate on the build itself, not part
|
|
|
|
|
|
of the trust-tier judgment, and doesn't change this tiering.
|
|
|
|
|
|
|
2026-09-11 07:01:30 +00:00
|
|
|
|
### Surfacing trust, not just gating on it
|
|
|
|
|
|
|
|
|
|
|
|
- Every tracked package carries an explicit tier + one-line justification
|
|
|
|
|
|
(e.g. "minisign, key pinned 2024-03" vs. "same-domain sha256, no
|
|
|
|
|
|
independent signer") in a metadata file alongside the generated PKGBUILD —
|
|
|
|
|
|
something `repo-add`/pacman don't touch, but that a human or `pkgwatch
|
|
|
|
|
|
audit` can read.
|
|
|
|
|
|
- `pkgwatch audit` (or similar) lists all tracked packages sorted
|
|
|
|
|
|
worst-tier-first, so weak links don't hide among strong ones in a repo
|
|
|
|
|
|
that otherwise looks uniformly trustworthy.
|
|
|
|
|
|
|
|
|
|
|
|
## Config schema (draft)
|
|
|
|
|
|
|
|
|
|
|
|
```toml
|
|
|
|
|
|
[package.uv]
|
|
|
|
|
|
source = "github-release"
|
|
|
|
|
|
repo = "astral-sh/uv"
|
|
|
|
|
|
asset_pattern = "uv-x86_64-unknown-linux-gnu.tar.gz"
|
|
|
|
|
|
|
|
|
|
|
|
[package.uv.verification]
|
|
|
|
|
|
tier = 4
|
|
|
|
|
|
method = "same-origin-sha256"
|
|
|
|
|
|
checksum_asset_pattern = "uv-x86_64-unknown-linux-gnu.tar.gz.sha256"
|
|
|
|
|
|
|
2026-09-11 07:10:06 +00:00
|
|
|
|
# Post-build sanity check — correctness only, not a security control.
|
|
|
|
|
|
# Runs the built binary and confirms it reports the version pkgwatch
|
|
|
|
|
|
# believes it just built; mismatch blocks publish.
|
|
|
|
|
|
[package.uv.sanity_check]
|
|
|
|
|
|
command = "uv --version"
|
|
|
|
|
|
version_regex = 'uv (\d+\.\d+\.\d+)'
|
|
|
|
|
|
|
2026-09-11 07:01:30 +00:00
|
|
|
|
# Tier 1 example:
|
|
|
|
|
|
[package.somepkg]
|
|
|
|
|
|
source = "url-with-version-regex"
|
|
|
|
|
|
url = "https://example.com/downloads/"
|
|
|
|
|
|
version_regex = 'somepkg-(\d+\.\d+\.\d+)\.tar\.gz'
|
|
|
|
|
|
|
|
|
|
|
|
[package.somepkg.verification]
|
|
|
|
|
|
tier = 1
|
|
|
|
|
|
method = "minisign"
|
|
|
|
|
|
pinned_key = "RWQ...base64pubkey..."
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Open questions on the schema:
|
|
|
|
|
|
|
|
|
|
|
|
- How much of `nvchecker`'s source-type taxonomy (github, gitlab, pypi,
|
|
|
|
|
|
crates.io, regex, htmlparser, ...) to reimplement vs. shell out to
|
|
|
|
|
|
`nvchecker` itself for the version-check step and own only the
|
|
|
|
|
|
verification + publish pipeline.
|
|
|
|
|
|
- PKGBUILD generation: full Jinja-style templates per package vs. a small
|
|
|
|
|
|
fixed set of PKGBUILD "shapes" (single binary tarball, cargo-install,
|
|
|
|
|
|
etc.) parameterized by the config.
|
|
|
|
|
|
- Where the local pacman repo lives and how it's registered in
|
|
|
|
|
|
`pacman.conf` (one-time manual setup step vs. something pkgwatch manages).
|
|
|
|
|
|
- Failure/alerting channel for tier 4–6 change events — log only, or a
|
|
|
|
|
|
notification hook (this box already has a wofi/Mako notification setup —
|
|
|
|
|
|
see `project_wofi_notification_picker` in Claude's memory).
|
|
|
|
|
|
|
|
|
|
|
|
## Architecture sketch
|
|
|
|
|
|
|
|
|
|
|
|
- **Config loader**: parses the TOML above into an in-memory package list.
|
|
|
|
|
|
- **Checker**: per source type, resolves "what's the latest version" —
|
|
|
|
|
|
likely reuses `nvchecker`'s logic/sources conceptually, possibly shells
|
|
|
|
|
|
out to it initially for the PoC rather than reimplementing every source
|
|
|
|
|
|
type in Rust.
|
|
|
|
|
|
- **Fetcher**: downloads the artifact (and any checksum/signature/
|
|
|
|
|
|
attestation companion) for a resolved version.
|
|
|
|
|
|
- **Verifier**: tier-specific verification implementations behind a common
|
|
|
|
|
|
trait; returns a tier + pass/fail + justification string.
|
2026-09-11 07:10:06 +00:00
|
|
|
|
- **Builder**: for tiers 1–3 on pass, generates/updates the PKGBUILD
|
|
|
|
|
|
(strict validation on any upstream-controlled string — version, filename
|
|
|
|
|
|
— before it touches generated shell content; never unescaped
|
|
|
|
|
|
interpolation) and runs `makepkg`.
|
|
|
|
|
|
- **Sanity checker**: after a successful build, runs the package's
|
|
|
|
|
|
declared `sanity_check.command` against the built artifact and confirms
|
|
|
|
|
|
the reported version matches what pkgwatch believes it just built.
|
|
|
|
|
|
Mismatch = fail loud, do not publish. This is a correctness check, not a
|
|
|
|
|
|
security control — it catches checker bugs and mangled/wrong-artifact
|
|
|
|
|
|
downloads, not malicious releases.
|
|
|
|
|
|
- **Publisher**: runs `repo-add` against the local repo, only after the
|
|
|
|
|
|
sanity check passes.
|
2026-09-11 07:01:30 +00:00
|
|
|
|
- **Reviewer queue**: for tiers 4–6, records the detected change instead of
|
2026-09-11 07:10:06 +00:00
|
|
|
|
auto-building; a separate `pkgwatch review` command lets a human
|
|
|
|
|
|
approve/reject, which then triggers the build → sanity-check → publish
|
|
|
|
|
|
steps above.
|
2026-09-11 07:01:30 +00:00
|
|
|
|
- **Scheduling**: systemd `.service` (oneshot) + `.timer` running it
|
|
|
|
|
|
periodically, matching the pattern already used for other periodic tasks
|
|
|
|
|
|
on this box.
|
|
|
|
|
|
|
|
|
|
|
|
## Prior art / reference points
|
|
|
|
|
|
|
|
|
|
|
|
- `nvchecker` — version-check-only, no verification or publish step.
|
|
|
|
|
|
- `updpkgsums` (pacman-contrib/devtools) — checksum refresh only, manual
|
|
|
|
|
|
trigger.
|
|
|
|
|
|
- `aurutils` — local repo + AUR build automation, but AUR itself carries no
|
|
|
|
|
|
stronger verification guarantee than what each PKGBUILD maintainer does.
|
|
|
|
|
|
- `repology` — cross-distro version tracking, no verification/publish.
|
|
|
|
|
|
- GitHub artifact attestations (`gh attestation verify`) — tier 2 building
|
|
|
|
|
|
block for GitHub-hosted releases.
|
|
|
|
|
|
|
|
|
|
|
|
## Status / next steps
|
|
|
|
|
|
|
2026-09-11 07:10:06 +00:00
|
|
|
|
- [x] Scope decided: personal middle-ground tool for a curated package
|
|
|
|
|
|
list, not a general supply-chain-security framework (see Scope
|
|
|
|
|
|
above). Downgrade attacks and compromised-vendor-pipeline defense
|
|
|
|
|
|
are explicit non-goals.
|
|
|
|
|
|
- [ ] Refine config schema further (see open questions above), including
|
|
|
|
|
|
the `sanity_check` block per package.
|
2026-09-11 07:01:30 +00:00
|
|
|
|
- [ ] Decide version-check strategy: shell out to `nvchecker` vs. own
|
|
|
|
|
|
implementation, for the PoC.
|
2026-09-11 07:10:06 +00:00
|
|
|
|
- [ ] Implement PKGBUILD generation with strict upstream-string validation
|
|
|
|
|
|
from day one (see Builder, above) — cheap to do right up front,
|
|
|
|
|
|
expensive to retrofit.
|
2026-09-11 07:01:30 +00:00
|
|
|
|
- [ ] PoC scope: single tier-4 package (e.g. `uv`, ironically) end-to-end —
|
|
|
|
|
|
check, fetch, same-origin-checksum verify, flag-for-review, manual
|
2026-09-11 07:10:06 +00:00
|
|
|
|
approve, build, post-build version sanity check, local repo publish.
|
2026-09-11 07:01:30 +00:00
|
|
|
|
- [ ] Decide on project home: local-only for now, or push to
|
|
|
|
|
|
code.austinschaefer.com (Forgejo) once the spec settles.
|