Commit graph

8 commits

Author SHA1 Message Date
8c1fd1da2b fix: unbreak upgrade path + install-lock race
All checks were successful
Build ISO / build-iso (push) Successful in 17m28s
CI / lint (push) Successful in 27s
CI / test (push) Successful in 59s
CI / validate-json (push) Successful in 23s
CI / markdown-links (push) Successful in 15s
Release / release (push) Successful in 11m38s
Three interlocking issues that made 26.11/26.12 effectively
un-upgradable from pre-auth versions without manual pacman +
symlink surgery. Caught while SSH-testing the .196 VM which landed
on a rollback loop after every Update-now click.

1. auth.py imported werkzeug.security, but the target system runs
   core as bare system Python — neither flask nor werkzeug are
   pip-installed. Fresh 26.11+ boxes died on import. Replaced with
   a 50-line stdlib `furtka/passwd.py` using hashlib.pbkdf2_hmac
   for new hashes and parsing werkzeug's `scrypt:N:r:p$salt$hex`
   format for backward-read so existing users.json survives.

2. updater._health_check pinged /api/apps expecting 200. Post-
   auth, /api/apps returns 401 for unauth requests → HTTPError
   caught as URLError → retry loop → 30s timeout → rollback. Now
   any 2xx-4xx counts as "server alive"; only 5xx / connection
   errors fail. Server responding at all is proof it came back up.

3. _do_install released the fcntl lock between sync pre-validation
   and the systemd-run dispatch. A second POST could slip in,
   pass the lock check, return 202, and leave its install-bg child
   to die silently on the in-child lock. Now the API also reads
   install-state.json and refuses 409 on non-terminal stages —
   the state file is the reliable signal, the fcntl lock is
   defence in depth.

Test coverage:
- tests/test_passwd.py (new, 6 cases): roundtrip, salt uniqueness,
  format shape, werkzeug scrypt backward-compat against a real
  hash captured from the .196 box, malformed + non-string
  rejection.
- tests/test_updater.py: +3 cases for _health_check — 4xx=healthy,
  5xx=unhealthy, URLError retry loop.
- tests/test_api.py: +2 cases for install 409 on non-terminal
  state + 202 after terminal.

All 267 tests green, ruff check + format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:03:28 +02:00
3a8fad5185 feat(catalog): on-box apps catalog synced independently of core version
New `furtka catalog sync` pulls the latest daniel/furtka-apps release,
verifies its sha256, extracts under /var/lib/furtka/catalog/, and
atomically swaps into place — so apps can ship without cutting a new
Furtka core release. A daily timer (furtka-catalog-sync.timer, 10 min
post-boot + 24 h with ±6 h jitter) drives the sync; /apps gets a
manual "Sync apps catalog" button that kicks the same code path via a
detached systemd-run unit.

Layout of the new on-box tree:

  /var/lib/furtka/catalog/            synced catalog (survives self-updates)
    ├── VERSION
    └── apps/<name>/ ...
  /var/lib/furtka/catalog-state.json  sync stage + last version, UI-polled
  /run/furtka/catalog.lock            flock so timer + manual click can't race

Resolver precedence (furtka/sources.py): catalog wins over the bundled
seed (/opt/furtka/current/apps/, carried by the core release for offline
first-boot). Installed apps under /var/lib/furtka/apps/ are never auto-
swapped — user clicks Reinstall to move an existing install onto a
newer catalog version; settings merge-preserved via the existing
installer.install_from path.

New files:
- furtka/_release_common.py — shared Forgejo/tarball primitives lifted
  from furtka/updater.py. Both modules now import from here; updater's
  behaviour and public API unchanged.
- furtka/catalog.py — check_catalog(), sync_catalog() with staging +
  manifest validation + atomic rename. Refuses bad sha256 / broken
  manifests and leaves the live catalog intact on any failure path.
- furtka/sources.py — resolve_app_name() / list_available() abstraction
  used by installer.resolve_source and api._list_available.
- assets/systemd/furtka-catalog-sync.{service,timer} — oneshot service
  + daily timer. Timer auto-enables on self-update via a one-line
  addition to _link_new_units (fresh installs get enabled via the
  webinstaller's _FURTKA_UNITS list).

API + UI:
- /api/bundled renamed internally to _list_available; endpoint stays as
  a backcompat alias; /api/apps/available is the new canonical name.
  Each list entry carries a `source` field ("catalog" | "bundled").
- POST /api/catalog/sync/check + /apply + GET /api/catalog/status.
- /apps page grows a catalog-status row + Sync button; poll loop
  mirrors the Furtka self-update flow.

CLI: `furtka catalog sync [--check]` + `furtka catalog status` (both
support --json). Old `furtka app install` / `reconcile` / `update` /
`rollback` surfaces are unchanged.

Test gate: 194/170 baseline + 24 new tests covering catalog sync
(happy path, sha256 mismatch, invalid manifest, lock contention,
preserves-on-failure) + resolver precedence + api renames. ruff
check + format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 14:16:02 +02:00
54357aa2a3 style: ruff format — collapse two-line hostname file path + version loop
All checks were successful
Build ISO / build-iso (push) Successful in 21m29s
CI / lint (push) Successful in 37s
CI / test (push) Successful in 58s
CI / validate-json (push) Successful in 42s
CI / markdown-links (push) Successful in 23s
Format-only diff from `ruff format`. The 26.5-alpha push's CI run failed
on `ruff format --check`; these three files had two-line constructs that
fit on one line at ruff's default line length. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:41:58 +02:00
8fbe67ffb9 fix(https): restore TLS handshake — name hostname + correct PKI path
Some checks failed
Build ISO / build-iso (push) Waiting to run
CI / lint (push) Failing after 2m11s
CI / test (push) Successful in 2m8s
CI / validate-json (push) Successful in 55s
CI / markdown-links (push) Successful in 25s
Deploy site / deploy (push) Successful in 8s
Closes #10. Two linked bugs in 26.4-alpha's Phase 1 HTTPS made the
force-HTTPS toggle fatal: every SNI handshake on :443 died with
SSL_ERROR_INTERNAL_ERROR_ALERT, so the toggle redirected users from
working HTTP to broken HTTPS.

Root cause 1: bare `:443 { tls internal }` gives Caddy no hostname to
issue a leaf cert for, so /var/lib/caddy/certificates/ stayed empty and
Caddy sent TLS `internal_error` on every handshake. Fix: the :443 block
is now `__FURTKA_HOSTNAME__.local, __FURTKA_HOSTNAME__ { tls internal }`,
with the marker substituted by webinstaller/app.py at install time and
by furtka.updater._refresh_caddyfile on self-update (reads /etc/hostname,
falls back to "furtka"). `auto_https disable_redirects` keeps Caddy's
built-in redirect out of the way of the /settings toggle.

Root cause 2: furtka/https.py and the /rootCA.crt handler both referenced
/var/lib/caddy/.local/share/caddy/pki/authorities/local/ — a path that
doesn't exist. caddy.service sets XDG_DATA_HOME=/var/lib, so Caddy's
storage is /var/lib/caddy/ directly. Fix: both paths corrected.

Verified on the 192.168.178.110 smoke VM by swapping the Caddyfile in,
reloading, handshaking, restoring: TLS 1.3 handshake succeeds, leaf cert
issued under /var/lib/caddy/certificates/local/, /rootCA.crt returns 200.

Tests: new cases assert the Caddyfile ships the hostname placeholder,
the webinstaller substitutes it, _refresh_caddyfile re-substitutes from
/etc/hostname on update, and the asset sets auto_https disable_redirects.
Unit tests still stub the Caddy reload — the real handshake regression
needs a smoke-VM integration test (follow-up, separate from this fix).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:39:48 +02:00
b96f225c3c fix(updater): /releases?limit=1 instead of /releases/latest
Some checks failed
Build ISO / build-iso (push) Successful in 17m5s
CI / lint (push) Successful in 25s
CI / test (push) Successful in 33s
CI / validate-json (push) Successful in 23s
CI / markdown-links (push) Successful in 12s
Release / release (push) Has been cancelled
Forgejo's /releases/latest silently skips pre-releases (any release
with a -alpha / -beta / -rc suffix) and 404s when there's no stable
release. During Furtka's alpha stage every tag is a pre-release, so
the Check-for-updates button always 404'd against a perfectly-valid
releases page.

Switch check_update() to GET /releases?limit=1 and take the first
entry. Forgejo returns releases newest-first regardless of kind, so
this works whether the top of the list is pre-release or stable.
Empty list (no releases published yet) now returns a clean
"no releases" UpdateError instead of a raw 404.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 16:29:11 +02:00
19e72cf5c3 fix(furtka): chmod 755 on version dir + heredoc furtka.json
Some checks failed
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / validate-json (push) Waiting to run
CI / markdown-links (push) Waiting to run
Build ISO / build-iso (push) Has been cancelled
Two install-path bugs surfaced by SSHing into the hot-fixed test VM:

1. mktemp creates the staging dir with mode 700 by default; the rename
   to /opt/furtka/versions/<ver>/ preserved it, and Caddy (running as
   the unprivileged `caddy` user) got 403 Forbidden because it couldn't
   traverse the version dir. Now the install + self-update both
   `chmod 755` after the rename.

2. _furtka_json_cmd was a silent no-op on the 43a39a4 VM — the
   base64-encoded body + sed substitution approach layered two sets of
   quotes through archinstall's custom_commands eval, and the sed
   step either never ran or didn't match. Replaced with a plain
   heredoc that interpolates $(date -Iseconds) and $(cat VERSION) at
   chroot runtime. Result lands /var/lib/furtka/furtka.json reliably,
   which is what the landing page's hostname chip and the settings
   page's install-date field depend on.

