# Qortal preparation components — not a catalogue-ready package This directory contains startup validation and a locally buildable candidate image, not an installable manifest, published image or accepted app entry. Full package, first boot, lifecycle, credential, provenance and operator gates remain. Bounded initialized-fixture API evidence is described separately below. The initial packaging proposal is Gitea PR164 at `f115478fdfd7f561fed4f48d46333b6b204f7253`, subsequently updated to `4cc625e4d1aebe57fece3ef5bb8e70f40b372732`. The image/nginx/status page are selectively derived from that proposal, retaining the stronger guard here. Acceptance/replacement must follow Archipelago's canonical ngit workflow. Its generic manifest-selected credential reader is deliberately **not included**: declaring a shared generated-secret filename does not establish exclusive app ownership. ## Startup contract `start.sh` expects Python 3 and `check-settings.py` installed as `/usr/local/bin/qortal-check-settings`, the pinned jar at `/usr/local/qortal/qortal.jar`, and the companion nginx configuration at `/etc/qortal-ui/nginx.conf`. The Dockerfile supplies these prerequisites and pins the Temurin base, official JAR and upstream logging configuration. Build locally with `podman build -t localhost/archipelago-qortal:6.1.9-archy.1 apps/qortal`. The logging file is explicitly selected from the read-only image. The operator's `/qortal/settings.json` is never repaired or overwritten. It must be a regular, non-symlink file containing strict UTF-8 JSON, at most 1 MiB, with unique decoded property names. Core accepts some additional comment/trailing comma syntax; this candidate intentionally refuses those extensions rather than risk checking a different interpretation. Any existing settings conversion requires explicit review and preservation of the original, not an automatic migration. Required explicit booleans: ```json { "localAuthBypassEnabled": false, "apiRestricted": true, "autoUpdateEnabled": false } ``` Other ordinary settings are retained. `userPath`, `apiKey` and `apiKeyPath` are refused even if null: settings redirection or overriding the managed key source would invalidate the startup check. Auto-update is disabled for pinned package provenance; missing flags fail closed rather than silently modifying policy. `apiRestricted` is not a substitute for route-by-route API-key authorization. The helper writes a private, exclusive-created runtime snapshot in a fresh mode-0700 temporary directory. Only that snapshot is passed to Core. Edits to the persistent source after validation cannot change this launch's document. Snapshot files are removed after both children stop. SIGTERM/SIGINT are forwarded; the wrapper waits for Core and UI cleanup and preserves the stop exit status. `QORTAL_API_KEY` must contain the platform-generated 32-byte hexadecimal key. A new `apikey.txt` is created mode 0600 without a newline. An existing regular file must be private and match exactly; missing/malformed input, symlinks, FIFOs, exposed permissions or conflicts are refused. The wrapper does **not** rotate, trim, repair or replace an existing key to make startup succeed. Conflicts require explicit recovery. The environment secret is removed before either child launches, and failures do not print values. ## Evidence and remaining exits - `test_settings.py`: eleven tests with multiple adversarial cases, run in a secret-free, network-none, read-only disposable Python container. - Actual official Qortal 6.1.9 jar parser paired with the new entrypoint in a bounded disposable JRE/Python image: safe snapshot interpretation, original escaped-name regressions, duplicate keys, redirection, environment-key removal, key preservation, child cleanup and TERM behavior pass. - The initial entrypoint/parser probe used stub children; it did not qualify the actual daemon. Subsequent checks below run the actual JAR and nginx. The source jar's SHA256 was verified as `a5fbe763645a53995565c9cb93491e024384431ee29eee38a4146b6a13ed2e87`, matching the official release asset and proposal pin. No daemon/network/funds were launched by the parser test. Images used for tests are not release artifacts. ## Package-side empty-cache overlay prerequisite `prepare-runtime-jar.py` is the package entrypoint boundary for the accepted empty-cache behavior. It never replaces `/usr/local/qortal/qortal.jar`. It reads the official JAR and replacement class as bounded no-follow regular-file snapshots, verifies both against `empty-cache-overlay-contract.json`, and creates an exclusive runtime JAR plus rollback receipt in the private runtime directory. It rejects duplicate/missing target entries, signed JARs, changed bytes, noncanonical contract fields, existing outputs and rollback byte/path drift. `start.sh` runs this preparation before settings, credentials, nginx or Core and launches only the verified runtime JAR. The checked-in contract deliberately records `upstream_provenance_status` as `pending` and has no provenance receipt hash. The Dockerfile therefore copies the helper and contract but **does not embed the retained test-only class**. Until an independent upstream/package provenance decision supplies and reviews that exact class and changes the contract atomically, startup refuses with exit 78. The retained fixture JAR and class under historical evidence are not silently promoted into package inputs. Rollback verifies the unchanged official JAR and exact prepared runtime bytes before deleting only the runtime JAR and its receipt. Operator `/qortal` data is outside this helper. Credential drift remains separately fail-closed through `qortal-check-settings`; if an existing API key conflicts, the newly created settings snapshot is removed and neither key nor operator data is rewritten. Focused tests exercise nonempty state preservation, prepare/rollback, missing or changed official JAR, changed class, pending provenance, changed runtime rollback refusal and credential conflict through the production helpers. ## Actual image fixture — 2026-10-21 `tests/apps/qortal/run-disposable.sh IMAGE initialized-fixture` passes with the actual Core/nginx processes, nonroot UID/GID `1000:100`, network-none, read-only root and disposable storage. It checks missing/wrong API-key denial on the real **GET** `/admin/stop` route (403), correct-key stop (200), restricted nginx proxy routes, private managed-key bytes/mode/inode, absence of the environment secret from both children, operator-settings/sentinel preservation across recreation, and refusal of a changed secret without rotation or disclosure. No live node, wallet, peers or funds are involved. **This deliberately does not qualify fresh genesis or bootstrap.** With `bootstrap=false`, the official 6.1.9 JAR fails before API startup with `statement is not in batch mode` during empty QDN-cache initialization. Default `fresh` mode preserves that failing reproduction. The separate initialized fixture uses the actual pinned Core genesis processor, commits the empty-cache metadata flag, and uses `bootstrap=true, archiveEnabled=false` in synthetic settings to avoid the upstream height-below-three rebuild. It invents no chain transactions or signatures; it is nevertheless an explicit prepared database, not a fix, installer migration, first-boot pass or bootstrap qualification. Never use `InitializedGenesis.java` against operator data. The image's local tag is not yet admitted by the fixed credential reader; the same-origin privileged credential boundary is also unresolved. No generic credential schema or catalogue manifest is activated here. ## Empty-cache candidate — 2026-10-22 A narrow **unpublished test-only** source patch is retained at `tests/apps/qortal/empty-cache.patch`. It skips `executeBatch()` only when no signature entries were added; normal lower-case cache population, completion flag, transaction commit and nonempty batch updates remain unchanged. It does not preseed a database or bypass startup validation. `EmptyCacheRegression.java` runs the actual pinned HSQLDB and cache manager against an in-memory synthetic schema. The original class reproduces the empty batch error. The patched class passes empty initialization, idempotence and nonempty latest-signature selection/preservation. No Core or wallet is started by this focused probe. `make-cache-fixture.py` creates an exclusive new test JAR from the exact original SHA256, refuses signed or ambiguous originals, and verifies that exactly one class entry differs. It does not overwrite the original or publish a package. The candidate overlay SHA256 is `252b6c42a1ec2e7f35b879676650e44579a8f221665a7a69e421a2a5cba7581a`. With that overlay, `run-disposable.sh IMAGE fresh` passes actual Core/nginx fresh startup, API-key negatives, restricted UI proxy, graceful stop/recreation, private-key/settings preservation and changed-secret refusal. This used an empty named disposable volume, **not** `InitializedGenesis.java`, with external network disabled, 2 GiB memory and zero swap allowance. To supply a test overlay, set `QORTAL_FIXTURE_JAR` and its exact `QORTAL_FIXTURE_JAR_SHA256`; neither modifies the image or installed data. This does NOT make the original official JAR fixed. Source/provenance and independent review, clean compatible packaging, nonempty upgrade/rollback, network/bootstrap/gateway and installed-node qualification remain required. No production Dockerfile, manifest or catalog entry is switched to this overlay. Still required: independent security review, a bounded credential card or actual app-owned secret design, sanitized manifest/image/UI integration, API denial and Hub/gateway checks, health truthfulness, sizing, upgrade/backup/recovery/rollback, package/source acceptance and signed catalogue admission. Do not deploy these components as a completed app or overwrite a live operator configuration.