Commit graph

9 commits

Author SHA1 Message Date
Austin Schaefer
f55188e36f Fix issues from code review: shell-escaping gap, exit code, and more
All checks were successful
CI / build (pull_request) Successful in 51s
CI / test (pull_request) Successful in 3m35s
CI / audit (pull_request) Successful in 13s
CI / coverage (pull_request) Successful in 6m57s
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 12:06:40 +02:00
Austin Schaefer
3f30e0acc6 Add ARCHITECTURE.md and apply it to this PR's code
All checks were successful
CI / build (pull_request) Successful in 54s
CI / test (pull_request) Successful in 4m6s
CI / audit (pull_request) Successful in 17s
CI / coverage (pull_request) Successful in 6m19s
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 11:42:18 +02:00
Austin Schaefer
6305743e4d Check the custom repo is registered in pacman.conf before building
All checks were successful
CI / build (pull_request) Successful in 1m22s
CI / test (pull_request) Successful in 3m50s
CI / audit (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 9m2s
PR feedback: the local repo isn't guaranteed to exist on every box this
runs on, so it shouldn't just be 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 — instead of spending several seconds on a makepkg build that
would succeed and then publish into a repo pacman never syncs from.

The repo directory and its database file were already self-healing
(publish creates the dir if missing, repo-add creates the db on first
run) — the actual gap was the pacman.conf registration, which can't be
made self-healing without root, so this fails loud with instructions
instead of trying to write to /etc/pacman.conf itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 11:30:43 +02:00
Austin Schaefer
68fa648010 Close the loop: build, sanity-check, and publish for the first time
All checks were successful
CI / build (pull_request) Successful in 13m14s
CI / test (pull_request) Successful in 3m35s
CI / coverage (pull_request) Successful in 9m23s
CI / audit (pull_request) Successful in 13s
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 11:13:13 +02:00
Austin Schaefer
7f4bb4cccf Track scaleway-cli, add version-templated assets and combined checksums
Wire up scaleway/scaleway-cli as a watched package (extra's copy lags
weeks behind upstream). Getting a real second package running exposed
gaps uv's config never hit:

- asset_pattern/checksum_asset_pattern now support a {version}
  placeholder for release assets that embed the version in the
  filename (checker::version_from_tag strips a tag's leading `v`).
- same-origin-sha256 now matches the checksum line by filename instead
  of assuming a single-hash file, to support combined multi-asset
  checksum files like scaleway-cli's SHA256SUMS.
- latest_github_release now confirms each Atom-feed candidate against
  the releases API instead of trusting the newest entry outright —
  scaleway-cli publishes a `-dbg1` tag with no real Release object that
  otherwise sorts newest in the feed.

Confirmed correct against the real repo: no attestations upstream, so
tier 4 (same-origin-sha256) applies, and the checker correctly skips
the dbg1 tag to land on the real latest release.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 09:00:53 +02:00
Austin Schaefer
bf46cbf073 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 10:04:48 +02:00
Austin Schaefer
6361e9eb5e 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 09:24:13 +02:00
Austin Schaefer
8443ecea69 Narrow scope: personal middle ground, not a security framework
Adds an explicit Scope/Non-Goals section (curated personal package list,
not adversarial-config or compromised-vendor-pipeline defense) and a
post-build version sanity check as its own pipeline stage, distinct from
the trust tiers — a correctness gate (does the build report the version
we expected), not a security control. Trims the next-steps list to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2FEut5tVMNjeVjqhgVZbr
2026-09-11 09:10:06 +02:00
Austin Schaefer
0fa994f60a 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
2026-09-11 09:01:30 +02:00