Add initial design spec for declarative package-update watcher
Captures the verification trust-tier model (pinned-key signatures down to trust-me-bro install scripts) and the daemon architecture discussed: check -> fetch -> tier-aware verify -> PKGBUILD gen -> local repo publish, with weak-tier changes routed to human review instead of auto-publish. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
This commit is contained in:
commit
0fa994f60a
1 changed files with 176 additions and 0 deletions
176
SPEC.md
Normal file
176
SPEC.md
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# 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 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.
|
||||
|
||||
### 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 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.
|
||||
- **Publisher**: for tiers 1–3 on pass, generates/updates the PKGBUILD,
|
||||
runs `makepkg`, runs `repo-add` against the local repo.
|
||||
- **Reviewer queue**: for tiers 4–6, 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.
|
||||
Loading…
Reference in a new issue