From ecb8552ee98137af78e9e7ca77548d3e405b3fd2 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 6 Oct 2026 13:50:39 +0200 Subject: [PATCH] vaultwarden 1.36.0 -> 1.37.4: its first ladder step, proven on the bench and on 9202 (R-890); instruction files kept true (decision 150) Written by upgrade-test.py --write-ladder from both verdicts. Bench 9401: harness v5, anon peak 16.3 %, seed read back. Box 9202: guarded Update done in 12.3 s, seed read back, seeded through the admin invite inside the box (decision 149). CLAUDE.md: stale gate counts, the CI/R-161 and catalog_since-gate sentences, the checklist count, steps/ in the layout, engine service counts, decoy gate path. Factual only. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- .claude/rules/unprompted-work.md | 13 +++++++- CHANGELOG.md | 7 +++-- CLAUDE.md | 38 +++++++++++------------- templates/vaultwarden/.felhom.yml | 8 ++++- templates/vaultwarden/docker-compose.yml | 2 +- 5 files changed, 43 insertions(+), 25 deletions(-) diff --git a/.claude/rules/unprompted-work.md b/.claude/rules/unprompted-work.md index d7a83c5..49df8ff 100644 --- a/.claude/rules/unprompted-work.md +++ b/.claude/rules/unprompted-work.md @@ -6,7 +6,8 @@ unconditional: true > Goal sessions, nightly sessions, "work the register" sessions. **A session that starts from > `/goal` or a standing brief inherits these rules exactly as it inherits the gates.** They are the > part of `PROMPT-TEMPLATE.md` that a task file used to carry and a goal does not. Same wording lives -> in all three repos' `.claude/rules/`; change it in all three or in none. +> in `felhom.eu`, `felhom-controller` and `app-catalog-felhom.eu` `.claude/rules/`, and in the workspace root's +> unversioned `.claude/rules/`; change all four or none. ## 1. What you may pick up on your own @@ -56,3 +57,13 @@ One screen, plain language, in this order: **decisions you took** (§2) first; w what broke and whether you fixed it; rows opened and closed with the register size before and after; what needs the operator, each with what happens if they do nothing. No file paths, no function names, no row numbers as the subject of a sentence. + +## 5. Instruction files + +**Instruction files (`CLAUDE.md`, `.claude/rules/*`) are kept true by the session that finds them wrong** +(operator ruling 2026-10-06, `09` §3 decision 150). A session MAY, without asking: correct a stale fact (a command, a +count, a version, a path, a description of what a gate does), add a fact it proved, and remove a reference to something +that no longer exists. Each edit is named in the report (file, line, before, after, why). A session MAY NOT, without the +operator's word: loosen a safety rule, a fence, a „never", a protected machine, a secret rule, or a review step; or +remove a rule. When in doubt, it is a rule change, and it goes to the operator. If Claude Code's own permission check +asks before such an edit, wait for the operator's click; if it refuses, record that and file the exact line. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d6f923..13dc2d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ -## 2026-10-06 (afternoon) — the test box may seed vaultwarden through its admin page (R-890) +## 2026-10-06 (afternoon) — vaultwarden's first update step (1.36.0 → 1.37.4), proven on both venues; the test box may seed it through its admin page (R-890) + +**What runs on a box changed:** vaultwarden's pin moves to `vaultwarden/server:1.37.4-alpine` with its first ladder entry, so a box offers the update and the night may take it. Written by `upgrade-test.py --write-ladder` from both verdicts: bench LXC 9401 (harness v5, 10-minute memory watch, anon peak 16.3 %, seed read back, no restart) and scratch 9202 (the product's guarded Update, done in 12.3 s, seed read back, badge „Naprakész" after). Evidence: `felhom.eu/documentation/audits/r890-instructions-2026-10-06/{bench,box/vaultwarden}`. + +Instruction files (`09` §3 decision 150, factual only): `CLAUDE.md` — the gates runner and `--fast` described as they are (fourteen gates; `--fast` skips image-resolvable and volume-persistence; scoping applies to every gate that accepts it), the CI/R-161 sentence (CI exists; R-161 closed), the stale „no gate for catalog_since yet" paragraph removed (the gate exists, R-452), the onboarding checklist count (61), `steps/.yml` in the template layout, the MariaDB list (five, grimmory added) and the PostgreSQL count (twelve), the decoy gate's path; `.claude/rules/unprompted-work.md` §5 and its copies line. -**What runs on a box changed:** nothing. Test tools only. - R-890 (`09` §3 decision 149): the box walk may now seed vaultwarden through its admin invite on a TEST box, as the bench does. A guard refuses unless all five hold: not the bench venue, FELHOM_BOX_ADMIN_SEED=1, the walk targets scratch guest 9202 on demo-hp (never 9201; the Tester 1 box is allowed by the ruling but the walk has no route to it), THIS run installed the app (box_walk now records its installs), and the box's own controller.yaml names the drill catalog. The invite runs INSIDE the box: the token is read from the container's environment into a shell variable and handed to curl on stdin, the admin cookie lives in a 0600 file that is shredded, and only HTTP codes come back — the token never leaves the box. Tests: BoxAdminSeedGuard (red-proved: a guard that always allows fails three tests). diff --git a/CLAUDE.md b/CLAUDE.md index 4b391dd..e9bd376 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,23 +5,24 @@ ## What this repo is -The Felhom **app catalog**: one directory per app under `templates//`, each holding exactly +The Felhom **app catalog**: one directory per app under `templates//`, each holding `docker-compose.yml` + `.felhom.yml` (deploy fields, resources, healthcheck probe, app_info — all customer-facing text in Hungarian). The felhom-controller git-syncs these to every customer box; `.felhom.yml` drives the deploy wizard. `templates.json` + `scripts/generate-customer.sh` are LEGACY -(Portainer-era) — new apps don't touch them. +(Portainer-era) — new apps don't touch them. An app with a superseded ladder step also holds +`steps/.yml` (the box pins those, see below). ## Deploy contract **Push to `main` = deploy.** The controller's sync picks changes up within 15 minutes (or trigger via -the dashboard "Sablonok frissítése" button / `POST /api/sync`). Only the two template files sync; +the dashboard "Sablonok frissítése" button / `POST /api/sync`). Only the template files sync; deployed `app.yaml` (customer secrets) is never overwritten. Full deploy details: the `felhom-build-deploy` skill. ## Conventions - **Adding a NEW app starts with `cp onboarding/_TEMPLATE.md onboarding/.md`** (operator request - 2026-10-01). `NEW-APP-CHECKLIST.md` is the list — 60 checks in 10 groups, each with how to test it and + 2026-10-01). `NEW-APP-CHECKLIST.md` is the list — 61 checks in 10 groups, each with how to test it and why it exists; the record answers every id `done` (with evidence that exists), `n/a` (with a reason) or `open`. The template and its complete record are published in ONE commit: `scripts/check-onboarding.py` (gate `onboarding`, in `--fast`, so the hook and CI) refuses a new template directory without one. The 53 @@ -38,8 +39,8 @@ deployed `app.yaml` (customer secrets) is never overwritten. Full deploy details - No secrets in any committed file; secrets are generated at deploy time via `deploy_fields` `generate:` specs. - **Run `python3 scripts/catalog_gates.py ` after ANY template change** — it is the ONE entry - point and runs all four gates below, exiting non-zero if any fails. Name the app(s) you touched - and it scopes the two gates that accept scoping, which is fast; with no names the runtime gate + point and runs every gate in its `GATES` table (that table is the list), exiting non-zero if any + fails. Name the app(s) you touched and it scopes every gate that accepts an app scope, which is fast; with no names the runtime gate deploys **every** template, so that form belongs **on a scratch host, never a customer box**. Exit: 0 all clean · 1 convicted · 2 UNDETERMINED, which is never a pass. **Why a runner and not four separate invocations** (operator ruling 2026-08-02, R-161): of this @@ -47,12 +48,11 @@ deployed `app.yaml` (customer secrets) is never overwritten. Full deploy details CLAUDE.md — `felhom.eu/scripts/site_gates.py` is run, and R-29's three orphans are named nowhere and have stopped nothing. Controller-side enforcement was rejected because a check at template load can only read the file, and a static audit of all 53 templates reports the catalog clean **including - papra** — it would pass on the exact defect it exists to catch. CI was rejected for now: neither - repo has any, and there are no users yet. **R-161 stays open at reduced scope** — this is - convention, run by a person; real automatic enforcement is owed when a second person touches - templates. **Update 2026-08-02:** `.githooks/pre-push` now runs `catalog_gates.py --fast` on every - push, which is gate 1 (`check-image-pins.py`) and, since 2026-09-13, the engine-major gate with the - push range — the other two need network and a container + papra** — it would pass on the exact defect it exists to catch. CI was rejected at the time (neither + repo had any); it now exists and re-runs `--fast` on every push (below). R-161 was closed + 2026-10-05, accepted by the operator. **Update 2026-08-02:** `.githooks/pre-push` now runs `catalog_gates.py --fast` on every + push, which is every gate except `image-resolvable` and `volume-persistence` (the range-reading ones, + engine-major among them, get the push range) — those two need network and a container runtime and take minutes per app, and a push that pulls images and starts containers gets bypassed within a week, after which the bypass is the habit. They stay deliberate periodic runs. The hook is per-clone (`git config core.hooksPath .githooks`) and `git push --no-verify` bypasses it, which is @@ -71,9 +71,7 @@ deployed `app.yaml` (customer secrets) is never overwritten. Full deploy details A stale `catalog_since` under-reports how long a box has been behind, which is the one number the badge exists to give. Absent, empty, malformed or FUTURE-dated all degrade to a badge with no age and one WARN in the controller log — never a broken template. - **There is no gate for this yet** and that is a known gap, filed as a register row: the gates - runner fetches at `--depth 1` and has no parent commit to diff an `image:` line against, so a drift - gate needs a deeper fetch. Backfilled for all 53 apps from git history on 2026-09-02. + Backfilled for all 53 apps from git history on 2026-09-02. - **A pinned tag can still rot away upstream** — the pin gate is syntactic and cannot see that. Second gate: `python3 scripts/check-image-resolvable.py` (exit 0 resolve / 1 GONE / 2 inconclusive), run at the start of every catalog campaign and before any publish train that vouches the catalog. @@ -108,11 +106,11 @@ deployed `app.yaml` (customer secrets) is never overwritten. Full deploy details shipped. The original rule — *no database-engine image crosses a major until the Update button takes a verified backup as its precondition* — named its own expiry, and that condition is met: Slice 4 shipped 2026-09-13 (controller v0.237.0/v0.238.0; any backup tier since v0.239.0). - **What is lifted.** The **four MariaDB** services (`bookstack-db`, `kimai-db`, `nextcloud-db`, - `romm-db`) may now cross a major. They have both halves they need: a verified backup in front of + **What is lifted.** The **MariaDB** services (`bookstack-db`, `grimmory-db`, `kimai-db`, + `nextcloud-db`, `romm-db` on 2026-10-06; the gate judges by a glob) may now cross a major. They have both halves they need: a verified backup in front of the Update, and `MARIADB_AUTO_UPGRADE=1` on every sidecar (R-459), whose conversion the harness has WATCHED run on the E3/E3b edges with the seeded data read back after. - **What is NOT lifted.** The **eleven PostgreSQL** services stay refused: the image performs no + **What is NOT lifted.** The **PostgreSQL** services (twelve on 2026-10-06, postgis and immich's postgres image counted) stay refused: the image performs no `pg_upgrade` and REFUSES to start on an older major's datadir (R-463). A backup is a route back, not a conversion — the app simply would not come up. MySQL is refused too, with nothing measured at all. **ONE APP AT A TIME, since 2026-09-25 (`09` §3 decision 35):** the BOX converts a PostgreSQL @@ -158,8 +156,8 @@ deployed `app.yaml` (customer secrets) is never overwritten. Full deploy details **A gate ships with a decoy test that has been seen to fail (R-421).** A decoy is the LABEL without the FACT — a directory with the right name and no bake log, a note whose prose mentions the marker it -lacks. `scripts/decoy_coverage_gate.py` refuses a new gate that has neither a decoy nor a named +lacks. `felhom.eu/scripts/decoy_coverage_gate.py` (run by felhom.eu's `repo_gates.py`, for all four repos) refuses a new gate that has neither a decoy nor a named exemption carrying its row. The four shapes, the 2026-09-01 sweep that fooled 16 of 29 gates, and the -decoys withdrawn as illegitimate: `documentation/audits/AUDIT-gate-decoys-2026-09-01.md` and +decoys withdrawn as illegitimate: `felhom.eu/documentation/audits/AUDIT-gate-decoys-2026-09-01.md` and `felhom-controller/.claude/rules/gates.md`. **Scope is a fact too** — prefer `os.walk` over `os.listdir`, and a glob over a hand-maintained list. diff --git a/templates/vaultwarden/.felhom.yml b/templates/vaultwarden/.felhom.yml index 7b0ce86..e79879a 100644 --- a/templates/vaultwarden/.felhom.yml +++ b/templates/vaultwarden/.felhom.yml @@ -15,7 +15,7 @@ subdomain: "vault" slug: "vaultwarden" # catalog_since: the date THIS repo last changed this app's pinned images. Any commit that # changes an image: line must set this to the same day (see CLAUDE.md). -catalog_since: "2026-07-18" +catalog_since: "2026-10-06" # --- Resource hints (displayed on deploy screen) --- resources: @@ -150,3 +150,9 @@ i18n: label: 'No - by invitation only (recommended)' - value: 'true' label: 'Yes - anybody who knows the address can sign up' + +# update_ladder — the test record: one tested step per line, oldest first (JSON flow mappings, +# `09-update-architecture.md` §6.4 part 4). WRITTEN BY scripts/upgrade-test.py, never by hand; +# gated by scripts/check-test-record.py. An image: move without a proven entry here is refused. +update_ladder: + - {"from": {"vaultwarden": "vaultwarden/server:1.36.0-alpine"}, "to": {"vaultwarden": "vaultwarden/server:1.37.4-alpine"}, "digest": {"vaultwarden": "sha256:19ea2b669945d842dadc3d500f2d6a795866a003330f59a47b9634658bae98bb"}, "verdict": "proven", "tested_at": "2026-10-06T11:49:21Z", "harness_version": 5, "evidence": "felhom.eu/documentation/audits/r890-instructions-2026-10-06/bench", "box_evidence": "felhom.eu/documentation/audits/r890-instructions-2026-10-06/box/vaultwarden", "memory_peak_pct": 16.3, "marks": {"files_may_change": false, "needs_person": null, "memory_tight": false}, "memory_basis": "anon", "memory_cgroup_peak_pct": 22.3} diff --git a/templates/vaultwarden/docker-compose.yml b/templates/vaultwarden/docker-compose.yml index 1a58165..0e0b9da 100644 --- a/templates/vaultwarden/docker-compose.yml +++ b/templates/vaultwarden/docker-compose.yml @@ -19,7 +19,7 @@ services: vaultwarden: - image: vaultwarden/server:1.36.0-alpine + image: vaultwarden/server:1.37.4-alpine container_name: vaultwarden restart: unless-stopped environment: