pkgwatch/SPEC.md

177 lines
8.1 KiB
Markdown
Raw Normal View History

# 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.
## 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
The central design problem: from the install UX, a cryptographically strong
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 13**: a passing verification is a real trust signal. Safe to
auto-bump, auto-verify, auto-publish unattended.
- **Tiers 46**: 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.
### 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"
# 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 46 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.
- **Publisher**: for tiers 13 on pass, generates/updates the PKGBUILD,
runs `makepkg`, runs `repo-add` against the local repo.
- **Reviewer queue**: for tiers 46, records the detected change instead of
auto-publishing; a separate `pkgwatch review` command lets a human
approve/reject, which then triggers the publish step.
- **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
- [ ] Refine config schema further (see open questions above).
- [ ] Decide version-check strategy: shell out to `nvchecker` vs. own
implementation, for the PoC.
- [ ] PoC scope: single tier-4 package (e.g. `uv`, ironically) end-to-end —
check, fetch, same-origin-checksum verify, flag-for-review, manual
approve, PKGBUILD generation, local repo publish.
- [ ] Decide on project home: local-only for now, or push to
code.austinschaefer.com (Forgejo) once the spec settles.