#437: revoked is a watch, not a verdict — transient refusals self-heal, and the drop explains itself #439

Merged
Cordy merged 12 commits from license-drop-437 into main 2026-09-06 02:46:38 +00:00
Owner

Closes #437.

The August incident, closed end to end:

Backend (TDD, red witnessed on the runner): a 403 revoked/unknown still drops the licence to the free tier immediately, but now starts a 14-day revoked watch — daily check-ins continue and the first 200 restores the licence with no admin action. Past the watch the instance goes quiet for good. Legacy revoked state re-enters the watch on load. The state file records a capped drop/restore transition history with reasons (refused/grace/expired) — the forensic trail the incident lacked. Status API adds inactiveReason/inactiveSince/retryUntil/licSeats. TestForbiddenRevokedStillStops asserted the incident-causing behaviour and became TestForbiddenRevokedEntersWatch.

Observability: /metrics gains cairn_license_installed / cairn_license_licensed / cairn_license_seat_cap (absent when unwired) — installed=1 with licensed=0 is the silent free-tier fallback and becomes alertable (homelab-config rule follows separately).

Frontend (per the approved license-inactive-states mockup): one state-driven dropcard on the Licence page for the three inactive states, shared calm line, Check in now action; the seat meter dims and names the suspended cap; the bell renders license-dropped/license-restored transitions (admin-only, click lands on Licensing); 20 i18n keys ×4 locales.

Docs: CHANGELOG v0.6.133; licensing-faq and ARCHITECTURE revoked wording updated to the watch semantics.

Closes #437. The August incident, closed end to end: **Backend (TDD, red witnessed on the runner):** a 403 revoked/unknown still drops the licence to the free tier immediately, but now starts a 14-day revoked watch — daily check-ins continue and the first 200 restores the licence with no admin action. Past the watch the instance goes quiet for good. Legacy revoked state re-enters the watch on load. The state file records a capped drop/restore transition history with reasons (refused/grace/expired) — the forensic trail the incident lacked. Status API adds `inactiveReason`/`inactiveSince`/`retryUntil`/`licSeats`. `TestForbiddenRevokedStillStops` asserted the incident-causing behaviour and became `TestForbiddenRevokedEntersWatch`. **Observability:** `/metrics` gains `cairn_license_installed` / `cairn_license_licensed` / `cairn_license_seat_cap` (absent when unwired) — installed=1 with licensed=0 is the silent free-tier fallback and becomes alertable (homelab-config rule follows separately). **Frontend (per the approved license-inactive-states mockup):** one state-driven dropcard on the Licence page for the three inactive states, shared calm line, Check in now action; the seat meter dims and names the suspended cap; the bell renders license-dropped/license-restored transitions (admin-only, click lands on Licensing); 20 i18n keys ×4 locales. **Docs:** CHANGELOG v0.6.133; licensing-faq and ARCHITECTURE revoked wording updated to the watch semantics.
The 2026-08 incident: one 403 (reason "unknown", during a licence-server
redeploy) set Revoked=true, which made checkinRequiredLocked() false, so
the instance never asked again — a permanent free-tier downgrade healed
only by an admin re-pasting the key. This test witnesses the bug: after a
refused check-in, the next daily retry must still reach the server, and a
healthy 200 must restore the licence.
Compile-red against main: Transitions, RevokedSince, InactiveReason,
RetryUntil and LicSeats do not exist yet. The licence state records its
own drop/restore history (the forensic trail the 2026-08 incident
lacked), Status names WHY an installed key is not counting, and legacy
revoked state re-enters the watch on load instead of staying silent
forever.
Compile-red against main: SetLicenseProvider does not exist yet. The
silent free-tier fallback of the 2026-08 incident becomes an alertable
signal: installed=1 with licensed=0 pages instead of waiting a month to
be noticed on the licence page.
The old test encoded the terminal-revoked behaviour that CAUSED the
2026-08 incident: it asserted that one 403-revoked stops check-ins
forever. Under #437 the refusal still drops the licence immediately, but
check-ins continue daily inside the 14-day revoked watch and stop only
past it - which is what the test now asserts.
checkinRequiredLocked keeps daily check-ins alive for revokedWatchWindow (14d) after a 403 revoked/unknown; past it the instance goes quiet for good. State gains RevokedSince plus a capped Transitions history (dropped/restored with reason) - the forensic trail the 2026-08 incident lacked - and legacy revoked state re-enters the watch on load. Status names the inactive state (inactiveReason/inactiveSince/retryUntil/licSeats). The bell derives admin-only license-dropped/license-restored items from Transitions. /metrics gains cairn_license_installed/licensed/seat_cap gauges, absent when unwired.

TDD: TestTransientRefusalSelfHeals witnessed red (server hits stayed 1); transitions_test.go and license_gauges_test.go witnessed compile-red; all green after splice.
#437 one-shot: frontend dropcard + dimmed meter + bell items + i18n ×4
Some checks failed
fe-437.yml / #437 one-shot: frontend dropcard + dimmed meter + bell items + i18n ×4 (push) Failing after 0s
fe4bca9ecc
One state-driven card on the Licence page (refused at check-in / server unreachable / expired, per the approved license-inactive-states mockup) with the shared calm line and a Check in now action; the seat meter dims and names the suspended cap; the bell renders license-dropped/license-restored transitions (admin-only, click lands on Licensing); 20 new i18n keys in all four locales.
#437 docs: CHANGELOG v0.6.133; revoked-watch wording in FAQ and ARCHITECTURE
All checks were successful
ci / test-and-build (pull_request) Successful in 42s
61b4575f84
Cordy merged commit 109aae8898 into main 2026-09-06 02:46:38 +00:00
Cordy deleted branch license-drop-437 2026-09-06 02:46:42 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#439
No description provided.