`09` §3 decision 146. vaultwarden's fixture tries the household's own
/identity/accounts/register first (400 while sign-up is closed, R-512); on
the BENCH ONLY it then signs in to /admin with the ADMIN_TOKEN the bench
generated for this run, invites the drill address and registers it — the
route measured on 9202 2026-09-15 (E1-vaultwarden-spike). The token goes to
curl on stdin, the admin cookie in a 0600 header file that is shredded.
The dead /api/accounts/register (404 on 1.36) is gone.
bench_admin_seed_allowed(): the venue is the bench's (upgrade_boxport.Venue
VENUE="bench"), FELHOM_BENCH_ADMIN_SEED=1, and /opt/docker/stacks does not
exist (every Felhom box has it). Any one missing refuses; the edge stays
inconclusive with what was tried.
upgrade-test.py: the run's .env is written 0600 and shredded after the
teardown; every printed line and every evidence file is redacted of the
generated deploy secrets and the fixture's own password/key.
zipline needs no held secret: its first-run /api/setup already makes the
SUPERADMIN with a per-run password (measured 2026-09-30), now redacted too.
Tests: BenchAdminSeedGuard, SecretHygiene (red-proved).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
`09` §3 decision 145. MARKER_IGNORE in upgrade-test.py: per app, the files the
files_may_change mark does not count — first immich's six 13-byte
{encoded-video,library,backups,profile,thumbs,upload}/.immich folder markers,
rewritten at every start (bench measurement 2026-09-30). A listed file is
ignored only when changed/added and still <= 64 bytes; a removed or grown
marker, any unlisted file, and a moved tree the file walk cannot name still
mark the step. The verdict records files_ignored with the reasons.
HARNESS_VERSION 5. Tests: test_upgrade_bench.py MarkerIgnore (red-proved).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
mealie (09 §3 decision 144): five wrong logins lock the account for 1-2 hours,
even for the right password — wait, then sign in again.
Karakeep (decision 148): the official phone app sends crash reports to its
makers (Sentry).
Both are a new last first_steps entry (app_info has no notes field); the
Hungarian freeze admits them with the reason (check-copy-i18n --add-app).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
test_gate_decoys.py runs test_check_volume_persistence.py (the blind and
crying-wolf probers refused rc=3, `wrote nothing` never CLEAN, the papra
signature convicted) and requires it green, so COVERS is a fact; and runs
the working-tree gate in a scratch catalog with PATH = ONLY a stub docker
that fails every call: a runtime that answers nothing, no docker, nothing
to judge are each HARNESS REFUSED rc=3, never 0. An always-succeeding stub
is deliberately not used - it would walk the prober into the host
filesystem. COVERS gains "volume-persistence".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
The gate's unit tests inject `resolver`, so docker_resolver - where both
live traps sit - never ran under test. test_gate_decoys.py now copies the
working-tree gate into a scratch catalog and runs it with PATH = ONLY a
stub docker (the real runtime acts on DooPlex and cannot be reached; no
network). 9 cases: a `manifest unknown` pin is convicted naming the app;
rc=0 carrying a throttle or any error text, a docker that resolves the
.invalid canary too, no docker, nothing to judge are INCONCLUSIVE or
HARNESS REFUSED, never 0; a throttle or unrecognised error on rc=1 is
never an accusation. COVERS gains "image-resolvable".
check-image-resolvable.py: the "same shape as check-image-pins.py"
comment was made false by the image-pins fix; it now says why the
narrower regex is safe.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
check-image-pins.py now refuses a QUOTED `"image":` key (was not read at
all), an interpolated `${APP_IMAGE:-nginx}` ref (the tag cannot be read),
and `@sha256:` with no 64-hex digest behind it (the label of a pin). It
takes --root=<dir> (the decoy seam) and accepts the runner's --all.
test_gate_decoys.py: 17 image-pins cases — nine facts that must be
refused (untagged, a registry port read as a tag, quoted/capital :latest,
:edge, a comment claiming a pin, the quoted key, interpolation, a fake
digest), seven inert/genuine shapes that must pass, and the real catalog.
COVERS gains "image-pins".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
The case added privatebin's English and expected the gate to report coverage ABOVE a ceiling of 0;
since the catalog reached full coverage nothing was missing and the gate rightly said OK, so the
suite was red on its own premise. Red-proof: with check 5 disabled the case fails.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Location history with PostGIS 17 + Redis + Sidekiq. The seeded known login replaced by after_install behind the install
hold; geocoding off; SECRET_KEY_BASE a data_key; smtp_mapping with mail-off boot measured. onboarding/dawarich.md complete.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Bookmarks/articles/notes with a crawler (karakeep-chrome) and search (meilisearch v1.41.0). AI off unless the household
enters a key; setup gate + sign-up closed twice; Chrome healthcheck over bash /dev/tcp; smtp_mapping (plaintext), mail-off
boot measured; web memory 768M -> 1536M on measurements (bench watch, box crawl burst). onboarding/karakeep.md complete.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Calendar and contacts (CalDAV/CardDAV), ghcr.io/kozea/radicale:3.8.1. Login file written from the generated password on
the first start only (R-765: rewriting it at every start lost the household's login on a restore). onboarding/radicale.md
complete; bench + 9202 proven; FIRST-ADMIN, README, Hungarian freeze.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
NEW-APP-CHECKLIST.md: the reviewer's draft reviewed - 60 rows in 10 groups, each with how/why and a since date;
7 rows added, 16 sharpened, 9 wrong claims fixed. onboarding/_TEMPLATE.md (one line per id), onboarding/wger.md
(the pilot, exempt app, 11 open rows each a register row), onboarding/EXISTING-APPS-GAPS.md (read only, from
scripts/onboarding_gaps.py). Gate onboarding (scripts/check-onboarding.py) in --fast: a template directory not
among the 53 published before 2026-10-01 needs a complete record; decoys in test_gate_decoys.py (16 cases, 5 gate
mutants seen red). CLAUDE.md, REUSE.md 5, README point to it. No template changed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
after_install renames admin to the generated ADMIN_USER in app.db, sets the password with cps.py -s, and proves both
before its success line. Proven on 9202: 40 wrong tries on admin, the household still in at once (form and OPDS).
Hungarian freeze re-captured for the five changed calibre-web strings only.
Evidence: felhom.eu/documentation/audits/calibre-name-and-prune-2026-10-01/A/
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Outline 1.10 names the CSRF cookie __Host-csrfToken on a secure request and csrfToken over plain HTTP (server/utils/
csrf.ts getCookieName); 1.9.1 always said csrfToken. Proven on 9202 at the live pin 1.10.1: installation.create 302,
cookie __Host-csrfToken, apiKeys.create, a published document read back, unknown id and wrong key refused
(felhom.eu/documentation/audits/rulings-2026-10-01/D/D2-outline-fixture-9202.txt). The old pattern cannot match that
header line (the red: 2026-09-30, both venues).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
The digest was dropped and the tag's current digest returned, so 'is this digest still served' got a false yes
(measured: redis:7-alpine@sha256:000…0 resolved to the tag's digest; now HTTP 404). A malformed digest is refused
without a request. test_image_digest.py (no network); red-proof: the pre-fix resolver fails 3 of 4 cases.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
The check asked for /opt/upg/upgrade-test.py before sync_bench() copies it, so a bench freshly created by the
runbook was refused in one minute (2026-10-01). After the sync, the file is now required.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
- A RE-TEST entry: from == to, digest = the registry's new digest, digest_from = the tested one, box_evidence.
ladder.check_entry refuses one with no new digest, no digest_from or no box proof; check-test-record rule 2b
ties digest_from to the previous entry's digest; check-test-record-move now judges re-tests too (they change
.felhom.yml only — the gate looked at compose moves alone) and refuses a digest the registry no longer serves.
Decoys: 8 cases in test_gate_decoys.py, seen red with the rules switched off.
- upgrade-test.py --retest <app> [svc]: FROM the ladder head's tested digest TO the registry's current one, the
full method; --write-ladder writes a re-test entry (plain refs + digest_from), refusing without the box venue or
when the registry moved again. Writer tests, red-proofed.
- scripts/retest-floating.py — ONE command: --dry-run lists, --engines-only is the ruled start; bench, box
(retest_box.py on 9202 via the drill catalog), writer, gates, one commit per app. box_walk.py moves the box
client into the catalog. Run today: nothing to re-test on the database/redis lines.
- End to end on 9202 (drill): docmost at the OLD redis digest, the re-test, "run tonight's chain now" -> the leg
pressed it, the new digest runs, read back, badge current.
- Also: upgrade-test.py BENCH_ENV_OVERRIDES (R-739, wanderer's DB address on the bench, recorded per verdict);
test_gate_decoys.py read kimai's tag and date from the clone (red on main since kimai moved).
Evidence: felhom.eu/documentation/audits/night-rulings-2026-09-30/A/
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
- upgrade-test.py --restep <verdict> --definition <dir> --catalog <c> --evidence <rel>: the only way a
superseded step's own files (steps/<key>.yml + .felhom.yml) change after the fact (Part D: immich's
step 0b8272068aab36bf still pins 512M). Refuses a non-proven or OOM-killing re-proof, the head entry,
and a definition whose images are not the step's; leaves the ladder entry untouched (digests, box
evidence, the box's failed-step fingerprint). Two tests; the gate accepts the result.
- zipline's verify waited on `/` for 200/302/307; on 9202 it answers 301 and the readback returned
False with no line — it now waits on /api/healthcheck like its seed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
- fixtures (upgrade_fixtures_box.py): calibre-web (Upload form -> OPDS readback + the served EPUB),
wger (web login -> weight entry API), crafty-controller (API v2 roles), uptime-kuma (socket.io
polling: setup, login, addStatusPage -> public /api/status-page/<slug>); gitea posts its own
first-run installer form (R-624's fixable case) and keeps the admin CLI for an installed one.
calibre-web and wger run the template's own after_install on the bench, which has none.
- R-735: the bench's `password:N:special` now has the controller's shape (randomWithSpecial);
test seen failing first (length 32, no special), then 45/45.
- upgrade_boxport / the memory watch: a backend traefik reaches over https (loadbalancer.server.scheme)
is reached over https on the bench too (crafty-controller).
- upgrade-test: files-before/after-detail.json and `files_changed_detail` NAME the files behind a
files_may_change mark (R-734's method, now in the harness); test ChangedFiles.
- test_catalog_gates: the gate count was stale (9, the runner has 10) and red on main; now 10.
Evidence: felhom.eu/documentation/audits/more-night-apps-2026-09-30/
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
zipline 4's first-run route is POST /api/setup (measured on the bench 2026-09-30, v4.6.1: GET ->
{"firstSetup":true}, POST {username,password} -> 200 SUPERADMIN, the login works). The two paths the
fixture tried stay as fallbacks. No image moves in this commit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
- Sparkyfitness: better-auth sign-up/sign-in, a check-in weight stored and read back; a wrong
password and an empty date must read as absent. Waits out the app's own 429 (one client
address behind traefik).
- Rallly: sign-up, the six-digit e-mail code READ (select only) from the app's own
verifications row in place of a mailbox, verify-email, sign-in, polls.make, readback by the
public polls.get; an unknown id must be not found.
- Outline: the self-hosted first-run route installation.create (workspace + admin, refused once a
team exists), an API key with Outline's own CSRF pair, a document, readback by documents.info;
an unknown id must 404 and a wrong key 401.
- outline and rallly leave the NoRoute list: both had a front-door route after all.
Measured on the bench (LXC 9401) and on 9202 2026-09-30:
felhom.eu/documentation/audits/pg-last-six-2026-09-30/
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Proven on 9202 with controller 0.279.0 (drill 5ac5daf): the default no longer logs in, the generated first
password from the app page does, a wrong one does not. Copy freeze: bookstack's new/changed strings signed off.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
- memory watch: load no longer follows redirects to the unresolvable test
domain (every request had been 'err' on box-fixture apps), and sends the
app's own Host; the app's own memory (anon) is sampled beside the cgroup
peak, and memory_tight reads anon where measured (09 decision 22, CC).
- ladder writer: memory_peak_pct = anon (else cgroup peak), with
memory_basis and memory_cgroup_peak_pct beside it.
- fixtures: opengist 1.15 serves under /-/ and marks its cookie Secure
(readback = the account's own page + a never-created user 404); komga's
user endpoint is /api/v2/users/me; a wishlist fixture (form actions).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
update_ladder: in .felhom.yml, one JSON entry per line (spiked live on
controller v0.266.0 and v0.267.0 first). Two gates: check-test-record.py
(static, CI too) and check-test-record-move.py (history + registry for
moved refs only). 16 decoys, 3 red-proofs. The ONLY writer is
upgrade-test.py --write-ladder (bench AND box proven, digests resolved).
Harness v3: box fixtures on the bench, files_may_change.
Backfill: the 21 moves of 2026-09-22, 21 proven from their records.
No image: line moved.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
After an edge reads back, --soak seconds (default 600) of light load while
the kernel's own oom_kill counter is read host-side from the container's
cgroup. A kill or restart turns proven into failed; a peak over 80% of the
limit adds the memory_tight mark. New Romm fixture; edges M1 / M1old.
Red-proof on scratch 9202: M1old (template as promoted, 512M, 4 workers)
OOM-killed at +76 s -> failed. M1 (current, 768M, 2 workers) proven, 0
kills in 608.5 s, peak 81% -> memory_tight.
Test code only; no template changed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Suite 56 -> 64. Both faults re-introduced one at a time and refused; an explicit container no
service declares refused; a unique prefix still resolves; the name moving in a comment convicts
nothing; and the no-PyYAML mode CI runs is covered both ways.
The unique-prefix case first used paperless-ngx and was WRONG - paperless-webserver does not begin
with paperless-ngx, so the gate was right to convict. Rewritten with immich.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
No image: line moved.
paperless-ngx has no container named after its stack, so its probe had NEVER run on any box.
immich has four immich-* containers and no exact match, so the old first-prefix rule picked
whichever came first - possibly the database.
The gate now resolves the target by the same four rules as findProbeContainerMeta: exact name,
explicit container, a UNIQUE prefix, else refuse - and refusing is right, because verifying waits
on this probe and a successful update of such an app gets stopped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS