diff --git a/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/.openspec.yaml b/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/.openspec.yaml new file mode 100644 index 0000000..ad94bad --- /dev/null +++ b/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-02 diff --git a/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/design.md b/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/design.md new file mode 100644 index 0000000..0a5baac --- /dev/null +++ b/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/design.md @@ -0,0 +1,64 @@ +# Design + +## Context + +See proposal.md — Why. The `fpagent-installer` capability (change +`fpagent-release-installer`) installs the newest release and hands off to the +bundle's `deploy/install.sh`, which replaces the binary but only warns when a +daemon is running. The bundle name already embeds the git hash +(`fpagent--.tar.gz`) and `bundle.sh` already builds the binary +with that hash as its embedded `Version`. + +## Goals / Non-Goals + +**Goals:** install a pinned release; make a rerun a real upgrade that reports +what changed and takes effect; keep both install paths (hosted and direct +bundle) consistent. + +**Non-Goals:** a separate version service or a new `--version` CLI flag; signing +or provenance beyond sha256; a downgrade-unsafe rollback. + +## Decisions + +1. **The version is the asset-name git hash.** It is already the binary's + embedded `Version` and needs no new endpoint or flag. The hosted installer + parses it, and also writes it as `VERSION` in the extracted bundle so the + bundle installer has it even for bundles built before this change. + +2. **The bundle installer owns the upgrade reporting and the restart.** It + knows `INSTALL_PATH` and the service; the hosted installer only fetches, + verifies and hands off. `bundle.sh` ships `VERSION` so direct bundle installs + behave the same. The stamp lives at `${INSTALL_PATH}.version`, so a custom + `--install-path` keeps its own record, and uninstall removes it. + +3. **Restart only on a managed, already-running service.** `enable --now` starts + a stopped unit; for an upgrade the installer checks `is-active` before + enabling and restarts only if it was active, avoiding a redundant restart on + a fresh install. + +4. **`--tag` reuses the release handler.** A second same-origin metadata route + (`/fpagent/releases/tags/:tag`) shares the latest route's handler, so the + filtering and non-disclosure rules are identical; the installer switches only + the metadata URL, then downloads by the resolved tag. + +## Risks / Trade-offs + +- [Bundles built before this change carry no VERSION and do not restart] → the + hosted installer writes VERSION from the asset name (fixes reporting), but the + restart is in the bundle and appears only after a new `fingerprint-agent` + release. Until then upgrades still need a manual restart. +- [Upgrade without `--enable-service` leaves a running daemon on the old binary] + → the installer warns; the documented install command uses `--enable-service`. +- [A pinned tag whose assets differ] → the tag's own metadata drives asset + selection, the same as latest. + +## Migration Plan + +Release a new `fingerprint-agent` bundle (ships VERSION and restarts on +upgrade), then deploy the app. The hosted installer is backward compatible: with +an older bundle it reports the version but the bundle's older installer ignores +the stamp. Rollback = revert the app; `--tag` can pin the previous release. + +## Open Questions + +None. diff --git a/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/proposal.md b/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/proposal.md new file mode 100644 index 0000000..b09f3e1 --- /dev/null +++ b/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/proposal.md @@ -0,0 +1,42 @@ +# Proposal: fpagent-install-upgrade + +## Why + +The installer always took the newest release and left any running `fpagent` +daemon on the old binary, so there was no way to hold a known-good version and +an "upgrade" did not take effect until a reboot. Operators need a controlled +version and a rerun that actually upgrades. + +## What Changes + +- The installer accepts `--tag TAG` to install a specific release instead of + `latest`, and the app proxies that tag's metadata through a same-origin route. +- The installed version is the git hash carried in the bundle's asset name. The + installer passes it to the bundle installer, which records it beside the + binary and reports `upgrading -> ` on the next run. +- When the installer manages the systemd service and the service was already + running, an upgrade restarts it so the new binary takes effect. The version + stamp is removed on uninstall. + +## Capabilities + +### New Capabilities + +(none) + +### Modified Capabilities + +- `fpagent-installer`: adds release pinning, a pinned-tag metadata endpoint, + installed-version tracking with upgrade reporting, and restart of a managed + service on upgrade. + +## Impact + +- `tools/server.js`: `/fpagent/releases/tags/:tag` route (shares the release + metadata handler with `/fpagent/releases/latest`). +- `tools/public/fpagent/install.sh`: `--tag`, version parse from the asset name, + and handoff of the version to the bundle installer. +- `fingerprint-agent` `deploy/install.sh` and `deploy/bundle.sh`: read/stamp the + version and restart an already-running service; `bundle.sh` ships `VERSION`. +- Depends on the `fingerprint-agent` bundle naming, which already embeds the + git hash (`fpagent--.tar.gz`). diff --git a/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/specs/fpagent-installer/spec.md b/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/specs/fpagent-installer/spec.md new file mode 100644 index 0000000..538ad20 --- /dev/null +++ b/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/specs/fpagent-installer/spec.md @@ -0,0 +1,92 @@ +# Spec Delta + +## ADDED Requirements + +### Requirement: Pinned release selection + +The installer SHALL accept a release tag and install that release instead of the +latest. Without a tag it SHALL install the latest release. + +#### Scenario: A pinned release is installed + +- **WHEN** the installer is invoked with a release tag +- **THEN** it fetches that tag's metadata and installs its bundle + +#### Scenario: Default is the latest release + +- **WHEN** the installer is invoked without a release tag +- **THEN** it installs the latest release + +#### Scenario: Unknown pinned release + +- **WHEN** the pinned tag does not exist +- **THEN** the installer exits with a failure message and installs nothing + +### Requirement: Pinned release metadata endpoint + +The app SHALL expose a same-origin endpoint that returns a specific release's +tag and asset names as same-origin URLs, with the same authentication, filtering +and non-disclosure guarantees as the latest-release endpoint. It SHALL reject a +tag that is not a valid release tag without contacting the forge. + +#### Scenario: Tagged release is returned + +- **WHEN** a client requests the metadata for an existing tag +- **THEN** the response carries that release's tag and its assets as same-origin + URLs + +#### Scenario: Invalid tag is rejected + +- **WHEN** a client requests metadata for a tag that is not a valid release tag +- **THEN** the app rejects the request without contacting the forge + +### Requirement: Installed version is tracked and upgrades are reported + +The installer SHALL treat the git hash in the bundle's asset name as the version. +It SHALL hand that version to the bundle installer, which SHALL record it beside +the installed binary and SHALL report an upgrade with the previous and new +versions on the next run. Uninstalling SHALL remove the recorded version. + +#### Scenario: Version is derived from the asset name + +- **WHEN** the installer selects a bundle named `fpagent--.tar.gz` +- **THEN** it uses `` as the version + +#### Scenario: Upgrade is reported + +- **WHEN** the recorded version differs from the version being installed +- **THEN** the bundle installer reports the change from the old to the new + version + +#### Scenario: First install records the version + +- **WHEN** no version has been recorded yet +- **THEN** the bundle installer records the new version and reports no upgrade + +#### Scenario: Uninstall removes the recorded version + +- **WHEN** the installer is asked to uninstall +- **THEN** the recorded version is removed + +### Requirement: A managed running service is restarted on upgrade + +When the installer is asked to manage the systemd service and a service is +already running, the installer SHALL restart it after installing the new binary +so the new binary takes effect. A fresh install SHALL NOT perform a redundant +restart, and the installer SHALL NOT restart the service when service management +is not requested. + +#### Scenario: Upgrade restarts the running service + +- **WHEN** the running service is managed and a new binary is installed +- **THEN** the service is restarted on the new binary + +#### Scenario: Fresh install starts the service + +- **WHEN** the service is not running and service management is requested +- **THEN** the service is started and no separate restart is performed + +#### Scenario: Service management not requested + +- **WHEN** service management is not requested +- **THEN** the installer does not restart the service diff --git a/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/tasks.md b/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/tasks.md new file mode 100644 index 0000000..9e89940 --- /dev/null +++ b/openspec/changes/archive/2026-10-02-fpagent-install-upgrade/tasks.md @@ -0,0 +1,19 @@ +# Tasks + +- [x] 1. App: add `/fpagent/releases/tags/:tag`, sharing the release-metadata + handler with `/fpagent/releases/latest`; reject invalid tags without contacting + the forge. +- [x] 2. Hosted installer: parse `--tag`, fetch the pinned tag's metadata, + derive the git hash from the asset name, and write it as `VERSION` into the + extracted bundle before handoff. +- [x] 3. Bundle installer (`fingerprint-agent`): read `VERSION` beside the script + or the binary, report `upgrading -> ` from `${INSTALL_PATH}.version`, + stamp the new version, remove it on uninstall. +- [x] 4. Bundle installer: restart an already-running `fpagent.service` after + `enable --now` (via `is-active`, so a fresh install does not restart). +- [x] 5. `bundle.sh`: ship `VERSION` in the bundle. +- [x] 6. Tests: tag-route metadata and invalid-tag rejection; `--tag` makes the + installer request the tag endpoint, not latest. Verified 59 tests pass. +- [x] 7. Verified end to end: a real patched bundle served through a fake gateway + → hosted installer → bundle installer reported `upgrading fpagent deadbeef -> + cafebabe`, version `cafebabe`, with options forwarded. diff --git a/openspec/specs/fpagent-installer/spec.md b/openspec/specs/fpagent-installer/spec.md index afe0cb5..c2e2969 100644 --- a/openspec/specs/fpagent-installer/spec.md +++ b/openspec/specs/fpagent-installer/spec.md @@ -134,3 +134,92 @@ asset. - **WHEN** the release key is supplied in the environment - **THEN** the installer and proxy endpoints use it without any change to the hardcoded configuration + +### Requirement: Pinned release selection + +The installer SHALL accept a release tag and install that release instead of the +latest. Without a tag it SHALL install the latest release. + +#### Scenario: A pinned release is installed + +- **WHEN** the installer is invoked with a release tag +- **THEN** it fetches that tag's metadata and installs its bundle + +#### Scenario: Default is the latest release + +- **WHEN** the installer is invoked without a release tag +- **THEN** it installs the latest release + +#### Scenario: Unknown pinned release + +- **WHEN** the pinned tag does not exist +- **THEN** the installer exits with a failure message and installs nothing + +### Requirement: Pinned release metadata endpoint + +The app SHALL expose a same-origin endpoint that returns a specific release's +tag and asset names as same-origin URLs, with the same authentication, filtering +and non-disclosure guarantees as the latest-release endpoint. It SHALL reject a +tag that is not a valid release tag without contacting the forge. + +#### Scenario: Tagged release is returned + +- **WHEN** a client requests the metadata for an existing tag +- **THEN** the response carries that release's tag and its assets as same-origin + URLs + +#### Scenario: Invalid tag is rejected + +- **WHEN** a client requests metadata for a tag that is not a valid release tag +- **THEN** the app rejects the request without contacting the forge + +### Requirement: Installed version is tracked and upgrades are reported + +The installer SHALL treat the git hash in the bundle's asset name as the version. +It SHALL hand that version to the bundle installer, which SHALL record it beside +the installed binary and SHALL report an upgrade with the previous and new +versions on the next run. Uninstalling SHALL remove the recorded version. + +#### Scenario: Version is derived from the asset name + +- **WHEN** the installer selects a bundle named `fpagent--.tar.gz` +- **THEN** it uses `` as the version + +#### Scenario: Upgrade is reported + +- **WHEN** the recorded version differs from the version being installed +- **THEN** the bundle installer reports the change from the old to the new + version + +#### Scenario: First install records the version + +- **WHEN** no version has been recorded yet +- **THEN** the bundle installer records the new version and reports no upgrade + +#### Scenario: Uninstall removes the recorded version + +- **WHEN** the installer is asked to uninstall +- **THEN** the recorded version is removed + +### Requirement: A managed running service is restarted on upgrade + +When the installer is asked to manage the systemd service and a service is +already running, the installer SHALL restart it after installing the new binary +so the new binary takes effect. A fresh install SHALL NOT perform a redundant +restart, and the installer SHALL NOT restart the service when service management +is not requested. + +#### Scenario: Upgrade restarts the running service + +- **WHEN** the running service is managed and a new binary is installed +- **THEN** the service is restarted on the new binary + +#### Scenario: Fresh install starts the service + +- **WHEN** the service is not running and service management is requested +- **THEN** the service is started and no separate restart is performed + +#### Scenario: Service management not requested + +- **WHEN** service management is not requested +- **THEN** the installer does not restart the service