From e3392c7750a5ae3607480f3ca3f2172ccf25028a Mon Sep 17 00:00:00 2001 From: "Foluso ADEBISI (DevBox Aux)" Date: Fri, 2 Oct 2026 09:53:35 +0100 Subject: [PATCH] docs(openspec): archive fpagent-release-installer and sync specs --- .../.openspec.yaml | 2 + .../design.md | 85 +++++++++++ .../proposal.md | 49 +++++++ .../specs/fpagent-installer/spec.md | 136 ++++++++++++++++++ .../specs/tools-app/spec.md | 34 +++++ .../tasks.md | 23 +++ openspec/specs/fpagent-installer/spec.md | 136 ++++++++++++++++++ openspec/specs/tools-app/spec.md | 19 ++- 8 files changed, 479 insertions(+), 5 deletions(-) create mode 100644 openspec/changes/archive/2026-10-02-fpagent-release-installer/.openspec.yaml create mode 100644 openspec/changes/archive/2026-10-02-fpagent-release-installer/design.md create mode 100644 openspec/changes/archive/2026-10-02-fpagent-release-installer/proposal.md create mode 100644 openspec/changes/archive/2026-10-02-fpagent-release-installer/specs/fpagent-installer/spec.md create mode 100644 openspec/changes/archive/2026-10-02-fpagent-release-installer/specs/tools-app/spec.md create mode 100644 openspec/changes/archive/2026-10-02-fpagent-release-installer/tasks.md create mode 100644 openspec/specs/fpagent-installer/spec.md diff --git a/openspec/changes/archive/2026-10-02-fpagent-release-installer/.openspec.yaml b/openspec/changes/archive/2026-10-02-fpagent-release-installer/.openspec.yaml new file mode 100644 index 0000000..ad94bad --- /dev/null +++ b/openspec/changes/archive/2026-10-02-fpagent-release-installer/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-02 diff --git a/openspec/changes/archive/2026-10-02-fpagent-release-installer/design.md b/openspec/changes/archive/2026-10-02-fpagent-release-installer/design.md new file mode 100644 index 0000000..37d32bc --- /dev/null +++ b/openspec/changes/archive/2026-10-02-fpagent-release-installer/design.md @@ -0,0 +1,85 @@ +# Design + +## Context + +See proposal.md — Why. The `fingerprint-agent` repository and its releases are +private: an unauthenticated request to the forge's releases API answers `404`. +Releases already publish, per build, a generic bundle +(`fpagent--linux-x86_64.tar.gz`), per-Ubuntu offline bundles +(`fpagent--ubuntu--x86_64.tar.gz`) with the libfprint `.deb` +closure vendored, and a `.sha256` beside each. Every bundle already contains +the fpagent binary, the udev rule, and its own `install.sh` that handles +libfprint, udev, `plugdev` and the systemd service. The tools app is an Express +server with a hardcoded-configuration policy whose only sanctioned environment +variables were the Image Cropper credentials. + +## Goals / Non-Goals + +**Goals:** + +- A field laptop installs or updates fpagent with one public command. +- The forge credential never reaches the field laptop. +- Reuse the bundle's installer rather than reimplementing hardware setup. + +**Non-Goals:** + +- Replacing the bundle installer, or the forge's own release tooling. +- Authenticating the installer: it is intentionally public. +- Supporting architectures other than x86_64 (the bundle installer already + requires it). + +## Decisions + +1. **Proxy the release through the app; do not embed the key in the script.** + The credential lives in `GITEA_RELEASE_INSTALL_KEY` and the app fetches the + private release server-side. The alternative — rendering the key into the + served script so the client could call the forge directly — publishes a + repository credential to every installer and any `curl`; rejected. + +2. **Two thin routes, no proxy framework.** `/fpagent/releases/latest` returns + a trimmed `{tag, assets:[{name,url}]}` with same-origin URLs, and + `/fpagent/releases/download/:tag/:name` streams one asset. Both are + registered before the session middleware so they stay public and do no + per-request PocketBase work. A single combined "give me the right tarball" + route was considered but needs the server to guess the client's OS; the + client knows its own codename, so selection happens in the script. + +3. **The installer delegates to the bundle's own `install.sh`.** All hardware + and dependency logic already exists and is versioned with the binary; + reimplementing it in the hosted script would duplicate it and drift. The + hosted script only fetches, verifies, extracts, and forwards flags. + +4. **Bundle selection prefers the codename offline bundle.** When the host's + `/etc/os-release` `VERSION_CODENAME` matches a published offline bundle, use + it (no network needed for the dependency closure); otherwise fall back to + the generic online bundle, whose installer apt-gets libfprint. + +5. **Checksum verification, not signature verification.** The release publishes + sha256 only, so the installer verifies sha256 over TLS from the same origin + it trusts for the script itself. GPG would require release signing that does + not exist yet; deferred. + +6. **Asset names are validated server-side** against a conservative pattern, + so a crafted tag or name cannot walk the forge path or reach another host. + +## Risks / Trade-offs + +- [A compromised tools app serves a malicious installer] → the app is already + trusted to serve the tools pages and holds the operator session; the + installer is not a new trust boundary. TLS to `tools.yrka.app` is the same + trust as the app. +- [The forge changes its bundle naming] → selection falls back to the generic + bundle, and a release with no matching asset fails loudly rather than + installing something wrong. +- [The app buffers an asset in memory] → bundles are tens of MB; acceptable, + and requests are rare. Streaming can replace it if that changes. + +## Migration Plan + +Add `GITEA_RELEASE_INSTALL_KEY` to `.env`/`.env.production` (already supplied) +and deploy the app; the installer and proxy are new routes and change no +existing behavior. Rollback = remove the routes; nothing else depends on them. + +## Open Questions + +None. diff --git a/openspec/changes/archive/2026-10-02-fpagent-release-installer/proposal.md b/openspec/changes/archive/2026-10-02-fpagent-release-installer/proposal.md new file mode 100644 index 0000000..268606f --- /dev/null +++ b/openspec/changes/archive/2026-10-02-fpagent-release-installer/proposal.md @@ -0,0 +1,49 @@ +# Proposal: fpagent-release-installer + +## Why + +Field laptops need `fpagent` installed and kept current, but the +`fingerprint-agent` repository is private: its releases require a forge token +that operators do not have. Today an operator must obtain a bundle out of band +and run its installer by hand. A public one-line installer hosted by the tools +app removes that step while keeping the forge credential server-side. + +## What Changes + +- Add a public installer script served at `/fpagent/install.sh`, intended to be + piped into `bash`. It fetches the latest release, selects the bundle matching + the host (the Ubuntu-codename offline bundle when present, else the generic + online bundle), verifies the published sha256, extracts it, and runs the + bundle's own installer, forwarding flags such as `--enable-service`, + `--check` and `--uninstall`. +- Add same-origin proxy routes `/fpagent/releases/latest` and + `/fpagent/releases/download/:tag/:name` that read the private release from the + forge using the `GITEA_RELEASE_INSTALL_KEY` environment variable and stream + assets to the client. The token and the forge host are never exposed. +- Extend the app's sanctioned environment configuration to include the installer + key, alongside the existing Image Cropper credentials. + +## Capabilities + +### New Capabilities + +- `fpagent-installer`: the public installer surface — the download/verify/handoff + script and the token-guarded same-origin release proxy that backs it. + +### Modified Capabilities + +- `tools-app`: the hardcoded-configuration requirement now sanctions a second + environment variable (the fpagent installer key) in addition to the Image + Cropper credentials. + +## Impact + +- `tools/server.js`: `GITEA_URL` / `FPAGENT_REPO` constants, the installer key + read, and two public routes registered before the session middleware. +- `tools/public/fpagent/install.sh`: new installer script. +- `tools/test/fpagent-install.test.js`: proxy and script tests. +- `.env` / `.env.production` hold the key; `.env.example` and `README.md` + document the install flow. +- Depends on the private `fingerprint-agent` releases and their bundle naming + (`fpagent--{linux-x86_64|ubuntu--x86_64}.tar.gz`) plus the + matching `.sha256` assets. diff --git a/openspec/changes/archive/2026-10-02-fpagent-release-installer/specs/fpagent-installer/spec.md b/openspec/changes/archive/2026-10-02-fpagent-release-installer/specs/fpagent-installer/spec.md new file mode 100644 index 0000000..529ae01 --- /dev/null +++ b/openspec/changes/archive/2026-10-02-fpagent-release-installer/specs/fpagent-installer/spec.md @@ -0,0 +1,136 @@ +# fpagent-installer Specification + +## Purpose + +Lets a field laptop install the latest private `fpagent` release with a single +public command, without ever handling the forge credential — the tools app +proxies the private release and serves the installer. + +## ADDED Requirements + +### Requirement: Public installer endpoint + +The app SHALL serve an installer script at `/fpagent/install.sh` that is +reachable without authentication. When run, the script SHALL fetch the latest +release metadata, choose a bundle for the host, verify the bundle's published +sha256 checksum, extract it, and hand off to the extracted bundle's own +installer. The script SHALL be served uncached. + +#### Scenario: Installer is fetched anonymously + +- **WHEN** a client requests `/fpagent/install.sh` without a session +- **THEN** the app returns the installer script and does not redirect to login + +#### Scenario: Host-appropriate bundle is chosen + +- **WHEN** the latest release offers both a bundle for the host's Ubuntu + codename and a generic bundle +- **THEN** the installer uses the codename bundle + +#### Scenario: Generic bundle is the fallback + +- **WHEN** the latest release offers no bundle matching the host's Ubuntu + codename +- **THEN** the installer uses the generic bundle + +#### Scenario: Checksum is verified before extraction + +- **WHEN** the installer downloads a bundle and its published checksum +- **THEN** it verifies the checksum before extracting and refuses to install + when the checksum does not match + +#### Scenario: Options are forwarded to the bundled installer + +- **WHEN** the installer is invoked with options such as `--enable-service` or + `--check` +- **THEN** it passes those options through to the extracted bundle's installer + +#### Scenario: No compatible bundle + +- **WHEN** the latest release contains no bundle for the host architecture +- **THEN** the installer exits with a failure message and installs nothing + +### Requirement: Release metadata endpoint + +The app SHALL expose a same-origin endpoint that returns the latest release's +tag and the names of its downloadable assets as same-origin URLs. The app SHALL +authenticate to the private forge using the configured release key, SHALL omit +asset names that are not valid asset names, and SHALL NOT reveal the release +key or the forge host to clients. + +#### Scenario: Latest release is returned + +- **WHEN** a client requests the latest-release endpoint and the forge returns + a release +- **THEN** the response carries the release tag and each asset's name and a + same-origin URL under `/fpagent/releases/download/` + +#### Scenario: Unsafe asset names are omitted + +- **WHEN** the forge release lists an asset whose name is not a valid asset + name +- **THEN** that asset does not appear in the response + +#### Scenario: Release key is not exposed + +- **WHEN** any client fetches the latest-release endpoint +- **THEN** the response body contains neither the release key nor the forge host + +#### Scenario: Installer not configured + +- **WHEN** the release key is not configured +- **THEN** the endpoint answers that the installer is not configured + +#### Scenario: Forge unavailable + +- **WHEN** the forge cannot be reached or returns an error for the latest + release +- **THEN** the endpoint answers with a gateway error + +### Requirement: Release asset download proxy + +The app SHALL stream a requested release asset to the client through a +same-origin route, authenticating the upstream request with the release key so +the client sends no credentials. It SHALL reject asset names that are not valid +asset names and SHALL report a missing asset as not found. + +#### Scenario: Asset is streamed without client credentials + +- **WHEN** a client requests a release asset by tag and name without any + credential +- **THEN** the app fetches it from the forge with the release key and streams + its bytes to the client + +#### Scenario: Invalid asset name is rejected + +- **WHEN** a client requests an asset whose tag or name is not a valid asset + name +- **THEN** the app rejects the request without contacting the forge + +#### Scenario: Missing asset + +- **WHEN** the forge reports the requested asset as not found +- **THEN** the app answers with not found + +#### Scenario: Installer not configured + +- **WHEN** the release key is not configured +- **THEN** the endpoint answers that the installer is not configured + +### Requirement: Installer credential stays server-side + +The app SHALL read the private-forge release key from the +`GITEA_RELEASE_INSTALL_KEY` environment variable. The key SHALL NOT appear in +the installer script, in any release metadata response, or in any streamed +asset. + +#### Scenario: Served installer carries no credential + +- **WHEN** a client fetches `/fpagent/install.sh` +- **THEN** the response contains no release key + +#### Scenario: Credential supplied by the environment + +- **WHEN** the release key is supplied in the environment +- **THEN** the installer and proxy endpoints use it without any change to the + hardcoded configuration diff --git a/openspec/changes/archive/2026-10-02-fpagent-release-installer/specs/tools-app/spec.md b/openspec/changes/archive/2026-10-02-fpagent-release-installer/specs/tools-app/spec.md new file mode 100644 index 0000000..49c773f --- /dev/null +++ b/openspec/changes/archive/2026-10-02-fpagent-release-installer/specs/tools-app/spec.md @@ -0,0 +1,34 @@ +# tools-app Specification + +## MODIFIED Requirements + +### Requirement: Configuration is hardcoded + +The app SHALL hardcode all configuration in source: port `4173`, portal +`https://portal.yrka.app`, PocketBase `https://pb.itcass.net`, a2loft +`https://apis.a2loft.net`, agent `http://127.0.0.1:9000`, and the fingerprint +agent release forge `https://sources.yrka.app`. There SHALL be no runtime +configuration via environment variables or config files, with two exceptions: +the Image Cropper's service credentials (Minio keys, Azure storage connection +string, nodered credentials, cleanup-service key) SHALL come from environment +variables and SHALL NOT be stored in source; and the Fingerprint Agent +installer's release key (`GITEA_RELEASE_INSTALL_KEY`) SHALL come from the +environment and SHALL NOT be stored in source. + +#### Scenario: No configuration required to run + +- **WHEN** the app is started with no environment variables or config files +- **THEN** it serves on port 4173 with the hardcoded external endpoints + +#### Scenario: Cropper credentials from the environment + +- **WHEN** the cropper credentials are supplied as environment variables +- **THEN** the cropper routes use them without any change to the hardcoded + URL/port configuration + +#### Scenario: Installer key from the environment + +- **WHEN** the fpagent installer release key is supplied as an environment + variable +- **THEN** the installer and release-proxy routes use it without any change to + the hardcoded URL/port configuration diff --git a/openspec/changes/archive/2026-10-02-fpagent-release-installer/tasks.md b/openspec/changes/archive/2026-10-02-fpagent-release-installer/tasks.md new file mode 100644 index 0000000..7d1fd97 --- /dev/null +++ b/openspec/changes/archive/2026-10-02-fpagent-release-installer/tasks.md @@ -0,0 +1,23 @@ +# Tasks + +- [x] 1. Server: `GITEA_URL`/`FPAGENT_REPO` constants, the installer key read + from `GITEA_RELEASE_INSTALL_KEY`, and two public proxy routes registered + before the session middleware: `GET /fpagent/releases/latest` (trimmed + `{tag, assets:[{name,url}]}`, same-origin URLs, unsafe names dropped) and + `GET /fpagent/releases/download/:tag/:name` (validated names, streamed with + the key, 404/502 mapping, 503 when unconfigured). +- [x] 2. Installer script `tools/public/fpagent/install.sh`: fetch metadata, + prefer the host-codename offline bundle over the generic one, download and + verify sha256, extract, and forward all options to the bundle's `install.sh`. + Served uncached at `/fpagent/install.sh`. +- [x] 3. Tests (`tools/test/fpagent-install.test.js`): latest proxy shape and + token non-disclosure, asset streaming with upstream auth, invalid-name + rejection, missing-asset 404, unconfigured 503, public script serving, and + `bash -n` on the script. +- [x] 4. Docs/config: `GITEA_RELEASE_INSTALL_KEY` in `.env.example`, installer + and configuration notes in `README.md`. +- [x] 5. Verified: 55 tests pass (48 existing + 7 new); routes exercised live + against the forge (latest release `v1.0.0`, proxied checksum matches the + publisher's, invalid name refused); the installer run end to end against a + fake release base — codename bundle chosen, checksum pass and fail, options + forwarded to the extracted bundle installer. diff --git a/openspec/specs/fpagent-installer/spec.md b/openspec/specs/fpagent-installer/spec.md new file mode 100644 index 0000000..afe0cb5 --- /dev/null +++ b/openspec/specs/fpagent-installer/spec.md @@ -0,0 +1,136 @@ +# fpagent-installer Specification + +## Purpose + +Lets a field laptop install the latest private `fpagent` release with a single +public command, without ever handling the forge credential — the tools app +proxies the private release and serves the installer. + +## Requirements + +### Requirement: Public installer endpoint + +The app SHALL serve an installer script at `/fpagent/install.sh` that is +reachable without authentication. When run, the script SHALL fetch the latest +release metadata, choose a bundle for the host, verify the bundle's published +sha256 checksum, extract it, and hand off to the extracted bundle's own +installer. The script SHALL be served uncached. + +#### Scenario: Installer is fetched anonymously + +- **WHEN** a client requests `/fpagent/install.sh` without a session +- **THEN** the app returns the installer script and does not redirect to login + +#### Scenario: Host-appropriate bundle is chosen + +- **WHEN** the latest release offers both a bundle for the host's Ubuntu + codename and a generic bundle +- **THEN** the installer uses the codename bundle + +#### Scenario: Generic bundle is the fallback + +- **WHEN** the latest release offers no bundle matching the host's Ubuntu + codename +- **THEN** the installer uses the generic bundle + +#### Scenario: Checksum is verified before extraction + +- **WHEN** the installer downloads a bundle and its published checksum +- **THEN** it verifies the checksum before extracting and refuses to install + when the checksum does not match + +#### Scenario: Options are forwarded to the bundled installer + +- **WHEN** the installer is invoked with options such as `--enable-service` or + `--check` +- **THEN** it passes those options through to the extracted bundle's installer + +#### Scenario: No compatible bundle + +- **WHEN** the latest release contains no bundle for the host architecture +- **THEN** the installer exits with a failure message and installs nothing + +### Requirement: Release metadata endpoint + +The app SHALL expose a same-origin endpoint that returns the latest release's +tag and the names of its downloadable assets as same-origin URLs. The app SHALL +authenticate to the private forge using the configured release key, SHALL omit +asset names that are not valid asset names, and SHALL NOT reveal the release +key or the forge host to clients. + +#### Scenario: Latest release is returned + +- **WHEN** a client requests the latest-release endpoint and the forge returns + a release +- **THEN** the response carries the release tag and each asset's name and a + same-origin URL under `/fpagent/releases/download/` + +#### Scenario: Unsafe asset names are omitted + +- **WHEN** the forge release lists an asset whose name is not a valid asset + name +- **THEN** that asset does not appear in the response + +#### Scenario: Release key is not exposed + +- **WHEN** any client fetches the latest-release endpoint +- **THEN** the response body contains neither the release key nor the forge host + +#### Scenario: Installer not configured + +- **WHEN** the release key is not configured +- **THEN** the endpoint answers that the installer is not configured + +#### Scenario: Forge unavailable + +- **WHEN** the forge cannot be reached or returns an error for the latest + release +- **THEN** the endpoint answers with a gateway error + +### Requirement: Release asset download proxy + +The app SHALL stream a requested release asset to the client through a +same-origin route, authenticating the upstream request with the release key so +the client sends no credentials. It SHALL reject asset names that are not valid +asset names and SHALL report a missing asset as not found. + +#### Scenario: Asset is streamed without client credentials + +- **WHEN** a client requests a release asset by tag and name without any + credential +- **THEN** the app fetches it from the forge with the release key and streams + its bytes to the client + +#### Scenario: Invalid asset name is rejected + +- **WHEN** a client requests an asset whose tag or name is not a valid asset + name +- **THEN** the app rejects the request without contacting the forge + +#### Scenario: Missing asset + +- **WHEN** the forge reports the requested asset as not found +- **THEN** the app answers with not found + +#### Scenario: Installer not configured + +- **WHEN** the release key is not configured +- **THEN** the endpoint answers that the installer is not configured + +### Requirement: Installer credential stays server-side + +The app SHALL read the private-forge release key from the +`GITEA_RELEASE_INSTALL_KEY` environment variable. The key SHALL NOT appear in +the installer script, in any release metadata response, or in any streamed +asset. + +#### Scenario: Served installer carries no credential + +- **WHEN** a client fetches `/fpagent/install.sh` +- **THEN** the response contains no release key + +#### Scenario: Credential supplied by the environment + +- **WHEN** the release key is supplied in the environment +- **THEN** the installer and proxy endpoints use it without any change to the + hardcoded configuration diff --git a/openspec/specs/tools-app/spec.md b/openspec/specs/tools-app/spec.md index 83f1d63..5dd389c 100644 --- a/openspec/specs/tools-app/spec.md +++ b/openspec/specs/tools-app/spec.md @@ -119,11 +119,14 @@ session; the OTP SHALL NOT be accepted without one. ### Requirement: Configuration is hardcoded The app SHALL hardcode all configuration in source: port `4173`, portal `https://portal.yrka.app`, PocketBase `https://pb.itcass.net`, a2loft -`https://apis.a2loft.net`, and agent `http://127.0.0.1:9000`. There SHALL be -no runtime configuration via environment variables or config files, with one -exception: the Image Cropper's service credentials (Minio keys, Azure storage -connection string, nodered credentials, cleanup-service key) SHALL come from -environment variables and SHALL NOT be stored in source. +`https://apis.a2loft.net`, agent `http://127.0.0.1:9000`, and the fingerprint +agent release forge `https://sources.yrka.app`. There SHALL be no runtime +configuration via environment variables or config files, with two exceptions: +the Image Cropper's service credentials (Minio keys, Azure storage connection +string, nodered credentials, cleanup-service key) SHALL come from environment +variables and SHALL NOT be stored in source; and the Fingerprint Agent +installer's release key (`GITEA_RELEASE_INSTALL_KEY`) SHALL come from the +environment and SHALL NOT be stored in source. #### Scenario: No configuration required to run - **WHEN** the app is started with no environment variables or config files @@ -134,6 +137,12 @@ environment variables and SHALL NOT be stored in source. - **THEN** the cropper routes use them without any change to the hardcoded URL/port configuration +#### Scenario: Installer key from the environment +- **WHEN** the fpagent installer release key is supplied as an environment + variable +- **THEN** the installer and release-proxy routes use it without any change to + the hardcoded URL/port configuration + ### Requirement: Carbon Design System UI The tools pages SHALL be built on the Carbon Design System: Carbon web components (version-pinned via CDN) for buttons, notifications, toasts, tags,