Skip to content

feat(appstore): view detail command + catalogue v2 metadata#255

Merged
Alexgodoroja merged 2 commits into
mainfrom
feature/appstore-view
Jun 15, 2026
Merged

feat(appstore): view detail command + catalogue v2 metadata#255
Alexgodoroja merged 2 commits into
mainfrom
feature/appstore-view

Conversation

@Alexgodoroja

Copy link
Copy Markdown
Collaborator

What

Adds pilotctl appstore view <id> — a detail page for app-store apps, the way a human app store shows one. Today the catalogue only carries id, version, a one-line description, and the bundle pin; there is nowhere to see a structured description, vendor, changelog, size, source URL, or methods before installing. This adds that.

view merges three provenance-labelled bands:

  • catalogue — the index teaser (publisher-attested, sha-anchored)
  • metadata — a sha-pinned per-app detail doc (full description, changelog, vendor, screenshots…)
  • local-manifest — verified install facts when installed (integrity, real on-disk size, granted permissions, methods)

It works whether or not the app is installed, and whether or not it is in the catalogue (a sideloaded app renders from local facts alone). --json emits the merged report; --all-changelog shows full history. The publisher copy and the verified install facts are kept visually distinct so they are never conflated.

Catalogue schema v2

The catalogue index gains optional teaser fields (display_name, vendor, categories, bundle_size, source_url, license) and a metadata_url + metadata_sha256 pin to a per-app catalogue/apps/<id>/metadata.json detail document. The detail doc is fetched lazily by view and verified against the pin exactly the way bundle tarballs are — the same CDN-substitution defence.

The detail schema reserves a reviews slot for a future signed reviews service; it is parsed but never written here.

Backward compatibility

  • v1 catalogues still load unchangedloadCatalogue now accepts versions 1 and 2; all v2 fields are omitempty.
  • An older pilotctl ignores the v2 fields — the bump is backward and forward compatible by construction.
  • No change to install, status, list, or any existing output shape. The catalogue listing gains a teaser line and a view: hint only when v2 fields are present.

Worked example

catalogue.json is migrated to v2 with real teaser data + metadata pins, and catalogue/apps/{io.pilot.cosift,io.pilot.wallet}/metadata.json are populated from the real manifests, repo licenses, bundle sizes, and release notes.

Tests

  • loadCatalogue accepts v1/v2, rejects v3; v1 entries decode with empty v2 fields
  • loadAppMetadata sha-pin verification (correct / mismatch / unpinned / id-mismatch)
  • buildAppViewReport band-merge precedence (detail overrides teaser; real on-disk size wins; manifest-method fallback; sideloaded path)
  • cmdAppStoreView JSON output for the catalogue+metadata+installed case
  • scripts/smoke-test-appstore.sh extended to stage a v2 catalogue + detail doc and assert view end-to-end (catalogue teaser + sha-verified detail + installed manifest)

go test ./cmd/pilotctl/, go vet, gofmt -l, and go build ./... all clean.