Both issues confirmed fixed by applying them manually on the VM
(chmod 755 /opt/furtka/versions/26.0-alpha + writing furtka.json by
hand): landing page, /apps, /settings, /furtka.json all now 200 with
correct content.

Tests updated (the chmod 755 gets asserted; the old base64+sed test
gets replaced with a heredoc-shape check; the updater test asserts
0o755 mode on the finished version dir).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:46:56 +02:00
b8fdb62b41 fix(furtka): pre-ISO audit fixes — chmod, Caddyfile refresh, unit linking
Five issues surfaced by the Phase-2 audit before the next ISO rebuild:

P1 (real blockers for a fresh install / self-update):

1. chmod +x furtka/assets/bin/furtka-status, furtka-welcome. They were
   mode 644 in git, so the tarball shipped them non-executable and every
   ExecStart referencing /opt/furtka/current/assets/bin/furtka-* would
   have failed on first boot with Permission denied.

2. apply_update now refreshes /etc/caddy/Caddyfile from the new version
   when the content differs, then reloads caddy. Without this, a release
   that changes Caddy routes silently stays on the old config.

3. apply_update now systemctl-links any new unit files shipped by the
   update, not just the five linked at install time. A future release
   that adds furtka-foo.service would otherwise never appear in
   /etc/systemd/system/.

P2 (hardening, not blockers today):

4. _resource_manager_commands now aborts the install if the tarball's
   VERSION file is empty — otherwise `mv "$staging" /opt/furtka/versions/`
   would move the staging dir in as a subdirectory and the symlink
   target would be invalid.

5. _extract_tarball passes filter='data' to tarfile.extractall on
   Python 3.12+ to catch symlink-escape / setuid / device-node tricks
   that the regex path-check can't see. Falls back silently on older
   interpreters.

Plus the CHANGELOG [Unreleased] section got filled in with the whole
Phase-1 + Phase-2 + UI-uplevel body so a 26.1-alpha tag cut off main
has meaningful release notes.

Test additions / updates:
- test_refresh_caddyfile_{copies_when_different,noops_if_source_missing}
- test_link_new_units_only_links_missing
- test_extract_tarball_uses_data_filter_when_available
- test_apply_update_happy_path now verifies the Caddyfile gets copied.
- test_resource_manager_extracts_to_versioned_slot verifies the
  empty-VERSION guard is present in the install command.

Paths now overridable via FURTKA_CADDYFILE_PATH + FURTKA_SYSTEMD_DIR so
tests can pin a tmpdir for these new fs operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:10:07 +02:00
f0acc4427e feat(furtka): release CI + \furtka update\ / \furtka rollback\ CLI
Slice 2 of the self-update story. Tagging a release on main now
produces a downloadable self-update payload on the Forgejo releases
page, and a running box can pull it down, verify it, atomically swap
to the new version, and health-check the result.

New pieces:

- scripts/build-release-tarball.sh <version> — packages the furtka/
  package + bundled apps/ + a root-level VERSION file as
  dist/furtka-<version>.tar.gz, plus a .sha256 sidecar and a
  release.json metadata blob.
- scripts/publish-release.sh <version> — uses the Forgejo v1 API to
  create a release (body pulled from the CHANGELOG section for this
  tag, pre-release auto-flagged on -alpha/-beta/-rc) and upload the
  three assets sequentially. Needs \$FORGEJO_TOKEN.
- .forgejo/workflows/release.yml — tag-triggered, runs both scripts
  with the new \$FORGEJO_RELEASE_TOKEN repo secret.
- furtka/updater.py — check_update, prepare_update, apply_update,
  run_update, rollback. Atomic symlink swap, sha256 verify (TOCTOU-
  safe: re-hashes on-disk file), health-check post-restart with
  auto-rollback on failure, stage-by-stage progress persisted to
  /var/lib/furtka/update-state.json so the UI can poll independent
  of the (restarting) API process. Path overrides via FURTKA_ROOT /
  FURTKA_STATE_DIR / FURTKA_LOCK_PATH so tests pin a tmpdir.
- furtka/cli.py — \`furtka update [--check] [--json]\` and
  \`furtka rollback\`.
- tests/test_updater.py — 15 tests: version compare, sha256 verify,
  tarball extract (including traversal refusal), lockfile, apply
  happy + rollback paths, rollback CLI, check_update with stubbed
  Forgejo.
- iso/build.sh — writes VERSION at the tarball root so the install
  path matches the self-update path (previously assumed only the
  release script did this).

RELEASING.md now points at the automated flow — no more manually
clicking "Create release" on the Forgejo UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 13:30:45 +02:00