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.
|
|
|
|
|
|
|
Add ARCHITECTURE.md and apply it to this PR's code
Researched current industry practice on code organization/maintainability
(Ousterhout's deep modules and information hiding, package-by-feature vs.
package-by-layer, functional-core/imperative-shell testability, tech-debt
prevention via ADR-equivalent inline rationale) and wrote it into
ARCHITECTURE.md as a set of concrete, project-specific rules rather than
a generic essay — each principle cites a real example already in this
codebase or fixed by this commit. Cross-linked from SPEC.md, which stays
about product design, not code organization.
Applied it to this PR's own code:
- Pulled process_package/fetch_and_verify/build_and_publish/run_review/
approve out of main.rs into a new pipeline.rs. main.rs's own main() had
grown to 278 lines and zero tests by treating "it's just the entry
point" as an excuse to skip separating logic from wiring; now main.rs
is argv dispatch only.
- Extracted decide_tier_action as a pure function (verification outcome +
pending-state -> what to do), replacing dispatch logic that was
previously inlined into a function that also made the real network/
build calls. Four unit tests, no I/O, covering all four outcomes.
- Added a `//!` module doc comment to every file touched in this branch,
each stating that module's one job in a sentence, per the "deep
modules" principle the spec argues for.
Coverage's reported total drops (94% -> 78%) because pipeline.rs is
deliberately NOT excluded from it the way main.rs is, even though it's
mostly the same kind of untestable I/O orchestration — excluding it would
hide decide_tier_action's real unit-test coverage along with the untested
parts. Noted inline in Makefile.toml/ci.yml so the number doesn't look
like a quality regression at a glance.
Also added a project reference memory pointing at ARCHITECTURE.md rather
than duplicating its content there, per this session's own memory-hygiene
rules (architecture/conventions are derivable from the repo and shouldn't
be duplicated somewhere that can go stale).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:42:18 +00:00
|
|
|
|
This is the *product* design — what pkgwatch does and why. For how the
|
|
|
|
|
|
code implementing it is organized (module boundaries, testability
|
|
|
|
|
|
conventions, what CI does and doesn't enforce), see `ARCHITECTURE.md`.
|
|
|
|
|
|
|
2026-09-11 07:01:30 +00:00
|
|
|
|
## 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
|
Design for scale: many tracked packages, not multi-user
Corrects the earlier non-goal framing: per-user scale (tracking dozens of
packages) is explicitly in scope, distinct from multi-user/adversarial
config trust, which stays out. Adds a Scaling section covering review-queue
fatigue at volume, per-package check cadence, packages.d/ config layout,
audit-as-core, local repo retention, and GitHub rate limits/staggering.
Also settles the GitHub push-notification question: no true webhook push
for repos we don't own, and a relay-based alternative would need a public
inbound receiver this box's WireGuard-only posture deliberately avoids.
Settles on outbound-only github-atom/conditional github-api polling
instead, added to the config schema as check_method/check_interval.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 07:24:13 +00:00
|
|
|
|
below) is acceptable, even desirable, regardless of list size — it's
|
|
|
|
|
|
about who's allowed to add a trust decision, not about volume.
|
|
|
|
|
|
|
|
|
|
|
|
This is a narrower non-goal than "doesn't need to scale" — see **Scaling to
|
|
|
|
|
|
many packages**, below. If pkgwatch actually solves the release-cadence
|
|
|
|
|
|
problem, the natural outcome is tracking many packages, not a handful, and
|
|
|
|
|
|
the design should hold up under that.
|
2026-09-11 07:10:06 +00:00
|
|
|
|
|
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,
|
Design for scale: many tracked packages, not multi-user
Corrects the earlier non-goal framing: per-user scale (tracking dozens of
packages) is explicitly in scope, distinct from multi-user/adversarial
config trust, which stays out. Adds a Scaling section covering review-queue
fatigue at volume, per-package check cadence, packages.d/ config layout,
audit-as-core, local repo retention, and GitHub rate limits/staggering.
Also settles the GitHub push-notification question: no true webhook push
for repos we don't own, and a relay-based alternative would need a public
inbound receiver this box's WireGuard-only posture deliberately avoids.
Settles on outbound-only github-atom/conditional github-api polling
instead, added to the config schema as check_method/check_interval.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 07:24:13 +00:00
|
|
|
|
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.
|
2026-09-11 07:01:30 +00:00
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
Design for scale: many tracked packages, not multi-user
Corrects the earlier non-goal framing: per-user scale (tracking dozens of
packages) is explicitly in scope, distinct from multi-user/adversarial
config trust, which stays out. Adds a Scaling section covering review-queue
fatigue at volume, per-package check cadence, packages.d/ config layout,
audit-as-core, local repo retention, and GitHub rate limits/staggering.
Also settles the GitHub push-notification question: no true webhook push
for repos we don't own, and a relay-based alternative would need a public
inbound receiver this box's WireGuard-only posture deliberately avoids.
Settles on outbound-only github-atom/conditional github-api polling
instead, added to the config schema as check_method/check_interval.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 07:24:13 +00:00
|
|
|
|
## Scaling to many packages
|
|
|
|
|
|
|
|
|
|
|
|
At a handful of tracked packages, several design choices above are
|
|
|
|
|
|
invisible non-issues. At dozens, they become real:
|
|
|
|
|
|
|
|
|
|
|
|
- **Review-queue fatigue.** Most real-world packages will land in tiers
|
|
|
|
|
|
4–6 (bare GitHub release, no signing, is the common case, not the
|
|
|
|
|
|
exception). If every release of every tracked package produces one
|
|
|
|
|
|
review-and-approve event, the queue turns into something rubber-stamped
|
|
|
|
|
|
to clear it — which degrades the already-weak tier 4–6 review into pure
|
|
|
|
|
|
theater, worse than the single-package case. Mitigation: batch same-tier,
|
|
|
|
|
|
low-signal changes (patch-version bump, no maintainer/key change) into a
|
|
|
|
|
|
digest, and reserve individual review prompts for changes that look more
|
|
|
|
|
|
significant (new signing key, jump of more than one minor version, new
|
|
|
|
|
|
maintainer/publisher identity where that's knowable).
|
|
|
|
|
|
- **Per-source check cadence, not one global interval.** The original
|
|
|
|
|
|
motivating problem — some software moves weekly, some quarterly — argues
|
|
|
|
|
|
against checking everything on the same timer tick. See "Check method"
|
|
|
|
|
|
below for the GitHub-specific answer; other source types likely want an
|
|
|
|
|
|
explicit fast/normal/slow interval field per package rather than one
|
|
|
|
|
|
daemon-wide interval.
|
|
|
|
|
|
- **Config as a directory, not one file.** `packages.d/*.toml` (one file
|
|
|
|
|
|
per package), loaded as a directory — same convention as
|
|
|
|
|
|
sudoers.d/systemd drop-ins — scales better than a single growing TOML
|
|
|
|
|
|
file: easier to add/remove/diff one package, plays nicer with putting
|
|
|
|
|
|
the config itself under version control.
|
|
|
|
|
|
- **`pkgwatch audit` becomes core, not peripheral.** At 3 packages you
|
|
|
|
|
|
remember the trust tiers by heart. At 30 you don't. The audit/surfacing
|
|
|
|
|
|
command (see "Surfacing trust," above) is what keeps the weak tiers safe
|
|
|
|
|
|
to have around at all once the list is too big to hold in your head.
|
|
|
|
|
|
- **Local repo retention.** `makepkg`/`repo-add` output accumulates. Needs
|
|
|
|
|
|
a "keep last N versions per package" prune step, or disk fills quietly
|
|
|
|
|
|
over time.
|
|
|
|
|
|
- **Rate limits become real.** Enough GitHub-sourced packages checked on
|
|
|
|
|
|
the same schedule can hit unauthenticated API limits — argues for an
|
|
|
|
|
|
optional auth token in config, and/or staggering check times across
|
|
|
|
|
|
packages rather than firing every check on the same tick.
|
|
|
|
|
|
- **Template reuse matters more.** Already an open schema question below,
|
|
|
|
|
|
but at scale "a small fixed set of parameterized PKGBUILD shapes" clearly
|
|
|
|
|
|
wins over "bespoke template per package" on maintenance-burden grounds
|
|
|
|
|
|
alone, not just taste.
|
|
|
|
|
|
|
|
|
|
|
|
### Check method: polling vs. push (GitHub specifically)
|
|
|
|
|
|
|
|
|
|
|
|
True server-initiated push isn't available for repos you don't own —
|
|
|
|
|
|
GitHub webhooks require admin access on the repo being watched, which
|
|
|
|
|
|
rules them out for upstream projects you're only consuming. A third-party
|
|
|
|
|
|
relay (e.g. newreleases.io) could convert this into a webhook on your end,
|
|
|
|
|
|
but that requires a publicly reachable HTTPS receiver on this box, which
|
|
|
|
|
|
cuts against the existing WireGuard-only/no-public-SSH posture for real
|
|
|
|
|
|
inbound exposure and a purely cosmetic latency win — refreshing a local
|
|
|
|
|
|
pacman repo doesn't need sub-minute notification.
|
|
|
|
|
|
|
|
|
|
|
|
The practical middle ground, outbound-only:
|
|
|
|
|
|
|
|
|
|
|
|
- **`github-atom` check method**: poll `https://github.com/<owner>/<repo>/releases.atom`.
|
|
|
|
|
|
Public, unauthenticated, and (worth reconfirming at implementation time)
|
|
|
|
|
|
historically not counted against the REST API rate limit — cheap enough
|
|
|
|
|
|
to poll every few minutes, getting close to push-latency for the
|
|
|
|
|
|
GitHub-hosted slice of tracked packages without any inbound exposure.
|
|
|
|
|
|
- **`github-api` check method**: for anything the Atom feed doesn't cover
|
|
|
|
|
|
(asset-level metadata, attestations), use conditional GETs
|
|
|
|
|
|
(`If-None-Match`/ETag) against `api.github.com` — a `304 Not Modified`
|
|
|
|
|
|
historically didn't consume rate-limit quota either, so frequent polling
|
|
|
|
|
|
stays cheap even on the real API.
|
|
|
|
|
|
- Non-GitHub sources still need the per-package interval field above;
|
|
|
|
|
|
there's no equivalent free-to-poll feed for most of them.
|
|
|
|
|
|
|
2026-09-11 07:01:30 +00:00
|
|
|
|
## Config schema (draft)
|
|
|
|
|
|
|
|
|
|
|
|
```toml
|
|
|
|
|
|
[package.uv]
|
|
|
|
|
|
source = "github-release"
|
|
|
|
|
|
repo = "astral-sh/uv"
|
|
|
|
|
|
asset_pattern = "uv-x86_64-unknown-linux-gnu.tar.gz"
|
Design for scale: many tracked packages, not multi-user
Corrects the earlier non-goal framing: per-user scale (tracking dozens of
packages) is explicitly in scope, distinct from multi-user/adversarial
config trust, which stays out. Adds a Scaling section covering review-queue
fatigue at volume, per-package check cadence, packages.d/ config layout,
audit-as-core, local repo retention, and GitHub rate limits/staggering.
Also settles the GitHub push-notification question: no true webhook push
for repos we don't own, and a relay-based alternative would need a public
inbound receiver this box's WireGuard-only posture deliberately avoids.
Settles on outbound-only github-atom/conditional github-api polling
instead, added to the config schema as check_method/check_interval.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 07:24:13 +00:00
|
|
|
|
check_method = "github-atom" # or "github-api"; see Scaling > Check method
|
|
|
|
|
|
check_interval = "5m" # per-package, not a global daemon interval
|
2026-09-11 07:01:30 +00:00
|
|
|
|
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
# Verified 2026-09-11 against the real repo: uv publishes GitHub
|
|
|
|
|
|
# build-provenance attestations (sigstore bundle) for every release asset
|
|
|
|
|
|
# — tier 2, not the tier-4 same-origin-sha256 originally guessed here.
|
|
|
|
|
|
# Checked via `gh attestation verify` rather than reimplementing sigstore
|
|
|
|
|
|
# verification in Rust. Tier is *derived* from `method`, not stored
|
|
|
|
|
|
# separately — the PoC found that storing both invites a tier/method
|
|
|
|
|
|
# mismatch that would mean nothing (see `Verification::tier()` in the PoC).
|
2026-09-11 07:01:30 +00:00
|
|
|
|
[package.uv.verification]
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
method = "github-attestation"
|
2026-09-11 07:01:30 +00:00
|
|
|
|
|
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+)'
|
|
|
|
|
|
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
# Tier 4 example — same-origin checksum only, proves transport integrity,
|
|
|
|
|
|
# not authorship (this is what the uv example above was, until checked):
|
|
|
|
|
|
[package.otherpkg]
|
|
|
|
|
|
source = "github-release"
|
|
|
|
|
|
repo = "someorg/otherpkg"
|
|
|
|
|
|
asset_pattern = "otherpkg-x86_64-unknown-linux-gnu.tar.gz"
|
|
|
|
|
|
|
|
|
|
|
|
[package.otherpkg.verification]
|
|
|
|
|
|
method = "same-origin-sha256"
|
|
|
|
|
|
checksum_asset_pattern = "otherpkg-x86_64-unknown-linux-gnu.tar.gz.sha256"
|
|
|
|
|
|
|
Close the loop: build, sanity-check, and publish for the first time
Implements the last unimplemented pipeline stage from SPEC.md: PKGBUILD
generation + makepkg (builder.rs), a post-build version sanity check
(sanity.rs), and repo-add publishing (publisher.rs), wired into main.rs
for both the tier 1-3 auto-publish path and a new tier 4-6 review queue
(`pkgwatch review` / `pkgwatch review <name> --approve`, persisted via
state::{load,save,clear}_pending_version, tracked separately from
last-published-version since approving one release isn't a standing
auto-publish grant for future ones).
Publishing targets an existing, already-registered local pacman repo
(~/.local/share/pacman/custom, `[custom]` in /etc/pacman.conf) rather
than one pkgwatch invents — found already in real use for a hand-packaged
AppImage, which resolves SPEC's open question on where the repo lives
without pkgwatch ever touching pacman.conf. Publishing stops at
`repo-add`; actually installing/upgrading (`pacman -Syu`/`pacman -S`) is
left to the operator, not run automatically.
Getting a real second package (scaleway-cli, tier 4) through the new
pipeline immediately surfaced a real gap: its pacman package is named
`scaleway-cli` but the actual binary is `scw` (confirmed via `pacman -Ql`
against the currently-installed extra package) — without a way to
declare that, the build would install alongside extra's package under
the wrong name instead of shadowing it. Added `Package::binary_name`
(config.rs) to cover it.
Every upstream-controlled string (version, asset name, download URL)
is validated before it touches generated shell content in the PKGBUILD
template — rejects anything containing a single quote or newline, since
values are embedded in single-quoted bash strings.
Verified for real, end to end: uv (tier 2) auto-built and published
against the real astral-sh/uv release with no human step; scaleway-cli
(tier 4) queued for review, then approved via `pkgwatch review
scaleway-cli --approve`, which re-verified, built, and published it —
confirmed the built package contains exactly usr/bin/scw. Both landed in
the real custom repo's database. Left scaleway-cli's real-repo review
pending rather than approving it myself: the tier 4-6 gate exists for a
human judgment call, not the agent's.
69 tests, cargo make ci clean (fmt, clippy, complexity, coverage, audit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:13:13 +00:00
|
|
|
|
# binary_name: only needed when the installed binary's name differs from
|
|
|
|
|
|
# the pacman package name — e.g. real-world case, scaleway-cli's package
|
|
|
|
|
|
# is named scaleway-cli but its actual binary is `scw` (see
|
|
|
|
|
|
# packages.d/scaleway-cli.toml). Defaults to the package name.
|
|
|
|
|
|
binary_name = "otherbin"
|
|
|
|
|
|
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
# Tier 1 example — not yet implemented in the PoC (only
|
|
|
|
|
|
# same-origin-sha256 and github-attestation exist so far):
|
2026-09-11 07:01:30 +00:00
|
|
|
|
[package.somepkg]
|
|
|
|
|
|
source = "url-with-version-regex"
|
|
|
|
|
|
url = "https://example.com/downloads/"
|
|
|
|
|
|
version_regex = 'somepkg-(\d+\.\d+\.\d+)\.tar\.gz'
|
|
|
|
|
|
|
|
|
|
|
|
[package.somepkg.verification]
|
|
|
|
|
|
method = "minisign"
|
|
|
|
|
|
pinned_key = "RWQ...base64pubkey..."
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-09-17 07:00:53 +00:00
|
|
|
|
**PoC status** (see `src/`, `packages.d/uv.toml`, `packages.d/scaleway-cli.toml`):
|
|
|
|
|
|
implements `repo`, `asset_pattern`, and `verification.method`
|
|
|
|
|
|
(`same-origin-sha256` | `github-attestation` only), loaded from
|
|
|
|
|
|
`packages.d/*.toml`. Confirmed working end to end against two real repos:
|
|
|
|
|
|
|
|
|
|
|
|
- `astral-sh/uv` — `github-atom` feed → fetch → `gh attestation verify`
|
|
|
|
|
|
(tier 2) → state persisted so a second run reports "up to date."
|
|
|
|
|
|
- `scaleway/scaleway-cli` — first live exercise of `same-origin-sha256`
|
|
|
|
|
|
(tier 4). Verified upstream ships no build-provenance attestations
|
|
|
|
|
|
(`attestations` API 404s), so this is genuinely tier 4, not an
|
|
|
|
|
|
under-verified tier 2. Surfaced two schema/implementation gaps beyond
|
|
|
|
|
|
what uv exercised, both now handled:
|
|
|
|
|
|
- Release asset names embed the version
|
|
|
|
|
|
(`scaleway-cli_2.62.0_linux_amd64`), unlike uv's static names.
|
|
|
|
|
|
`asset_pattern`/`checksum_asset_pattern` now support a `{version}`
|
|
|
|
|
|
placeholder, substituted via `checker::version_from_tag` (which also
|
|
|
|
|
|
strips a tag's leading `v`, since scaleway-cli tags `vX.Y.Z` but
|
|
|
|
|
|
filenames use the bare version).
|
|
|
|
|
|
- Checksums ship as one combined `SHA256SUMS` (one line per platform
|
|
|
|
|
|
asset) rather than a per-asset file like uv's — the verifier now
|
|
|
|
|
|
matches the line by filename instead of assuming a single-hash file.
|
|
|
|
|
|
- Separately, scaleway-cli's Atom feed lists a `vX.Y.Z-dbg1` tag newest,
|
|
|
|
|
|
with no real Release object behind it (`releases/tags/<tag>` 404s) —
|
|
|
|
|
|
`checker::latest_github_release` now confirms each feed candidate
|
|
|
|
|
|
against the releases API in feed order rather than trusting the first
|
|
|
|
|
|
entry outright.
|
|
|
|
|
|
|
Close the loop: build, sanity-check, and publish for the first time
Implements the last unimplemented pipeline stage from SPEC.md: PKGBUILD
generation + makepkg (builder.rs), a post-build version sanity check
(sanity.rs), and repo-add publishing (publisher.rs), wired into main.rs
for both the tier 1-3 auto-publish path and a new tier 4-6 review queue
(`pkgwatch review` / `pkgwatch review <name> --approve`, persisted via
state::{load,save,clear}_pending_version, tracked separately from
last-published-version since approving one release isn't a standing
auto-publish grant for future ones).
Publishing targets an existing, already-registered local pacman repo
(~/.local/share/pacman/custom, `[custom]` in /etc/pacman.conf) rather
than one pkgwatch invents — found already in real use for a hand-packaged
AppImage, which resolves SPEC's open question on where the repo lives
without pkgwatch ever touching pacman.conf. Publishing stops at
`repo-add`; actually installing/upgrading (`pacman -Syu`/`pacman -S`) is
left to the operator, not run automatically.
Getting a real second package (scaleway-cli, tier 4) through the new
pipeline immediately surfaced a real gap: its pacman package is named
`scaleway-cli` but the actual binary is `scw` (confirmed via `pacman -Ql`
against the currently-installed extra package) — without a way to
declare that, the build would install alongside extra's package under
the wrong name instead of shadowing it. Added `Package::binary_name`
(config.rs) to cover it.
Every upstream-controlled string (version, asset name, download URL)
is validated before it touches generated shell content in the PKGBUILD
template — rejects anything containing a single quote or newline, since
values are embedded in single-quoted bash strings.
Verified for real, end to end: uv (tier 2) auto-built and published
against the real astral-sh/uv release with no human step; scaleway-cli
(tier 4) queued for review, then approved via `pkgwatch review
scaleway-cli --approve`, which re-verified, built, and published it —
confirmed the built package contains exactly usr/bin/scw. Both landed in
the real custom repo's database. Left scaleway-cli's real-repo review
pending rather than approving it myself: the tier 4-6 gate exists for a
human judgment call, not the agent's.
69 tests, cargo make ci clean (fmt, clippy, complexity, coverage, audit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:13:13 +00:00
|
|
|
|
`sanity_check` and `binary_name` are now real, implemented fields (see
|
|
|
|
|
|
Builder/Sanity checker above) — added `packages.d/uv.toml`'s and
|
|
|
|
|
|
`packages.d/scaleway-cli.toml`'s own `sanity_check` blocks, and
|
|
|
|
|
|
scaleway-cli's `binary_name = "scw"`. `source`, `check_method`, and
|
|
|
|
|
|
`check_interval` are still schema sketch, not yet read by the code — the
|
|
|
|
|
|
PoC only knows how to check GitHub-release sources, on a single one-shot
|
|
|
|
|
|
run rather than a scheduled loop.
|
|
|
|
|
|
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
Build/publish/review-queue (`makepkg`, `repo-add`, tier 4–6 human review)
|
Close the loop: build, sanity-check, and publish for the first time
Implements the last unimplemented pipeline stage from SPEC.md: PKGBUILD
generation + makepkg (builder.rs), a post-build version sanity check
(sanity.rs), and repo-add publishing (publisher.rs), wired into main.rs
for both the tier 1-3 auto-publish path and a new tier 4-6 review queue
(`pkgwatch review` / `pkgwatch review <name> --approve`, persisted via
state::{load,save,clear}_pending_version, tracked separately from
last-published-version since approving one release isn't a standing
auto-publish grant for future ones).
Publishing targets an existing, already-registered local pacman repo
(~/.local/share/pacman/custom, `[custom]` in /etc/pacman.conf) rather
than one pkgwatch invents — found already in real use for a hand-packaged
AppImage, which resolves SPEC's open question on where the repo lives
without pkgwatch ever touching pacman.conf. Publishing stops at
`repo-add`; actually installing/upgrading (`pacman -Syu`/`pacman -S`) is
left to the operator, not run automatically.
Getting a real second package (scaleway-cli, tier 4) through the new
pipeline immediately surfaced a real gap: its pacman package is named
`scaleway-cli` but the actual binary is `scw` (confirmed via `pacman -Ql`
against the currently-installed extra package) — without a way to
declare that, the build would install alongside extra's package under
the wrong name instead of shadowing it. Added `Package::binary_name`
(config.rs) to cover it.
Every upstream-controlled string (version, asset name, download URL)
is validated before it touches generated shell content in the PKGBUILD
template — rejects anything containing a single quote or newline, since
values are embedded in single-quoted bash strings.
Verified for real, end to end: uv (tier 2) auto-built and published
against the real astral-sh/uv release with no human step; scaleway-cli
(tier 4) queued for review, then approved via `pkgwatch review
scaleway-cli --approve`, which re-verified, built, and published it —
confirmed the built package contains exactly usr/bin/scw. Both landed in
the real custom repo's database. Left scaleway-cli's real-repo review
pending rather than approving it myself: the tier 4-6 gate exists for a
human judgment call, not the agent's.
69 tests, cargo make ci clean (fmt, clippy, complexity, coverage, audit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:13:13 +00:00
|
|
|
|
are now implemented too — see Builder/Sanity checker/Publisher/Reviewer
|
|
|
|
|
|
queue above and the Status entry below for the first full end-to-end run.
|
|
|
|
|
|
Tier 4-6 packages still don't auto-publish (by design, see Verification
|
|
|
|
|
|
trust tiers > Automation posture per tier); they queue for
|
|
|
|
|
|
`pkgwatch review <name> --approve`.
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
|
2026-09-11 07:01:30 +00:00
|
|
|
|
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).
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
- ~~Config layout: single TOML vs. `packages.d/*.toml` directory~~ —
|
|
|
|
|
|
resolved: PoC loads `packages.d/*.toml` directly.
|
Design for scale: many tracked packages, not multi-user
Corrects the earlier non-goal framing: per-user scale (tracking dozens of
packages) is explicitly in scope, distinct from multi-user/adversarial
config trust, which stays out. Adds a Scaling section covering review-queue
fatigue at volume, per-package check cadence, packages.d/ config layout,
audit-as-core, local repo retention, and GitHub rate limits/staggering.
Also settles the GitHub push-notification question: no true webhook push
for repos we don't own, and a relay-based alternative would need a public
inbound receiver this box's WireGuard-only posture deliberately avoids.
Settles on outbound-only github-atom/conditional github-api polling
instead, added to the config schema as check_method/check_interval.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 07:24:13 +00:00
|
|
|
|
- Digest/batching rules for low-signal tier 4–6 changes (see Scaling,
|
|
|
|
|
|
above) — what counts as "low-signal" needs a concrete definition, not
|
|
|
|
|
|
just "not a major version bump."
|
2026-09-11 07:01:30 +00:00
|
|
|
|
|
|
|
|
|
|
## Architecture sketch
|
|
|
|
|
|
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
- **Config loader**: parses `packages.d/*.toml` into an in-memory package
|
|
|
|
|
|
list. *(Implemented — `src/config.rs`.)*
|
2026-09-20 08:01:54 +00:00
|
|
|
|
- **Paths**: where config, state and work files live, resolved by
|
|
|
|
|
|
`src/paths.rs` per the XDG base-directory spec rather than the current
|
|
|
|
|
|
working directory, so an installed binary behaves the same wherever it's
|
|
|
|
|
|
launched from. *(Implemented.)*
|
|
|
|
|
|
|
|
|
|
|
|
| What | Default | XDG variable | Override |
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
| Package declarations (`packages.d/*.toml`) | `~/.config/pkgwatch/packages.d` | `XDG_CONFIG_HOME` | `PKGWATCH_CONFIG_DIR` (the dir *containing* `packages.d`) |
|
|
|
|
|
|
| Last-published / pending versions | `~/.local/state/pkgwatch` | `XDG_STATE_HOME` | `PKGWATCH_STATE_DIR` |
|
|
|
|
|
|
| Downloads and build trees (safe to delete) | `~/.cache/pkgwatch` | `XDG_CACHE_HOME` | `PKGWATCH_WORK_DIR` |
|
|
|
|
|
|
|
2026-09-20 08:19:12 +00:00
|
|
|
|
Precedence per directory: override, then the XDG variable, then the
|
|
|
|
|
|
default under `$HOME`; an empty variable counts as unset. The overrides
|
|
|
|
|
|
are used verbatim (no `pkgwatch/` suffix) and exist for dry runs against
|
|
|
|
|
|
scratch directories, like `PKGWATCH_REPO_DIR` does for the pacman repo.
|
|
|
|
|
|
The XDG variables and `$HOME` must be absolute paths: a relative XDG
|
|
|
|
|
|
value is ignored, as the XDG spec requires, and a relative `$HOME` is an
|
|
|
|
|
|
error.
|
|
|
|
|
|
|
|
|
|
|
|
The checkout's `packages.d/` is no longer read on its own; it's just the
|
|
|
|
|
|
source to link from. Migrating from the old cwd-relative layout: move
|
2026-09-20 08:01:54 +00:00
|
|
|
|
`state/` to the state dir and copy or symlink `packages.d/` into the
|
|
|
|
|
|
config dir; `work/` is cache and can simply be dropped.
|
2026-09-11 07:01:30 +00:00
|
|
|
|
- **Checker**: per source type, resolves "what's the latest version" —
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
likely reuses `nvchecker`'s logic/sources conceptually for non-GitHub
|
|
|
|
|
|
sources eventually. For GitHub sources, prefers the `github-atom` feed
|
|
|
|
|
|
(see Scaling > Check method) over unconditional REST polling.
|
|
|
|
|
|
*(Implemented for GitHub only — `src/checker.rs` regex-matches the first
|
|
|
|
|
|
`releases/tag/<tag>` link in the feed rather than doing a full XML parse;
|
|
|
|
|
|
fine while the feed's newest-entry-first shape holds, revisit if that
|
|
|
|
|
|
ever changes. `check_interval`/per-package cadence not wired up yet —
|
|
|
|
|
|
the PoC is a single one-shot run, not a scheduled loop.)*
|
2026-09-11 07:01:30 +00:00
|
|
|
|
- **Fetcher**: downloads the artifact (and any checksum/signature/
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
attestation companion) for a resolved version. *(Implemented —
|
|
|
|
|
|
`src/fetcher.rs`, via the GitHub releases API; exact asset-name match,
|
|
|
|
|
|
not a glob.)*
|
|
|
|
|
|
- **Verifier**: tier-specific verification implementations, dispatched via
|
|
|
|
|
|
a `Verification` enum matched on `method` (an internally-tagged serde
|
|
|
|
|
|
enum) rather than a trait — simpler while there are only two methods;
|
|
|
|
|
|
revisit as a trait if the method count grows. Returns a tier + pass/fail
|
|
|
|
|
|
+ justification string; tier is derived from `method`, never configured
|
|
|
|
|
|
separately. *(Implemented for `same-origin-sha256` and
|
|
|
|
|
|
`github-attestation` — `src/verifier.rs`. The latter shells out to `gh
|
|
|
|
|
|
attestation verify` rather than reimplementing sigstore verification.)*
|
Close the loop: build, sanity-check, and publish for the first time
Implements the last unimplemented pipeline stage from SPEC.md: PKGBUILD
generation + makepkg (builder.rs), a post-build version sanity check
(sanity.rs), and repo-add publishing (publisher.rs), wired into main.rs
for both the tier 1-3 auto-publish path and a new tier 4-6 review queue
(`pkgwatch review` / `pkgwatch review <name> --approve`, persisted via
state::{load,save,clear}_pending_version, tracked separately from
last-published-version since approving one release isn't a standing
auto-publish grant for future ones).
Publishing targets an existing, already-registered local pacman repo
(~/.local/share/pacman/custom, `[custom]` in /etc/pacman.conf) rather
than one pkgwatch invents — found already in real use for a hand-packaged
AppImage, which resolves SPEC's open question on where the repo lives
without pkgwatch ever touching pacman.conf. Publishing stops at
`repo-add`; actually installing/upgrading (`pacman -Syu`/`pacman -S`) is
left to the operator, not run automatically.
Getting a real second package (scaleway-cli, tier 4) through the new
pipeline immediately surfaced a real gap: its pacman package is named
`scaleway-cli` but the actual binary is `scw` (confirmed via `pacman -Ql`
against the currently-installed extra package) — without a way to
declare that, the build would install alongside extra's package under
the wrong name instead of shadowing it. Added `Package::binary_name`
(config.rs) to cover it.
Every upstream-controlled string (version, asset name, download URL)
is validated before it touches generated shell content in the PKGBUILD
template — rejects anything containing a single quote or newline, since
values are embedded in single-quoted bash strings.
Verified for real, end to end: uv (tier 2) auto-built and published
against the real astral-sh/uv release with no human step; scaleway-cli
(tier 4) queued for review, then approved via `pkgwatch review
scaleway-cli --approve`, which re-verified, built, and published it —
confirmed the built package contains exactly usr/bin/scw. Both landed in
the real custom repo's database. Left scaleway-cli's real-repo review
pending rather than approving it myself: the tier 4-6 gate exists for a
human judgment call, not the agent's.
69 tests, cargo make ci clean (fmt, clippy, complexity, coverage, audit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:13:13 +00:00
|
|
|
|
- **Builder**: for tiers 1–3 on pass, generates a PKGBUILD (strict
|
|
|
|
|
|
validation on every upstream-controlled string — version, asset name,
|
Fix issues from code review: shell-escaping gap, exit code, and more
A single-agent code review of this branch's diff (builder/pipeline/
publisher/sanity/hash + main/config/fetcher/state/verifier changes)
found six real issues, all fixed here:
- builder.rs: validate_shell_safe only rejected a literal single quote
and newline, written for the single-quoted PKGBUILD fields. But
asset_name (via install_source) and binary_name land in the install()
line, which is necessarily double-quoted so ${srcdir}/${pkgdir} can
expand — where $, backtick, and backslash are still live. Not
currently exploitable (the one variable component, version, is already
independently constrained by validate_pkgver's strict charset), but a
latent gap relying on that coincidence rather than the validator
actually covering its real use context. Widened the reject-list to
cover both quoting styles, added regression tests including one at the
generate_pkgbuild level. Corrected SPEC.md's "single-quoted" claim to
match.
- pipeline.rs: a verification failure returned Ok(()) from
process_package, so run_check never counted it as a failure and the
process exited 0 even on a failed cryptographic/attestation check —
exactly the event a monitoring setup (systemd OnFailure=, cron
mail-on-error) most needs a non-zero exit to catch. Now bails, which
run_check already treats as a package failure. Added an integration
test against a mocked GitHub server exercising this exact path.
- builder.rs: find_built_package hardcoded the .pkg.tar.zst suffix, so a
box with a different PKGEXT in makepkg.conf would report a false
"makepkg failed" for a build that actually succeeded. Widened to match
any .pkg.tar.* compression. Added direct unit tests (it had none).
- pipeline.rs: a newer tier 4-6 version silently overwrote a still-
unreviewed older pending version with no indication anything was
superseded. Now says so explicitly.
- hash.rs: builder/verifier each read a whole downloaded artifact into
memory via std::fs::read just to hash it, doubling peak memory for no
reason since the file's already on disk. Added sha256_hex_file,
streamed in fixed-size chunks; both callers switched to it.
- Deduplicated two near-identical test-only "write an executable shell
script" helpers (publisher.rs, sanity.rs) into a shared
src/test_support.rs.
75 tests (was 63), cargo make ci clean. Re-verified end to end against
the real astral-sh/uv release after all six fixes — build, sanity check,
and publish into a scratch repo all still succeed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 10:06:40 +00:00
|
|
|
|
download URL — before it touches generated shell content; most fields
|
|
|
|
|
|
are single-quoted, but the `install()` line necessarily uses double
|
|
|
|
|
|
quotes so `${srcdir}`/`${pkgdir}` expand, so the validation rejects `'`,
|
|
|
|
|
|
newline, `$`, backtick, *and* backslash — safe for either quoting style
|
|
|
|
|
|
rather than assuming a value only ever lands in one of them — never
|
Close the loop: build, sanity-check, and publish for the first time
Implements the last unimplemented pipeline stage from SPEC.md: PKGBUILD
generation + makepkg (builder.rs), a post-build version sanity check
(sanity.rs), and repo-add publishing (publisher.rs), wired into main.rs
for both the tier 1-3 auto-publish path and a new tier 4-6 review queue
(`pkgwatch review` / `pkgwatch review <name> --approve`, persisted via
state::{load,save,clear}_pending_version, tracked separately from
last-published-version since approving one release isn't a standing
auto-publish grant for future ones).
Publishing targets an existing, already-registered local pacman repo
(~/.local/share/pacman/custom, `[custom]` in /etc/pacman.conf) rather
than one pkgwatch invents — found already in real use for a hand-packaged
AppImage, which resolves SPEC's open question on where the repo lives
without pkgwatch ever touching pacman.conf. Publishing stops at
`repo-add`; actually installing/upgrading (`pacman -Syu`/`pacman -S`) is
left to the operator, not run automatically.
Getting a real second package (scaleway-cli, tier 4) through the new
pipeline immediately surfaced a real gap: its pacman package is named
`scaleway-cli` but the actual binary is `scw` (confirmed via `pacman -Ql`
against the currently-installed extra package) — without a way to
declare that, the build would install alongside extra's package under
the wrong name instead of shadowing it. Added `Package::binary_name`
(config.rs) to cover it.
Every upstream-controlled string (version, asset name, download URL)
is validated before it touches generated shell content in the PKGBUILD
template — rejects anything containing a single quote or newline, since
values are embedded in single-quoted bash strings.
Verified for real, end to end: uv (tier 2) auto-built and published
against the real astral-sh/uv release with no human step; scaleway-cli
(tier 4) queued for review, then approved via `pkgwatch review
scaleway-cli --approve`, which re-verified, built, and published it —
confirmed the built package contains exactly usr/bin/scw. Both landed in
the real custom repo's database. Left scaleway-cli's real-repo review
pending rather than approving it myself: the tier 4-6 gate exists for a
human judgment call, not the agent's.
69 tests, cargo make ci clean (fmt, clippy, complexity, coverage, audit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:13:13 +00:00
|
|
|
|
unescaped interpolation) and runs `makepkg`. *(Implemented —
|
|
|
|
|
|
`src/builder.rs`. One fixed "prebuilt binary" PKGBUILD shape covers both
|
|
|
|
|
|
tracked packages so far: a bare-binary download (scaleway-cli) and a
|
|
|
|
|
|
tarball extracting to a same-named directory (uv) — see Scaling >
|
|
|
|
|
|
Template reuse. `Package.binary_name` (config.rs) covers the case where
|
|
|
|
|
|
the installed binary's name differs from the pacman package name, which
|
|
|
|
|
|
turned out to matter immediately: scaleway-cli's real binary is `scw`,
|
|
|
|
|
|
not `scaleway-cli` — confirmed by inspecting the currently-installed
|
|
|
|
|
|
`extra` package with `pacman -Ql`, not guessable from the repo name.
|
|
|
|
|
|
Without it the build would install alongside `extra`'s package instead
|
|
|
|
|
|
of shadowing it.)*
|
2026-09-11 07:10:06 +00:00
|
|
|
|
- **Sanity checker**: after a successful build, runs the package's
|
Close the loop: build, sanity-check, and publish for the first time
Implements the last unimplemented pipeline stage from SPEC.md: PKGBUILD
generation + makepkg (builder.rs), a post-build version sanity check
(sanity.rs), and repo-add publishing (publisher.rs), wired into main.rs
for both the tier 1-3 auto-publish path and a new tier 4-6 review queue
(`pkgwatch review` / `pkgwatch review <name> --approve`, persisted via
state::{load,save,clear}_pending_version, tracked separately from
last-published-version since approving one release isn't a standing
auto-publish grant for future ones).
Publishing targets an existing, already-registered local pacman repo
(~/.local/share/pacman/custom, `[custom]` in /etc/pacman.conf) rather
than one pkgwatch invents — found already in real use for a hand-packaged
AppImage, which resolves SPEC's open question on where the repo lives
without pkgwatch ever touching pacman.conf. Publishing stops at
`repo-add`; actually installing/upgrading (`pacman -Syu`/`pacman -S`) is
left to the operator, not run automatically.
Getting a real second package (scaleway-cli, tier 4) through the new
pipeline immediately surfaced a real gap: its pacman package is named
`scaleway-cli` but the actual binary is `scw` (confirmed via `pacman -Ql`
against the currently-installed extra package) — without a way to
declare that, the build would install alongside extra's package under
the wrong name instead of shadowing it. Added `Package::binary_name`
(config.rs) to cover it.
Every upstream-controlled string (version, asset name, download URL)
is validated before it touches generated shell content in the PKGBUILD
template — rejects anything containing a single quote or newline, since
values are embedded in single-quoted bash strings.
Verified for real, end to end: uv (tier 2) auto-built and published
against the real astral-sh/uv release with no human step; scaleway-cli
(tier 4) queued for review, then approved via `pkgwatch review
scaleway-cli --approve`, which re-verified, built, and published it —
confirmed the built package contains exactly usr/bin/scw. Both landed in
the real custom repo's database. Left scaleway-cli's real-repo review
pending rather than approving it myself: the tier 4-6 gate exists for a
human judgment call, not the agent's.
69 tests, cargo make ci clean (fmt, clippy, complexity, coverage, audit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:13:13 +00:00
|
|
|
|
declared `sanity_check.command` — with the freshly built package's
|
|
|
|
|
|
`usr/bin` prepended to `PATH`, so it exercises what was just built
|
|
|
|
|
|
rather than whatever's already installed system-wide — 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. *(Implemented — `src/sanity.rs`.)*
|
|
|
|
|
|
- **Publisher**: copies the built package into the local repo directory
|
|
|
|
|
|
and runs `repo-add`, only after the sanity check passes. *(Implemented —
|
|
|
|
|
|
`src/publisher.rs`. Targets an existing, already-registered local pacman
|
|
|
|
|
|
repo rather than one pkgwatch creates — this box already has one at
|
|
|
|
|
|
`~/.local/share/pacman/custom`, registered as `[custom]` in
|
|
|
|
|
|
`/etc/pacman.conf` (`SigLevel = Optional TrustAll`) and already in use
|
2026-09-17 09:30:43 +00:00
|
|
|
|
for a hand-packaged AppImage. But that repo directory/registration isn't
|
|
|
|
|
|
guaranteed to exist on every box this ever runs on, so it isn't just
|
|
|
|
|
|
assumed: `publisher::ensure_registered` checks `/etc/pacman.conf` for an
|
|
|
|
|
|
active `[<repo_name>]` section before a build even starts, failing fast
|
|
|
|
|
|
with the exact snippet to add if it's missing, rather than wasting a
|
|
|
|
|
|
`makepkg` build on a repo pacman will never sync from. The repo
|
|
|
|
|
|
*directory* and its database file, by contrast, are fully self-healing —
|
|
|
|
|
|
`publish` creates the directory if missing and `repo-add` creates the
|
|
|
|
|
|
database on its first run. What's deliberately not automatic, and can't
|
|
|
|
|
|
safely be: writing the `[section]` into `/etc/pacman.conf` itself — that
|
|
|
|
|
|
needs root, which this process doesn't have and shouldn't grab for
|
|
|
|
|
|
itself. Similarly, publish deliberately stops at `repo-add`: getting the
|
|
|
|
|
|
new version onto the running system is a separate, deliberate
|
Close the loop: build, sanity-check, and publish for the first time
Implements the last unimplemented pipeline stage from SPEC.md: PKGBUILD
generation + makepkg (builder.rs), a post-build version sanity check
(sanity.rs), and repo-add publishing (publisher.rs), wired into main.rs
for both the tier 1-3 auto-publish path and a new tier 4-6 review queue
(`pkgwatch review` / `pkgwatch review <name> --approve`, persisted via
state::{load,save,clear}_pending_version, tracked separately from
last-published-version since approving one release isn't a standing
auto-publish grant for future ones).
Publishing targets an existing, already-registered local pacman repo
(~/.local/share/pacman/custom, `[custom]` in /etc/pacman.conf) rather
than one pkgwatch invents — found already in real use for a hand-packaged
AppImage, which resolves SPEC's open question on where the repo lives
without pkgwatch ever touching pacman.conf. Publishing stops at
`repo-add`; actually installing/upgrading (`pacman -Syu`/`pacman -S`) is
left to the operator, not run automatically.
Getting a real second package (scaleway-cli, tier 4) through the new
pipeline immediately surfaced a real gap: its pacman package is named
`scaleway-cli` but the actual binary is `scw` (confirmed via `pacman -Ql`
against the currently-installed extra package) — without a way to
declare that, the build would install alongside extra's package under
the wrong name instead of shadowing it. Added `Package::binary_name`
(config.rs) to cover it.
Every upstream-controlled string (version, asset name, download URL)
is validated before it touches generated shell content in the PKGBUILD
template — rejects anything containing a single quote or newline, since
values are embedded in single-quoted bash strings.
Verified for real, end to end: uv (tier 2) auto-built and published
against the real astral-sh/uv release with no human step; scaleway-cli
(tier 4) queued for review, then approved via `pkgwatch review
scaleway-cli --approve`, which re-verified, built, and published it —
confirmed the built package contains exactly usr/bin/scw. Both landed in
the real custom repo's database. Left scaleway-cli's real-repo review
pending rather than approving it myself: the tier 4-6 gate exists for a
human judgment call, not the agent's.
69 tests, cargo make ci clean (fmt, clippy, complexity, coverage, audit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:13:13 +00:00
|
|
|
|
`pacman -Syu`/`pacman -S <pkg>` step left to the operator, not run
|
|
|
|
|
|
automatically.)*
|
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
|
Close the loop: build, sanity-check, and publish for the first time
Implements the last unimplemented pipeline stage from SPEC.md: PKGBUILD
generation + makepkg (builder.rs), a post-build version sanity check
(sanity.rs), and repo-add publishing (publisher.rs), wired into main.rs
for both the tier 1-3 auto-publish path and a new tier 4-6 review queue
(`pkgwatch review` / `pkgwatch review <name> --approve`, persisted via
state::{load,save,clear}_pending_version, tracked separately from
last-published-version since approving one release isn't a standing
auto-publish grant for future ones).
Publishing targets an existing, already-registered local pacman repo
(~/.local/share/pacman/custom, `[custom]` in /etc/pacman.conf) rather
than one pkgwatch invents — found already in real use for a hand-packaged
AppImage, which resolves SPEC's open question on where the repo lives
without pkgwatch ever touching pacman.conf. Publishing stops at
`repo-add`; actually installing/upgrading (`pacman -Syu`/`pacman -S`) is
left to the operator, not run automatically.
Getting a real second package (scaleway-cli, tier 4) through the new
pipeline immediately surfaced a real gap: its pacman package is named
`scaleway-cli` but the actual binary is `scw` (confirmed via `pacman -Ql`
against the currently-installed extra package) — without a way to
declare that, the build would install alongside extra's package under
the wrong name instead of shadowing it. Added `Package::binary_name`
(config.rs) to cover it.
Every upstream-controlled string (version, asset name, download URL)
is validated before it touches generated shell content in the PKGBUILD
template — rejects anything containing a single quote or newline, since
values are embedded in single-quoted bash strings.
Verified for real, end to end: uv (tier 2) auto-built and published
against the real astral-sh/uv release with no human step; scaleway-cli
(tier 4) queued for review, then approved via `pkgwatch review
scaleway-cli --approve`, which re-verified, built, and published it —
confirmed the built package contains exactly usr/bin/scw. Both landed in
the real custom repo's database. Left scaleway-cli's real-repo review
pending rather than approving it myself: the tier 4-6 gate exists for a
human judgment call, not the agent's.
69 tests, cargo make ci clean (fmt, clippy, complexity, coverage, audit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:13:13 +00:00
|
|
|
|
steps above. *(Implemented — `state::{load,save,clear}_pending_version`
|
|
|
|
|
|
plus the `review`/`review <name> --approve` subcommands in `src/main.rs`.
|
|
|
|
|
|
Tracked separately from the last-published-version state: approving one
|
|
|
|
|
|
release doesn't mean future ones auto-publish. `--approve` re-verifies
|
|
|
|
|
|
before building rather than trusting a possibly-stale flag from an
|
|
|
|
|
|
earlier run. No reject/dismiss command yet — see Status below.)*
|
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
|
2026-09-20 07:51:10 +00:00
|
|
|
|
on this box. *(Implemented — user-level units under `systemd/`, run 10s
|
|
|
|
|
|
after login and then hourly, non-persistent. Install from the main
|
|
|
|
|
|
checkout:*
|
|
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
|
cargo build --release && mkdir -p ~/.config/systemd/user &&
|
|
|
|
|
|
cp systemd/* ~/.config/systemd/user/ && systemctl --user daemon-reload &&
|
|
|
|
|
|
systemctl --user enable --now pkgwatch.timer
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-09-20 08:03:24 +00:00
|
|
|
|
*`pkgwatch.service` runs the release binary from the checkout and sets
|
|
|
|
|
|
no `WorkingDirectory`: config, state and work dirs come from the XDG
|
|
|
|
|
|
paths above, so the service needs `~/.config/pkgwatch/packages.d` set
|
|
|
|
|
|
up first — see the migration note under Paths.)*
|
2026-09-20 07:45:26 +00:00
|
|
|
|
- **Notifications**: `notifier.rs` sends a desktop notification
|
|
|
|
|
|
(`notify-send`) when a tier 4-6 release is newly queued for review or a
|
|
|
|
|
|
tier 1-3 release is published; both are best-effort and never fail a
|
|
|
|
|
|
run. A non-zero exit (verification/build/network failure) triggers
|
|
|
|
|
|
`pkgwatch-failure.service` via `OnFailure=`. Approving via
|
|
|
|
|
|
`pkgwatch review --approve` doesn't notify — the operator is already at
|
|
|
|
|
|
the terminal.
|
2026-09-11 07:01:30 +00:00
|
|
|
|
|
|
|
|
|
|
## 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
|
Design for scale: many tracked packages, not multi-user
Corrects the earlier non-goal framing: per-user scale (tracking dozens of
packages) is explicitly in scope, distinct from multi-user/adversarial
config trust, which stays out. Adds a Scaling section covering review-queue
fatigue at volume, per-package check cadence, packages.d/ config layout,
audit-as-core, local repo retention, and GitHub rate limits/staggering.
Also settles the GitHub push-notification question: no true webhook push
for repos we don't own, and a relay-based alternative would need a public
inbound receiver this box's WireGuard-only posture deliberately avoids.
Settles on outbound-only github-atom/conditional github-api polling
instead, added to the config schema as check_method/check_interval.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 07:24:13 +00:00
|
|
|
|
are explicit non-goals; per-user scale to many packages is *not* a
|
|
|
|
|
|
non-goal (see Scaling to many packages, above).
|
|
|
|
|
|
- [x] Check method decided for GitHub sources: `github-atom`/conditional
|
|
|
|
|
|
`github-api` polling, outbound-only. Inbound webhooks explicitly
|
|
|
|
|
|
rejected — no repo-admin access on upstreams, and a relay-based
|
|
|
|
|
|
alternative would require a public receiver this box's networking
|
|
|
|
|
|
posture deliberately avoids.
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
- [x] `packages.d/*.toml` config layout implemented (`src/config.rs`).
|
|
|
|
|
|
- [x] First PoC iteration, working end to end against the real
|
|
|
|
|
|
`astral-sh/uv` repo: `github-atom` check → GitHub-API fetch →
|
|
|
|
|
|
`github-attestation` (tier 2) verify via `gh attestation verify` →
|
|
|
|
|
|
state persisted so re-runs report "up to date." Confirmed uv
|
|
|
|
|
|
actually ships attestations, correcting the spec's original tier-4
|
|
|
|
|
|
guess for it. Run: `cargo run` from the project root.
|
2026-09-17 07:00:53 +00:00
|
|
|
|
- [x] `same-origin-sha256` exercised against a real package:
|
|
|
|
|
|
`scaleway/scaleway-cli`, tracked via `packages.d/scaleway-cli.toml`
|
|
|
|
|
|
(added because Manjaro's `extra` scaleway-cli lags upstream). Tier 4
|
|
|
|
|
|
confirmed correct — no build-provenance attestations upstream.
|
|
|
|
|
|
Required adding `{version}`-placeholder support to `asset_pattern`/
|
|
|
|
|
|
`checksum_asset_pattern`, filename-matched parsing of combined
|
|
|
|
|
|
multi-asset checksum files, and having `latest_github_release`
|
|
|
|
|
|
confirm each Atom-feed candidate against the releases API (this
|
|
|
|
|
|
repo's newest feed entry, a `-dbg1` tag, has no real Release behind
|
|
|
|
|
|
it). Still just flags for human review, same as any tier 4-6 pass —
|
|
|
|
|
|
not auto-installed; see the unchecked build/publish item below.
|
Close the loop: build, sanity-check, and publish for the first time
Implements the last unimplemented pipeline stage from SPEC.md: PKGBUILD
generation + makepkg (builder.rs), a post-build version sanity check
(sanity.rs), and repo-add publishing (publisher.rs), wired into main.rs
for both the tier 1-3 auto-publish path and a new tier 4-6 review queue
(`pkgwatch review` / `pkgwatch review <name> --approve`, persisted via
state::{load,save,clear}_pending_version, tracked separately from
last-published-version since approving one release isn't a standing
auto-publish grant for future ones).
Publishing targets an existing, already-registered local pacman repo
(~/.local/share/pacman/custom, `[custom]` in /etc/pacman.conf) rather
than one pkgwatch invents — found already in real use for a hand-packaged
AppImage, which resolves SPEC's open question on where the repo lives
without pkgwatch ever touching pacman.conf. Publishing stops at
`repo-add`; actually installing/upgrading (`pacman -Syu`/`pacman -S`) is
left to the operator, not run automatically.
Getting a real second package (scaleway-cli, tier 4) through the new
pipeline immediately surfaced a real gap: its pacman package is named
`scaleway-cli` but the actual binary is `scw` (confirmed via `pacman -Ql`
against the currently-installed extra package) — without a way to
declare that, the build would install alongside extra's package under
the wrong name instead of shadowing it. Added `Package::binary_name`
(config.rs) to cover it.
Every upstream-controlled string (version, asset name, download URL)
is validated before it touches generated shell content in the PKGBUILD
template — rejects anything containing a single quote or newline, since
values are embedded in single-quoted bash strings.
Verified for real, end to end: uv (tier 2) auto-built and published
against the real astral-sh/uv release with no human step; scaleway-cli
(tier 4) queued for review, then approved via `pkgwatch review
scaleway-cli --approve`, which re-verified, built, and published it —
confirmed the built package contains exactly usr/bin/scw. Both landed in
the real custom repo's database. Left scaleway-cli's real-repo review
pending rather than approving it myself: the tier 4-6 gate exists for a
human judgment call, not the agent's.
69 tests, cargo make ci clean (fmt, clippy, complexity, coverage, audit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:13:13 +00:00
|
|
|
|
- [x] Full pipeline closed end to end for the first time: check → fetch →
|
|
|
|
|
|
verify → build → sanity-check → publish, against two real packages.
|
|
|
|
|
|
`uv` (tier 2) auto-built and published on the first run with no
|
|
|
|
|
|
human step. `scaleway-cli` (tier 4) queued for review, then
|
|
|
|
|
|
`pkgwatch review scaleway-cli --approve` re-verified, built, and
|
|
|
|
|
|
published it — confirmed the built package installs as
|
|
|
|
|
|
`/usr/bin/scw`, actually shadowing `extra`'s package rather than
|
|
|
|
|
|
installing alongside it under the wrong name. Both landed in the
|
|
|
|
|
|
real `~/.local/share/pacman/custom` repo's database
|
|
|
|
|
|
(`custom.db.tar.gz`), ready for `sudo pacman -Syu`/`sudo pacman -S`
|
|
|
|
|
|
— not run automatically. See Builder/Sanity checker/Publisher/
|
|
|
|
|
|
Reviewer queue above for what each piece does.
|
|
|
|
|
|
One cosmetic wrinkle, not a correctness issue: `makepkg` printed
|
|
|
|
|
|
`libfakeroot internal error: payload not recognized!` while
|
|
|
|
|
|
packaging scaleway-cli's large Go binary, but still produced a
|
|
|
|
|
|
correct package (verified: exactly `usr/bin/scw` plus standard
|
|
|
|
|
|
metadata) — looks like an environment quirk in this sandbox's
|
|
|
|
|
|
fakeroot, not something pkgwatch caused; revisit if a real build
|
|
|
|
|
|
ever actually fails on it.
|
|
|
|
|
|
- [ ] Not yet implemented: `pkgwatch review <name> --reject` (a pending
|
|
|
|
|
|
review can only be approved or left pending, not dismissed),
|
2026-09-20 07:51:10 +00:00
|
|
|
|
per-package `check_interval` (the timer is a fixed hourly tick),
|
2026-09-20 07:45:26 +00:00
|
|
|
|
non-GitHub sources, `minisign`/tier-1
|
Close the loop: build, sanity-check, and publish for the first time
Implements the last unimplemented pipeline stage from SPEC.md: PKGBUILD
generation + makepkg (builder.rs), a post-build version sanity check
(sanity.rs), and repo-add publishing (publisher.rs), wired into main.rs
for both the tier 1-3 auto-publish path and a new tier 4-6 review queue
(`pkgwatch review` / `pkgwatch review <name> --approve`, persisted via
state::{load,save,clear}_pending_version, tracked separately from
last-published-version since approving one release isn't a standing
auto-publish grant for future ones).
Publishing targets an existing, already-registered local pacman repo
(~/.local/share/pacman/custom, `[custom]` in /etc/pacman.conf) rather
than one pkgwatch invents — found already in real use for a hand-packaged
AppImage, which resolves SPEC's open question on where the repo lives
without pkgwatch ever touching pacman.conf. Publishing stops at
`repo-add`; actually installing/upgrading (`pacman -Syu`/`pacman -S`) is
left to the operator, not run automatically.
Getting a real second package (scaleway-cli, tier 4) through the new
pipeline immediately surfaced a real gap: its pacman package is named
`scaleway-cli` but the actual binary is `scw` (confirmed via `pacman -Ql`
against the currently-installed extra package) — without a way to
declare that, the build would install alongside extra's package under
the wrong name instead of shadowing it. Added `Package::binary_name`
(config.rs) to cover it.
Every upstream-controlled string (version, asset name, download URL)
is validated before it touches generated shell content in the PKGBUILD
template — rejects anything containing a single quote or newline, since
values are embedded in single-quoted bash strings.
Verified for real, end to end: uv (tier 2) auto-built and published
against the real astral-sh/uv release with no human step; scaleway-cli
(tier 4) queued for review, then approved via `pkgwatch review
scaleway-cli --approve`, which re-verified, built, and published it —
confirmed the built package contains exactly usr/bin/scw. Both landed in
the real custom repo's database. Left scaleway-cli's real-repo review
pending rather than approving it myself: the tier 4-6 gate exists for a
human judgment call, not the agent's.
69 tests, cargo make ci clean (fmt, clippy, complexity, coverage, audit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:13:13 +00:00
|
|
|
|
method, retention/pruning of old versions in the local repo (see
|
|
|
|
|
|
Scaling > Local repo retention), staggering/auth for GitHub API
|
|
|
|
|
|
rate limits at higher package counts.
|
|
|
|
|
|
- [ ] Refine config schema further (see open questions above).
|
First working iteration: check -> fetch -> verify for a real package
Rust PoC (cargo, packages.d/*.toml config) that checks astral-sh/uv's
GitHub Atom feed for a new release, fetches the matching asset via the
GitHub API, and verifies it. Confirmed live against the real repo: uv
actually ships GitHub build-provenance attestations (sigstore bundle) on
every release, so it's a tier-2 package, not the tier-4 same-origin-sha256
guessed in the original spec draft. Verified via `gh attestation verify`
rather than reimplementing sigstore in Rust. State persists across runs so
a second run correctly reports "up to date."
Also folds the finding back into SPEC.md: updates the uv example to
tier 2, derives tier from verification method instead of storing both
(avoids a tier/method mismatch that would mean nothing), marks the
packages.d/ layout question resolved, and updates Architecture/Status to
say what's actually implemented vs. still sketch (build/publish/review
queue, same-origin-sha256 against a live repo, scheduling, non-GitHub
sources, minisign are all still open).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 08:04:48 +00:00
|
|
|
|
- [ ] Decide version-check strategy for non-GitHub sources: shell out to
|
|
|
|
|
|
`nvchecker` vs. own implementation.
|
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.
|