docs(openspec): archive fpagent-release-installer and sync specs

This commit is contained in:
2026-10-02 09:53:35 +01:00
parent 46027acec1
commit e3392c7750
8 changed files with 479 additions and 5 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-02
@@ -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-<version>-linux-x86_64.tar.gz`), per-Ubuntu offline bundles
(`fpagent-<version>-ubuntu-<codename>-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.
@@ -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-<version>-{linux-x86_64|ubuntu-<codename>-x86_64}.tar.gz`) plus the
matching `.sha256` assets.
@@ -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
@@ -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
@@ -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.
+136
View File
@@ -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
+14 -5
View File
@@ -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,