diff --git a/docs/contributing/development.md b/docs/contributing/development.md index 0389167..1ed7c4d 100644 --- a/docs/contributing/development.md +++ b/docs/contributing/development.md @@ -85,6 +85,33 @@ builds. Same tooling, different ``. To promote an edge build to stable, rebuild with `make_release.sh stable` at that ref (or copy its artifacts into the `stable/` path and update `stable/latest.json`). +## Publish an app to the marketplace catalog + +The registry/marketplace rides the same signed channel as releases and hotfixes +(`docs/roadmap/updates-and-distribution.md` §8): one `index.json`, one signing +key, one static tree. An app in the catalog is just the drop-in definition +folder, packed and signed: + +```bash +scripts/release/make_app.sh [channel] [spec.json] # e.g. make_app.sh speedtest +``` + +Produces, under `dist//`: `payloads/app-.tar.gz(.minisig)`, a +pinned catalog icon under `payloads/icons/`, and the upserted + re-signed +`index.json`. Title/category/descriptions come from the app's own `.config` — +no second source of truth; the optional `spec.json` overlays envelope fields +(`id`, `version`, `why`, `applies_when.min_lp`, …). Re-publishing an unchanged +app is byte-identical and keeps its version; changed bytes auto-bump it. + +Boxes surface catalog apps in the App Center as **"Available — Add"** cards +(`updater check` refreshes `registry_catalog.json`), and `libreportal app add +` verifies + quarantine-validates the bundle and drops the definition +into the install tree — from there it's a normal app with a normal Install +button. Local definitions always win over the catalog; installed apps are +never touched. Test the whole loop against a local server exactly like a +release (`python3 -m http.server` + `LP_RELEASE_BASE_URL`), starting with +`libreportal artifact index`. + ## Test a release locally before publishing No hosting needed — serve `dist/` and point an install at it: diff --git a/docs/roadmap/updates-and-distribution.md b/docs/roadmap/updates-and-distribution.md index 71db913..7e597f7 100644 --- a/docs/roadmap/updates-and-distribution.md +++ b/docs/roadmap/updates-and-distribution.md @@ -358,6 +358,36 @@ components is **purely additive**: This is exactly the §3 "registry, not marketplace" shape, now expressed in the format. +### 8.4b App bundles — the registry payload (spec addendum, built 2026-07-03) + +The first non-hotfix type through the pipe. **Envelope**: byte-for-byte §8.1 with +`type:"app"`, `payload.kind:"bundle"` (a `.tar.gz`), the app slug in +`applies_when.app` (fork 3's field doing double duty — for `type:"app"` the +resolver skips the "must be installed" gate; presence is the collision policy's +call), and one **optional additive object** for the browse UI: + +```jsonc +"meta": { "category": "media", "description": "…", "long_description": "…", + "icon": "stable/payloads/icons/.svg", "icon_sha256": "…" } +``` + +Old boxes ignore `meta` (parsers select known fields). Icons are mirrored locally +by the scan **only when their bytes match the pin** — the browser is never pointed +at a remote host. + +**Tarball contract** (enforced fail-closed by the box-side quarantine validator +*before* anything lands in the live-sourced definition tree — placing a definition +is deferred code execution, so trust gates → quarantine → placement, in that +order): gzip; exactly one top-level dir `== slug`; slug `^[a-z0-9][a-z0-9_]{0,31}$` +(shell-identifier-safe; `template`/`libreportal`/`tools`/`scripts`/`resources` +reserved); regular files + dirs only (no links/devices); no `..`/absolute paths; +path charset `[A-Za-z0-9._/@:+-]`; caps 5 MB tarball / 20 MB declared extracted / +400 entries; set-id bits stripped; must contain `.config` (TITLE+CATEGORY, +parsed line-wise, never sourced) + `docker-compose.yml`; every `*.sh` must pass +`bash -n`. **Collision policy:** an installed app refuses (updating live apps is +the updater's job); a local non-registry definition always wins; a registry-owned +definition updates reversibly (prior tree packed into the undo array). + ### 8.5 Fork resolutions (was §6) 1. **Hotfix scope** → **config/compose ops + checksum-pinned file patches; NO code @@ -449,8 +479,29 @@ This is exactly the §3 "registry, not marketplace" shape, now expressed in the - ✅ **Phase 5 — publisher tooling (BUILT 2026-05-31).** `make_hotfix.sh` turns a spec into the signed payload + index entry (serial bump, freshness, publishers map), minisign-signs with `LP_MINISIGN_SECKEY`. Verified end-to-end in unsigned/local mode. -- ⬜ **Deferred (registry; additive, demand-gated — intentionally NOT built).** - `payload.kind:"bundle"` applier (verify tarball → extract into the app tree → scan/regen) + - `type:"app"|"theme"|"component"` + the `app_add` task + community trust-tier **host-script - quarantine** (§3.2) + multi-source "tap" UX + the warrant-canary countersigning - `index_serial`. The hotfix product (Phases 1–5) is complete; the registry waits for demand. +- ✅ **Phase 6 — the registry slice: app bundles + Browse-&-Add (BUILT 2026-07-03).** + - `make_app.sh ` packs `containers//` (the unchanged drop-in contract) + into a deterministic signed tarball + a `type:"app"` / `payload.kind:"bundle"` + envelope (metadata derived from the app's own `.config`; `auto:false` forced — + apps never auto-apply). Shared index logic factored into `lib/release_index.sh`. + - Box side: the two designed seams opened (`_artifactResolve` accepts `app`; + `_artifactApplyBundleFlow` handles `bundle`) — fetch → sha256-pin → minisig → + **quarantine validation** (§8.8) → place in the definition tree → regen → + verify-in-apps.json → applied-record with precise undo → History. Collision + policy: installed-live refused, local definitions win, registry-owned re-add = + reversible definition update. Revert removes/restores the definition. + - `libreportal app add ` (the `app_add` task) + `webuiRegistryCatalogScan` + (registry_catalog.json + pin-verified same-origin icons) + App Center + "Available — Add" cards (indigo pill, official badge; local always wins). + The Improvements stream is now explicitly hotfix-only (unknown types skip+log). + - En route, two latent trust-gate bugs fixed: `LP_INDEX_SIGSTATE` (and the whole + resolve-global set) was stranded in command-substitution subshells — on a + signed box every apply would have refused as unsigned. `lpFetchIndexInto` + + direct-call resolve now propagate them. + - Harness-tested: 10/10 (read path), 46/46 (bundle applier), 14/14 (catalog scan); + live task-loop + WebUI verified visually. +- ⬜ **Deferred (additive, demand-gated — intentionally NOT built).** Community + trust-tier **host-script quarantine** (§3.2) + multi-source "tap" UX + + `type:"theme"|"component"` bundle handlers + the warrant-canary countersigning + `index_serial`. The browsable marketplace *website* (shipped as a dev-mode-gated + LibrePortal app) is designed in `marketplace-website.md`.