How to QA / test before deploy

  1. Build: go build -o /tmp/pilotctl ./cmd/pilotctl
  2. Stage the new catalogue locally (rewrite metadata_url to file:// so it resolves before merge) and run appstore catalogue + appstore view io.pilot.cosift.
  3. Full live e2e: scripts/smoke-test-appstore.sh (needs the wallet repo at ../wallet).

Detailed QA steps are in the PR thread.

Adds `pilotctl appstore view <id>` — a human-app-store-style detail page
that merges three provenance-labelled bands: the catalogue teaser, a
sha-pinned per-app detail doc, and verified local install facts (integrity,
size, granted permissions, methods). Works installed or not, catalogued or
sideloaded; `--json` emits the merged report and `--all-changelog` shows
full history.

Introduces catalogue schema v2: optional teaser fields on each index entry
plus a `metadata_url` + `metadata_sha256` pin to
`catalogue/apps/<id>/metadata.json`, fetched lazily by `view` and verified
the same way bundle tarballs are. v1 catalogues still load unchanged and an
older pilotctl ignores the new fields, so the bump is backward and forward
compatible. The detail schema reserves a `reviews` slot for a future signed
reviews service (not implemented here).

- cmd/pilotctl/appstore_catalogue.go: v2 entry fields, accept versions 1 and
  2, enrich the catalogue listing (vendor/categories/license/size + view hint)
- cmd/pilotctl/appstore_metadata.go: appMetadata schema + sha-pinned loader
- cmd/pilotctl/appstore_view.go: the view command, band merge, renderer
- cmd/pilotctl/appstore.go: dispatch + help wiring
- catalogue: v2 catalogue.json + cosift/wallet metadata.json detail docs
- catalogue/README.md: v2 index + detail schemas, publishing flow, trust model
- scripts/smoke-test-appstore.sh: stage a v2 catalogue + assert view e2e
- tests: loader versioning, sha pinning, band-merge precedence, JSON output
@Alexgodoroja Alexgodoroja requested a review from TeoSlayer as a code owner June 15, 2026 23:39
@Alexgodoroja Alexgodoroja merged commit 0f7b345 into main Jun 15, 2026
8 of 11 checks passed
TeoSlayer pushed a commit that referenced this pull request Jun 16, 2026
…st fixtures

The previous catalogue trust anchor's private key
(publicKeyB64 = "5aCD92R0UoZ2lGW6PYZeRrDw63ZNBC5oJZxFB8RNOPQ=") was lost and is
unrecoverable, so the committed catalogue.json.sig could never be regenerated
to match an edited catalogue. This anchor existed only on this PR branch — not
on main and not in any shipped binary — so rotating it now has zero
installed-base impact.

Changes:
- Generate a fresh ed25519 release keypair (private key stored only outside the
  repo, never committed) and embed its public half as the new trust anchor:
  publicKeyB64 = "iHdBWayA/hYjkwUOZopTXY70qOlR90d6ii/hin0ZMdI="
- Re-sign catalogue/catalogue.json with the new key (catalogue/catalogue.json.sig),
  which now verifies fail-closed against the embedded key.
- Fix the test fixtures #255 left unsigned: stageCatalogue now signs each
  fixture with a per-test ephemeral key (swapped into the embedded anchor for
  the test's duration via catalogtrust.SignWithEphemeralKey, restored on
  cleanup). Tests exercise REAL fail-closed verification against a VALID
  signature — the gate is not skipped, disabled, or weakened; the negative
  tests (missing-signature, tamper) still pass.

Rebased onto origin/main, reconciling #255's v2 metadata schema with this
branch's catalogue signing (kept the fail-closed sig gate in loadCatalogue,
both `view` and `sign-catalogue` subcommands, and main's metadata pin
discipline in catalogue/README.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TeoSlayer added a commit that referenced this pull request Jun 16, 2026
* feat(appstore): sign the catalogue and verify it fail-closed

Replace the placeholder catalogue trust anchor with a real embedded
ed25519 key (internal/catalogtrust, ldflags-overridable for rotation):

- pilotctl loadCatalogue fetches a detached <url>.sig and verifies it
  against the embedded key before trusting any entry; unsigned, missing,
  or tampered catalogues are refused.
- New 'pilotctl appstore sign-catalogue' writes the detached signature and
  refuses to sign with a key that doesn't match the embedded public key.
- cmd/daemon passes the embedded key into appstore.Config.CatalogPubkey,
  retiring the all-zeros placeholder at the wire-up point.
- Sign catalogue.json; document signing + key rotation in catalogue README.

Tests: catalogtrust verify (happy/tamper/short/no-key), signed-catalogue
load + fail-closed (missing sig, tamper).

* feat(catalogue): static site rendering the live catalogue

Dependency-free page (catalogue/site/index.html) that fetches
catalogue.json same-origin and renders one card per app (id, version,
description, install command, bundle link; methods when an entry provides
them). The Pages deploy workflow is added separately (requires workflow
scope on the push token).

* fix(catalogue): rotate signing key + re-sign after #255; sign #255 test fixtures

The previous catalogue trust anchor's private key
(publicKeyB64 = "5aCD92R0UoZ2lGW6PYZeRrDw63ZNBC5oJZxFB8RNOPQ=") was lost and is
unrecoverable, so the committed catalogue.json.sig could never be regenerated
to match an edited catalogue. This anchor existed only on this PR branch — not
on main and not in any shipped binary — so rotating it now has zero
installed-base impact.

Changes:
- Generate a fresh ed25519 release keypair (private key stored only outside the
  repo, never committed) and embed its public half as the new trust anchor:
  publicKeyB64 = "iHdBWayA/hYjkwUOZopTXY70qOlR90d6ii/hin0ZMdI="
- Re-sign catalogue/catalogue.json with the new key (catalogue/catalogue.json.sig),
  which now verifies fail-closed against the embedded key.
- Fix the test fixtures #255 left unsigned: stageCatalogue now signs each
  fixture with a per-test ephemeral key (swapped into the embedded anchor for
  the test's duration via catalogtrust.SignWithEphemeralKey, restored on
  cleanup). Tests exercise REAL fail-closed verification against a VALID
  signature — the gate is not skipped, disabled, or weakened; the negative
  tests (missing-signature, tamper) still pass.

Rebased onto origin/main, reconciling #255's v2 metadata schema with this
branch's catalogue signing (kept the fail-closed sig gate in loadCatalogue,
both `view` and `sign-catalogue` subcommands, and main's metadata pin
discipline in catalogue/README.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Teodor Calin <teodor@vulturelabs.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant