Qortal app (Core node + Hub API + gateway), manifest credentials, quadlet IEC memory limits #164

Closed
ssmithx wants to merge 0 commits from feat/qortal-app into main
Collaborator

Adds Qortal (Core v6.1.9) as an Archipelago app: a full Qortal node that desktop or mobile Qortal Hub can connect to, plus the built-in gateway for Qortal websites and Q-Apps. Minting is out of scope for this PR.

What's in it

  • apps/qortal/

    • Manifest.
    • Rootless image built from the official release jar, with its SHA-256 pinned.
    • Entrypoint that refuses to start if localAuthBypassEnabled is true or apiRestricted is false.
    • Bootstrap-aware health check.
    • Companion status page (nginx in the same container): sync height, peers, Hub connection details and a gateway launcher. Its proxy to Core allows only a short list of GET paths.
  • Network: network: pasta, so inbound p2p peers keep their real IPs and Core stays off the shared rootless netns.

  • API key: generated as a manifest secret (hex32) and handed to Core via secret_env. Core requires it on every admin route.

  • Ports:

    Port Purpose Access
    12390 Status UI Gated
    12380 Gateway Gated
    12391 Hub API auth: open (rationale in the manifest; Core enforces its own API key)
    12392, 12394 Blockchain and QDN p2p Public
  • New manifest feature: credentials block (8287467). Apps can declare credentials for package.credentials, sourced from their own generated_secrets, with optional prompt_before_launch: false. Qortal uses it to show the Hub API key on its dashboard page without a launch prompt. Documented in docs/app-manifest-spec.md.

  • App store: entries in app-catalog/catalog.json and neode-ui/public/catalog.json, plus the generated launch-port and title entries.

Platform fix: memory limits (294dc3a)

The quadlet renderer's parse_memory_mib only understood 4g/512m. It silently dropped 4Gi/512Mi, the form used by 64 manifests, so none of those apps got a memory cap. It now accepts IEC suffixes.

  • Effect: caps take effect on each app's next container restart.
  • Risk: on archy-x250-pa3, every running app was well under its declared limit (bitcoin-core 1.3 GiB of 4 GiB, lnd 348 MiB of 1 GiB). Watch for OOM kills on nodes where an app spikes during first sync.

Tested on archy-x250-pa3

  • Install: from the app store, through the real orchestrator. Data dir, settings.json, ownership (data_uid) and the secret were all created correctly.
  • Hardening: pasta network, read-only root filesystem, all capabilities dropped, non-root user. The status UI, API and gateway bind to loopback only, and are reachable from the LAN only through the app gate.
  • Bootstrap: the API was up in about 10 minutes, then the node synced to 100%. Health stayed healthy throughout the bootstrap.
  • Memory: after the fix, the quadlet renders --memory=4096m and the cap is live (4 GiB).
  • API key: without the key, admin routes return 403; with the generated key, 200. The status UI proxy returns 404 for /core/admin/stop.
  • Gateway: rendered the Qortal site (after the expected first-fetch "Loading" page).
  • Dashboard: the app launches into the status page; checked by the user.

Before release

  • Push qortal-core:6.1.9 to the source.archipelago-foundation.org/lfg2025 mirror. It is only built locally so far.
  • Regenerate and sign releases/app-catalog.json.
  • tests/lifecycle/run-gate.sh on a node.
  • Real-device test of desktop Qortal Hub against the node.

Found along the way (not fixed here)

  • Service restart failure: archipelago.service can fail to restart with Failed to spawn executor: Device or resource busy. Podman leaves nested empty runtime/ cgroups (296 deep on x250-pa3) under the delegated service cgroup. Removing them deepest-first fixes it.
  • Generator includes untracked apps: scripts/generate-app-catalog.py picks up untracked app dirs, which put nomus into the generated launch-port and title lists until it was removed by hand.

🤖 Generated with Claude Code

Adds **Qortal** (Core v6.1.9) as an Archipelago app: a full Qortal node that desktop or mobile Qortal Hub can connect to, plus the built-in gateway for Qortal websites and Q-Apps. Minting is out of scope for this PR. ## What's in it - **`apps/qortal/`** - Manifest. - Rootless image built from the official release jar, with its SHA-256 pinned. - Entrypoint that refuses to start if `localAuthBypassEnabled` is true or `apiRestricted` is false. - Bootstrap-aware health check. - Companion status page (nginx in the same container): sync height, peers, Hub connection details and a gateway launcher. Its proxy to Core allows only a short list of GET paths. - **Network:** `network: pasta`, so inbound p2p peers keep their real IPs and Core stays off the shared rootless netns. - **API key:** generated as a manifest secret (`hex32`) and handed to Core via `secret_env`. Core requires it on every admin route. - **Ports:** | Port | Purpose | Access | |---|---|---| | 12390 | Status UI | Gated | | 12380 | Gateway | Gated | | 12391 | Hub API | `auth: open` (rationale in the manifest; Core enforces its own API key) | | 12392, 12394 | Blockchain and QDN p2p | Public | - **New manifest feature: `credentials` block** (`8287467`). Apps can declare credentials for `package.credentials`, sourced from their own `generated_secrets`, with optional `prompt_before_launch: false`. Qortal uses it to show the Hub API key on its dashboard page without a launch prompt. Documented in `docs/app-manifest-spec.md`. - **App store:** entries in `app-catalog/catalog.json` and `neode-ui/public/catalog.json`, plus the generated launch-port and title entries. ## Platform fix: memory limits (`294dc3a`) The quadlet renderer's `parse_memory_mib` only understood `4g`/`512m`. It silently dropped `4Gi`/`512Mi`, the form used by **64 manifests**, so none of those apps got a memory cap. It now accepts IEC suffixes. - **Effect:** caps take effect on each app's next container restart. - **Risk:** on archy-x250-pa3, every running app was well under its declared limit (bitcoin-core 1.3 GiB of 4 GiB, lnd 348 MiB of 1 GiB). Watch for OOM kills on nodes where an app spikes during first sync. ## Tested on archy-x250-pa3 - **Install:** from the app store, through the real orchestrator. Data dir, `settings.json`, ownership (`data_uid`) and the secret were all created correctly. - **Hardening:** pasta network, read-only root filesystem, all capabilities dropped, non-root user. The status UI, API and gateway bind to loopback only, and are reachable from the LAN only through the app gate. - **Bootstrap:** the API was up in about 10 minutes, then the node synced to 100%. Health stayed `healthy` throughout the bootstrap. - **Memory:** after the fix, the quadlet renders `--memory=4096m` and the cap is live (4 GiB). - **API key:** without the key, admin routes return 403; with the generated key, 200. The status UI proxy returns 404 for `/core/admin/stop`. - **Gateway:** rendered the `Qortal` site (after the expected first-fetch "Loading" page). - **Dashboard:** the app launches into the status page; checked by the user. ## Before release - [ ] Push `qortal-core:6.1.9` to the `source.archipelago-foundation.org/lfg2025` mirror. It is only built locally so far. - [ ] Regenerate and sign `releases/app-catalog.json`. - [ ] `tests/lifecycle/run-gate.sh` on a node. - [ ] Real-device test of desktop Qortal Hub against the node. ## Found along the way (not fixed here) - **Service restart failure:** `archipelago.service` can fail to restart with `Failed to spawn executor: Device or resource busy`. Podman leaves nested empty `runtime/` cgroups (296 deep on x250-pa3) under the delegated service cgroup. Removing them deepest-first fixes it. - **Generator includes untracked apps:** `scripts/generate-app-catalog.py` picks up untracked app dirs, which put `nomus` into the generated launch-port and title lists until it was removed by hand. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ssmithx added 7 commits 2026-10-21 00:42:19 +00:00
Qortal Core (v6.1.9) as a manifest-driven app. Upstream ships no image,
so apps/qortal/Dockerfile wraps the official release jar (SHA-256
pinned) on Temurin 17 JRE, uid 1000:100 (data_uid 100999:100099).

- API 12391 auth: open (Qortal Hub sends X-API-KEY, can't do cookie
  login); key is a generated secret written to apikey.txt by the
  entrypoint, which refuses to start with localAuthBypassEnabled.
- Gateway 12380 gated, launched as the main UI.
- P2P 12392 / QDN 12394 auth: none; review allowlists updated.
- qortal-health treats an in-progress bootstrap as healthy so the
  health monitor doesn't restart away a multi-GB download.

WIP: storage size and API whitelist pending the spike on
archy-x250-pa3; image not yet pushed to the mirror; catalog not
regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Companion UI: nginx inside the qortal image serves apps/qortal/ui on
12390 (gated, now interfaces.main): sync height vs best peer, chain/QDN
peers, version/uptime, Hub connection URL and a gateway launcher. Its
/core/ proxy is a GET-only allowlist of four public Core endpoints and
never carries the API key. start.sh supervises Core + nginx and exits
when either dies; SIGTERM reaches Core (clean shutdown in ~7s).

Spike findings on archy-x250-pa3:
- network: pasta. On a rootless bridge every forwarded connection
  appears to come from the container's own IP, so inbound p2p peers
  would all look identical; Core needs no other app.
- API whitelist off + apiRestricted pinned true (start.sh refuses
  otherwise). Forwarded connections arrive from a dynamic address no
  static whitelist can name; the key guards every state-changing route.
- Bootstrap + sync: 6.8 GB, ~17 min. QDN capped at 20 GiB via
  maxStorageCapacity; storage 40Gi.

API key reveal deliberately left out: the UI cannot verify the
dashboard session from a container, and the gate may be switched to
open at runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apps that hand the operator something to use elsewhere (an API key for
an external client, a first login) needed a hardcoded branch in
handle_package_credentials. Now a manifest declares it:

  credentials:
    title: ...
    prompt_before_launch: false
    items:
      - { label: API key, secret_file: <own generated secret> }
      - { label: Username, value: admin }

- Validation fails closed: a secret_file must be a file this app's own
  generated_secrets produce, so a registry manifest cannot surface
  another app's secret (Bitcoin RPC password, ...). Exactly one of
  value/secret_file; non-empty title, labels and items.
- package.credentials renders it from the effective manifest (verified
  catalog first, then disk) after the existing per-app branches; secret
  items are read at request time and always marked sensitive.
- prompt_before_launch (default false) lets credentials used by another
  client skip the Apps-tab pre-launch modal while still showing on the
  app page. Responses without the field keep the historical prompt.
- Older daemons ignore the block (it lands in extensions).

First user: qortal's Hub API key, which its own UI cannot safely serve
(no way to verify the dashboard session from a container; gated ports
can be flipped to open at runtime).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shown on the app's dashboard page behind the node session, no launch
prompt; the status page points there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the store entry to app-catalog/catalog.json and the dashboard's
bundled catalog.json, and the generated launch-port / title entries
(scripts/generate-app-catalog.py) for the status UI on 12390.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parse_memory_mib only understood g/m suffixes, so every manifest
written as "4Gi" / "512Mi" (64 of them) rendered a quadlet with no
--memory cap at all, silently. podman_client's parser already accepted
these forms; make the quadlet path match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Opening :12391 in a browser lands on Core's disabled API-docs notice,
which reads like a fault. Say so next to the URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ssmithx added 1 commit 2026-10-21 14:11:13 +00:00
Parse settings with jq and require explicit safe booleans before listeners start. Reject missing/malformed configuration and multiple JSON documents. Honour prompt_before_launch in the icon-grid flow and cover false, true and legacy absent flags.

Validation: 17 focused frontend tests, 4 Python guard tests, shell syntax checks, Qortal manifest preflight and diff check passed. Image rebuild and live-node lifecycle acceptance remain pending.
Author
Collaborator

Review continues on ngit

This contribution has been replicated to its canonical ngit proposal:

https://gitworkshop.dev/nevent1qqs2z50zhx3zwlhz7rj2fsv24r5hpkw9axeyhakve8780a0jdcsuujcpz3mhxue69uhhyetvv9ujumn8d96zuer9wc3ca960

The exact existing Git history is preserved, including the review fixes, at 4cc625e4d1aebe57fece3ef5bb8e70f40b372732. No squash, rebase, or merge was performed. The ngit proposal includes the original description, outstanding tasks, and updated source-test results/limitations.

Please continue review and updates on ngit. This Gitea PR remains open as historical context; it has not been merged or marked accepted. Reviews and discussions are platform-specific and have not been copied as approvals.

## Review continues on ngit This contribution has been replicated to its canonical ngit proposal: https://gitworkshop.dev/nevent1qqs2z50zhx3zwlhz7rj2fsv24r5hpkw9axeyhakve8780a0jdcsuujcpz3mhxue69uhhyetvv9ujumn8d96zuer9wc3ca960 The exact existing Git history is preserved, including the review fixes, at `4cc625e4d1aebe57fece3ef5bb8e70f40b372732`. No squash, rebase, or merge was performed. The ngit proposal includes the original description, outstanding tasks, and updated source-test results/limitations. Please continue review and updates on ngit. This Gitea PR remains open as historical context; it has **not** been merged or marked accepted. Reviews and discussions are platform-specific and have not been copied as approvals.
Collaborator

Reconciled against canonical ngit review: Qortal app (Core node + Hub API + gateway), manifest credentials, quadlet IEC memory limits is applied.

This PR's exact head 4cc625e4d1aebe57fece3ef5bb8e70f40b372732 is already an ancestor of accepted main 1a4ea15df9c8800b6d288277d2839c7b93662ff7, which matches on ngit and Gitea. The existing review fixes and commit history are preserved.

Closing the redundant Gitea PR record because its contribution has already been integrated through ngit. No second merge or history rewrite is being performed. Artifact publication and installed-device acceptance remain separate from this source-review disposition.

Reconciled against canonical ngit review: [Qortal app (Core node + Hub API + gateway), manifest credentials, quadlet IEC memory limits](https://gitworkshop.dev/nevent1qqs2z50zhx3zwlhz7rj2fsv24r5hpkw9axeyhakve8780a0jdcsuujcpz3mhxue69uhhyetvv9ujumn8d96zuer9wc3ca960) is **applied**. This PR's exact head `4cc625e4d1aebe57fece3ef5bb8e70f40b372732` is already an ancestor of accepted main `1a4ea15df9c8800b6d288277d2839c7b93662ff7`, which matches on ngit and Gitea. The existing review fixes and commit history are preserved. Closing the redundant Gitea PR record because its contribution has already been integrated through ngit. No second merge or history rewrite is being performed. Artifact publication and installed-device acceptance remain separate from this source-review disposition.
chaum closed this pull request 2026-11-09 12:31:33 +00:00

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.