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)
gates / gates (push) Successful in 8s

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-06 13:50:39 +02:00
parent 6b4877d556
commit ecb8552ee9
5 changed files with 43 additions and 25 deletions
+12 -1
View File
@@ -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.
+5 -2
View File
@@ -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/<StepKey>.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).
+18 -20
View File
@@ -5,23 +5,24 @@
## What this repo is
The Felhom **app catalog**: one directory per app under `templates/<app>/`, each holding exactly
The Felhom **app catalog**: one directory per app under `templates/<app>/`, 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/<StepKey>.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/<app>.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 <app>` 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.
+7 -1
View File
@@ -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}
+1 -1
View File
@@ -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: