scripts/test_repo_gates_docstring.py pins the numbered list in repo_gates.py's
docstring against the GATES table IN ORDER. I listed poster-facts as 17 but
inserted it before decoy-coverage, so the sets matched and the order did not:
'same set, different order'. Caught on DooPlex, where script-tests runs (it
needs fcntl and cannot run on Windows).
Moved the table entry after decoy-coverage rather than renumbering the
docstring: the gate genuinely belongs last, since it only reports.
PART A -- the poster. documentation/architecture/felhom-system-poster.html
(307 KB). Secret scan first: ZERO IPv4, zero PEM blocks, zero ssh keys, zero
Bearer. The one EAA... match is base64 inside an embedded "mime":"font/woff2"
blob, not a Facebook token. "token"/"secret"/"password" appear 11 times and
every one is a NAME ("6. ep0 read token", "the hub seal key"); the poster
itself says "Names only; no secret values". All five long base64 blobs are
declared assets: 1 image/png, 3 text/javascript, 1 font/woff2.
It renders with NO network: the source mentions cdn.jsdelivr.net and Google
Fonts, but the loaded requests are only the HTML plus blob:/data: URLs -- the
bundler inlined everything. Measured, not assumed, and it matters: this is a
disaster-recovery document, so needing the internet to draw would be a defect.
No console errors.
The operator's three Claude Design fixes are all present: (a) no "WG" badge,
WireGuard only for the tunnel, no badge on the ep0-copy tile; (b) the box ->
ep0 arrow reads "encrypted on the box, sent through WireGuard"; (c) "Known
gaps" holds two items and NOT the household-keys sentence, which is now a
neutral "By design" note under the ep0 household namespace.
ONE FACT ON IT WAS WRONG. The felhom.eu tile said "served from DooPlex through
Cloudflare". It is not: Cloudflare is DNS only and the traffic goes direct --
measured this morning for the privacy notice, which states exactly that. The
poster would have contradicted a published page. Fixed in place (a label):
"served from DooPlex, Cloudflare DNS only". The first wording overflowed the
fixed-size tile, so it was shortened to fit and the evidence lives in the
facts file instead -- checked by re-rendering, not by hoping.
PART B -- the facts and the rule. DESIGN-PROMPT-...md is renamed
felhom-system-poster.facts.md (one home per fact), with the three fixes folded
in as explicit instructions so a regeneration cannot undo them, plus a new
"Badges" section saying a "WG" chip must never come back.
New rule, section 6 "The system poster stays true", added IDENTICALLY to all
five copies of unprompted-work.md (the four repos and the workspace root on
DooPlex; verified identical by diff before and after) and to
PROMPT-TEMPLATE.md's end-of-session checklist as a FIFTH coupled artifact.
scripts/poster_facts_gate.py WARNS when the facts file has a newer commit than
the poster. It never fails a push, deliberately: a refresh needs Claude Design
and the operator, --no-verify is forbidden here, so a blocking gate would leave
deleting it as the only way out. It compares COMMIT times, not mtimes, because
a checkout rewrites mtimes and every fresh clone would shout.
RED-PROOF -- and it found a real bug in the gate. The first run warned
correctly but exited 1: a single non-ASCII character in its own warning raised
UnicodeEncodeError on this cp1250 console. A gate whose entire contract is
"never fails a push" was failing pushes. Fixed (ASCII output + an encode
guard), and the decoy now asserts BOTH the warning and exit 0. Three branches
proven: facts newer -> warns, rc 0; poster newer -> quiet, rc 0; poster
missing -> "could not tell", rc 2, not a false all-clear.
The decoy itself was seen to fail, twice, on Linux (the suite needs fcntl and
cannot run on Windows): breaking the warning gives STALE_WARNS=False, and
making it exit 1 gives RC_STALE=1. All 80 felhom.eu decoys behave.
PART C -- do box reports pass through Cloudflare? NO. Two channels. DNS from
PUBLIC resolvers (not DooPlex's own, which answers the LAN address):
hub.felhom.eu is a CNAME to dooplex.hopto.org -> 37.191.56.193, not a
Cloudflare address, and no cf-ray comes back. The manifest: an ordinary k3s
Ingress, Cloudflare named only in a DNS setup comment. THE CONTROL that makes
the negative mean something: iso.felhom.eu resolves to 172.67.x / 104.21.x,
real Cloudflare addresses -- so the method does detect proxying.
So nothing is added to the Cloudflare row: the hub path does not touch it.
06-offsite-connectivity.md section 1 claimed the public edge is a
Cloudflare-Tunnel and "DooPlex has no public IP" -- both untrue today. Kept
and marked STALE with the measurement rather than rewritten, because that
paragraph is the reason ep0 exists and the argument needs its premise visible.
total-loss-of-dooplex.md's "today a CNAME to dooplex.hopto.org" is confirmed
correct.
Register: 137 before, 137 after, 0 opened, 0 closed -- every finding here was
small and fixed in the session.
DRAFTS. COPY.md section 6: two versions of the Page's first post, shortened
from section 2. Hungarian, tegezo, no price. 6.1 = 347 characters, 6.2 = 638
(counted as Unicode characters). Every claim carries a source comment naming
the line of website/index.html it rests on; the first line of each carries the
point alone, because Facebook cuts after about three lines.
Deliberate: ZERO emoji and ZERO hashtags, though the brief allows two of each.
The Felhom design system uses no emoji (the website gate holds it at 0) and
two hashtags would serve no real search.
CHECKED, because the post repeats it: "56 alkalmazas" is CORRECT. The apps
page carries 57 <div class="app-card"> but states 56, which looks off by one
until you read the category line -- "6 alkalmazas + 1 beepitett". The 57th
card is FileBrowser, built into every box and deliberately not counted.
index.html says "56 telepitheto alkalmazas" too. I nearly "fixed" a live page
into being wrong. NOT-A-FINDING.
SCHEDULE-POST. A third sub-command on fb_probe.py, reusing its token loader
(R-453), redaction, Bearer call and evidence writer:
- the body is READ FROM COPY.md by section name. The Hungarian never passes
through a shell or an argv string (brief 9.6); the caller names a section.
- published is ALWAYS "false" and NO argument can change it (brief 9.7).
The operator's review in Planner is the safety net, so an immediate post
must be unreachable, not merely not-the-default.
- check_when refuses a time under Meta's 10-minute floor or over its
6-month ceiling, BEFORE the call, so a bad time is a readable local
refusal rather than a Graph error.
- budapest_to_epoch uses the real tz database. If zoneinfo has no
Europe/Budapest it REFUSES rather than falling back to a hardcoded
+01:00/+02:00 -- guessing the offset is how a post goes out an hour wrong
across a DST boundary.
- list_scheduled tries /scheduled_posts then feed?is_published=false and
RECORDS WHICH ANSWERED; when both are refused it returns None so the
caller says "unproven" instead of claiming a removal it never saw.
TESTS: 30, of which 2 skip on Windows (no tzdata in this interpreter; the
command runs on the Linux host, which has the system zoneinfo).
RED-PROOF of the guard that matters, as the brief requires. Made
schedule_form accept published=..., ran ScheduleForm, and watched
test_no_argument_can_publish_immediately FAIL:
AssertionError: 'true' != 'false' : published changed published
Guard restored, all 30 green again.
No post has been made. The dry check and the real post come next; the
operator picks the version and the time first.
WHY .gitignore "was not working": it was working. git never consults
.gitignore for a file it ALREADY TRACKS. The rule `*secret*` matched fine --
proved by dropping an untracked copy in and watching check-ignore name
`.gitignore:3:*secret*`. The file had been tracked since feea0606, which is
ironically the commit that de-gitted the Resend key.
WHAT THE EXPOSED VALUE ACTUALLY WAS. Not an analytics password: the GITEA
ADMIN ACCOUNT PASSWORD (is_admin true; /api/v1/admin/users answered 200), in
a repo gitea.dooplex.hu serves anonymously to the internet. That is push
access to every repo -- including the one whose website/ is git-synced live
and whose scripts/ is published by tag to every new box installer (R-110).
Re-ranked P2 -> P1 on that measurement; my first ranking had only measured
the analytics blast radius.
Every committed value was still live. Nothing had ever been rotated.
ROTATED (values never echoed; written to a 0600 file on DooPlex):
umami-config APP_SECRET + POSTGRES_PASSWORD. The password was
changed INSIDE postgres (ALTER USER) as well as in the
Secret -- the env var is only read at first init, so
patching the Secret alone would have changed nothing.
healthchecks-config SECRET_KEY + SUPERUSER_PASSWORD (nothing consumes them,
there is no healthchecks Deployment).
gitea-creds no longer holds the admin password at all: a SCOPED
token (read:package + read:repository).
gitea admin new random password; gitea-system/gitea-admin updated.
VERIFIED, not assumed:
- new admin password -> 200, OLD PUBLISHED PASSWORD -> 401 (the leak is dead)
- umami: a real beacon returns 200 (so the app authenticates to postgres and
writes) while a bogus site id still returns 400 (so the 200 means something)
- hub: "Registry version check: latest = 0.304.0" AND "Template fetched
(5881 bytes)", no auth failures
- BOTH token scopes are load-bearing, and the second was found by breaking
it: a package-only token made the hub log "Template fetch: unexpected
status 403", because the template fetcher reads a raw file out of the
felhom-controller repo, not the registry.
AN INCIDENT CAUSED BY THE FIX, recorded because it is the useful part: the
rollout restart needed to pick up the new umami secret put umami into
CrashLoopBackOff and took stats.felhom.eu down (503) for ~4 minutes. Not the
rotation -- at memory 512Mi that pod runs for months but CANNOT RESTART:
startup (Prisma + Next.js) peaks over the limit and is OOMKilled (exit 137).
Raised to 1Gi IN THE MANIFEST, not just live, per .claude/rules/manifests.md
("never bare kubectl set -- the next sync reverts it and the fix silently
disappears").
THE GATE: KNOWN_BACKLOG is removed from manifest_bearer_gate.py, as its own
comment instructed. Red-proofed with a decoy: exit 1 with it, exit 0 without.
An exemption kept this visible for three months and changed nothing.
WHAT REMAINS (operator, and it is bigger than what was fixed): the same
password is still the admin password in ~12 other namespaces -- nextcloud,
paperless, bookstack (a DATABASE ROOT password), tandoor, calibre,
adventurelog, gokapi, qbittorrent, servarr, homepage. Rotating Gitea does not
touch them. Also owed: a kisfenyo Gitea token sits in plaintext in the local
homelab-manifests remote URL and was printed to a session transcript during
this investigation, so it should be replaced regardless (R-580's shape).
NOT a finding: homelab-manifests is private (404 anonymously) and does not
contain the password; ArgoCD's repo credential is a separate token and was
untouched by the rotation.
Two new Hungarian pages, live on push: /adatkezeles (privacy notice) and
/feltetelek (what the free closed test is, and is not). Operator rulings of
2026-10-09: no company exists yet, so the operator is named as a PRIVATE
PERSON with no postal address and no phone; publish before the lawyer has
seen it, because the site was collecting data with no notice at all; the full
ASZF, the impresszum and the review wait for the company (R-802, R-809).
- All 18 pages carry the operator, info@felhom.eu and both links in the
footer. Counted, not assumed: 18 pages found = 18 with both links = 18
structurally valid. English footers say the legal texts are Hungarian.
- The contact form stopped claiming something untrue. The old consent said
"az adatokat harmadik felnek nem adjuk ki" while Resend, Cloudflare and
Google carry the message. Both languages replaced; the link opens in a new
tab so a filled form is not lost.
- site_gates.py NO_TWIN gains the two pages ON PURPOSE, with the reason in a
comment: an unreviewed English legal text would be worse than an honest
pointer from the English footer.
- documentation/legal/{adatkezelesi-tajekoztato,feltetelek}-1.0.md are the
text of record, DERIVED from the published HTML so they cannot drift. The
drafts are kept and marked superseded for the closed test.
FOUR LOAD-BEARING CLAIMS WERE MEASURED, not copied from a vendor or a README:
cookies zero, and no local storage - checked in the browser WITH A
POSITIVE CONTROL (a probe cookie WAS visible to the same
method) after the tracker fired; no Set-Cookie on any response
beacon the exact Umami payload: site id, screen, language, title,
url, referrer - no visitor identifier
Cloudflare DNS only: the public A record 37.191.56.193 is not a
Cloudflare address, so site traffic cannot be proxied
fsn1 Falkenstein, Germany (Hetzner's own location list)
Also measured: felhom-ep0-copy-gc.timer is installed and RAN SUCCESSFULLY
(2026-10-09 08:00, exit 0), so "deleted within 30 days" is true today where
on 2026-10-08 it was written but not switched on.
Three retentions are stated as having NO deadline, deliberately and with the
operator's word: website statistics, web server logs and contact messages
have no automatic deletion, and the pages say so instead of promising a date
nothing enforces. Every other period is enforced by configuration and cited.
No placeholder survived onto either page (0 of "[[", control: the draft still
has 36). The impresszum and the full ASZF are NOT published.
R-813 -> NARROWED. R-915 unblocked: the operator enters the two URLs in the
Meta app's Basic settings; the Live switch stays a separate decision.
R-917 and R-920 -> DEFERRED on the operator's (c): park, publish as posts
once posting starts.
Second Facebook task of the day. Page settings changed by hand in the operator's
Chrome; every edit read back from a channel other than the one that made it,
because Meta's toasts have lied here before (2026-10-08: "A modositas nincs
mentve" arrived with a partial save).
- Contact (B1) was ALREADY SET, by the operator, before this run: Graph reads
emails ["info@felhom.eu"] and phone "+36702378499". Verified, not typed.
- Place (B2) already city-only Budapest, as the operator chose. Service area
REFUSED: Facebook offers the field but its picker has no "Magyarorszag",
only cities. Control: "Szeged" returns Szeged. Left unset rather than
narrowed to Budapest, which would shrink a coverage claim nobody authorised.
- Categories (B3) DONE: Informatikai vallalat (kept first, the only one shown)
+ Internetes ceg + Szoftverceg. Facebook's Hungarian list has no IT-support,
IT-consulting or cloud category; eleven terms searched, and the one true
match is a repair counter, which the fences rule out.
- Hours (B4) CANNOT be set and need not be: Facebook requires a street address
first, which the fences forbid. Measured on the rendered page with controls
present -- Zarva 0, Nyitva 0 while Budapest 1, Informatikai vallalat 1. The
Page will never show "Zarva".
- Messenger FAQ (B5) REFUSED -> R-920: the automation does not exist for this
Page. Catalogue holds exactly three templates; search "kerdes" returns none
while the control "uzenet" returns two. COPY.md section 5 is written anyway,
questions verbatim from gyik.html and answers condensed from each question's
own answer, and waits like section 2 does (R-917).
- Link preview correct in both languages, re-scraped once each; only the
expected fb:app_id warning, deliberately not fixed. Both report HTTP 206
where a plain curl gets 200 -- Facebook's scraper, preview complete.
fb_probe.py read now also reports emails, phone, category_list, location,
single_line_address and hours, ONE FIELD PER CALL: a batched fields= list fails
whole when any member is unreadable, which would let one refused field hide the
other five. Refusals are logged verbatim and never retried; "null" and "not
returned" are logged apart. hours is never returned by Graph, which is why B4's
read-back had to come from the rendered page.
Also corrects the "Business & legal" header, which read 12 rows (P2 5) over a
section holding 11 (P2 4); with R-920 it is 12 (P2 4, P3 1, P4 7). Only the
section this commit edits -- the other drifting headers belong to a session
that owns the register.
No post, no invite, no money, no app or portfolio change, website unchanged.
Read phase passes (Page 1360018983863273, CREATE_CONTENT/MODERATE/ANALYZE, page token PAGE expires_at 0, three insights
metrics alive on v26.0). Write test: removal check accepts Meta's code-10 'Object does not exist' (fixed without a row,
4 tests); run 1 evidence kept. R-914 READY, R-915 narrowed to Live mode.
scripts/facebook/fb_probe.py (stdlib, read + write-test, no real-post command) with tests; read run twice:
SYSTEM_USER, expires_at 0, /me/accounts empty, so D/E did not run and nothing was posted. Findings, redacted
evidence, CONTEXT decision home, STATUS item, scripts CHANGELOG, REPORT-facebook-page-api.md.
Host page "Operator Actions" card: run off-site backup now, run a check now
(fixed job list), stop / extend (1-30 days) a deletion countdown. POST
/hosts/{id}/operator-action validates against the CLOSED list before
storing (unknown -> 400, no row), stores operator_actions(id, customer_id,
action, arg, requested_at, requested_by, done_at, outcome, message), logs
who pressed (channel + address) and bumps the box's intent. The report ACK
lists pending rows as operator_actions until the box's
operator_action_results closes them (matched on id AND reporting
customer); each closed row becomes a hub-minted operator_action event
(stored, never dispatched). Unanswered after 24 h: expired. A customer
RESET cancels pending rows. Wire gate: new root + field-by-field mirror
(controller report.OperatorAction) — needs the controller commit first.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
KernelDue / KernelNotify (09-20 h Budapest, one per 20 h, max 3, registered
address, only an accepted mail counts) / os_update.kernel {kver, tonight}
(no mail, no step) / layer kernel ingest + operator events / Approve kernel
set after every ring-0 box booted it healthily after a night stage / two
System page cells. 11 §5.11 written; §5.10 status corrected (proven).
Installer uninstall knows the two GRUB generators (unreleased).
Evidence: audits/kernel-lane-2026-10-07/ (red-proofs, boot timing).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
sysfacts reads the agent's top-level guest_disk_trim stanza (schedule + per-guest
last attempt: vmid, last_attempt_at, ok, bytes_trimmed, mounts, duration_seconds,
last_ok_at, error) into a field-by-field mirror. The System page's new 'Last disk
trim' column shows the last successful trim and the GiB it freed; amber when the
newest attempt failed (error shown) or last_ok_at is older than 14 days (judged on
the success time, never the attempt time); '—' when the agent sends no stanza.
wire_contract_gate: SUBTREE_MIRRORS checks guest_disk_trim field by field BOTH
ways against sysfacts.DiskTrim; decoys (ok renamed, last_ok_at dropped) in
test_gate_decoys.py. Decision 139.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
iso_bootstrap_gate.py runs scripts/iso/test/bootstrap-modes.sh in felhom-iso-assistant:trixie
(staged copy, read-only mount, --network none), registered fast=False in repo_gates.py so the
pre-push hook and CI (both --fast) never run it (decision 147). No docker / no image / docker
error -> exit 2 NOT CHECKED. Every green run is followed by a built-in decoy: the harness must
FAIL a bootstrap whose pairing banner never paints (R-496 shape), or the gate convicts the
instrument as blind. Docker-free decoys in test_iso_bootstrap_gate.py (fake docker on a
one-directory PATH), run from test_gate_decoys.py (COVERS).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
controller/offbox-rename kept its EXEMPT entry after R-425 gave it decoys, so
the debt list read longer than the debt. The gate now prints a STALE EXEMPTION
line for any exempt gate that has a decoy (named, not convicted: the decoy lands
in a sibling repo, and failing this repo's push for it would couple the two).
A check in test_gate_decoys.py plants a stale entry and asserts it is named;
seen to fail with the notice removed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
DONE/IDEA were searched over the whole state cell, so an open ROADMAP row whose
state named ANOTHER row's verdict (`READY — split out after R-86 CLOSED`)
escaped as shipped — R-87's shape from R-378, one file over. The gate now reads
register_table.leading_verdict(state); all 28 current rows classify unchanged.
Decoys: that shape (seen to PASS against the old gate), an id present in the
register only as prose (seen to PASS when `have` is loosened to any mention),
the existing suffix-id row; genuine: a row with a counterpart, and an `idea` row
asserted BY DESIGN — R-424's hole, accepted by the operator 2026-10-05.
EXEMPT drops `felhom.eu/one-register`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Four cases in test_gate_decoys.py, planted inside the real DUE-CHECKS block:
an orphaned check whose id survives only in prose, the due-TODAY boundary with
due-tomorrow as the genuine article (today pinned via FELHOM_GATE_TODAY to a
date before every real check), and the block's marker named in prose (exit 2).
Each convicting decoy was seen to PASS under a mutation of the gate (row match
loosened to the bare id; `<=` to `<`; duplicate-marker refusal removed). The
gate itself is unchanged — no hole found. EXEMPT drops `felhom.eu/due-checks`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
The R-185 check counted the bare word `felhom-backup-target-apply grant`, so the
dry-run echo stood in for a deleted real grant (2 resolutions, 2 "grants"); now
only path-qualified invocations at a command start count. The age check matched
a commented-out install; now comment lines are excluded. Decoys (plus a version
const in a new hub sub-package, and a comment naming the identifier that must
pass) live in test_gate_decoys.py; all three convicting decoys were seen to PASS
against the pre-fix gate. EXEMPT drops `felhom.eu/hostinstall`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
PATH_RE gains .md. The false-positive walk across all four repos found one: an audit document cited
by app-catalog's REUSE.md, hidden by the evidence-copy exclusion — excluded trees are now walked for
.md documents only, so a .go evidence copy there still never satisfies a citation. The KNOWN HOLE
decoy now expects a conviction.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Once retrieval_promise_gate.py imports customer_copy_vocab and drops its STEMS literal, the drift
check reads "ok — single source" instead of failing on the missing literal. An import only in a
comment, or beside a stale literal, is still drift. Pinned by scripts/test_hub_copy_drift.py.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
The escrow/retained and escrow-ACK wires are now compared path-by-path against the receiver's named
mirror type, so the measured mutation (agent renames superseded_at, the old name still a local-API map
key) convicts. Decoy pair in test_gate_decoys.py, seen failing with the mirror removed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
The check itself shipped with R-297 (golden_local_matches_manifest: sha256 or controller version
against the hub manifest; mismatch re-fetches, a named --golden is refused). This adds the
disclosure line the row also asked for and tests that pin the check's place before the adopt.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
- The R-297 named-golden refusal no longer repeats "The vouched golden is X." when its reason
already says it (kept when the reason does not name the version).
- --uninstall checks it can open /dev/tty before the typed vmid confirmation and, if not, refuses
with a sentence (deliberate; --force does not skip it) instead of "/dev/tty: No such device".
scripts/test_hostinstall.py: test_golden_refusal_* (2), test_require_tty_* (3, with a pty control).
The runbook line naming the pty requirement is a documentation edit for the lead.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Units carrying the agent's network-storage marker (mnt-*.automount first, then mnt-*.mount) are
disabled --now, reset-failed and removed before the drive umount loop; a share that will not stop is
not forced — its unit is kept and named in KEPT with the commands. Enrolled-drive and foreign units
are never touched. scripts/test_hostinstall.py: test_net_units_* (5).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
The step-8 restore reads the golden as the agent's token; a storage outside PVE_STORAGES (and not
the backup target, which step 6 grants since R-185) 403'd at step 8/8 — after the token was minted
and root@pam rotated. Pre-flight now refuses it with the two remedies (move the golden, or add the
storage to --acl-storages). scripts/test_hostinstall.py: test_archive_storage_* (4).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
HARD_MIN_LVM_GIB -> RECOMMENDED_MIN_LVM_GIB; the warning says the install continues and to proceed
only with deliberately sized grows (the day0-install runbook's wording). Behaviour unchanged.
scripts/test_hostinstall.py: test_lvm_minimum_is_named_as_what_it_does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
_state_put and _state_mark are no-ops under --preflight-only as well as --dry-run, so the banner
"no state written" is true and the dnsmasq ownership answer is recorded only by the real install's
own preflight. The log says "would be recorded at install" on a preflight-only run.
scripts/test_hostinstall.py: test_preflight_only_writes_no_state (+ its control and a no-other-writer check).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS