| 2026-08-03 |

rewrite causation to lead with the procedural miss; add tool index, hard rule 4, rebuild brief
...
Operator authorised committing the causation rewrite so the Chat rebuild project
has access to it, and directed the correction of a pattern this session exposed.
CAUSATION, REFRAMED. The prior draft blamed the --force flag. The accurate
headline is procedural: I ran an ad-hoc destroy instead of the tested D-061
teardown scripts this repo built for exactly this operation. Measured delta --
both scripts ALWAYS pass a storage flag (--destroy-storage on the destroy path,
--release-storage on the release path); I passed none, having removed it
deliberately after reasoning from juju help. D-061's problem statement names my
exact form as a known failure, observed 3x. I do NOT claim the missing flag is
the mechanism behind the orphaned status doc -- that is unproven -- but it is the
concrete delta and the procedural miss stands regardless. The scripts also do far
more than one command: remove-machine --keep-instance per host, hard MAAS
verification, then destroy, then orphan sweep, with a harness. The evidence ruling
OUT the 08-01 memory resize is unchanged and still sound.
THE PATTERN, recorded because it is the real finding: third time in one session I
bypassed an existing tested path.
CORRECTION, as artifacts rather than a promise:
- docs/tool-index.md -- task-first index of what is already built and tested.
93 scripts / 28 runbooks / 98 harnesses existed with NO index; the teardown pair
appeared only inside D-061 prose. Includes the agnosticism review the operator
asked for: the D-061 pair is HALF agnostic (MODEL and MAAS_PROFILE are
parameterised; HOSTS, ORPHANS, PROTECTED and --canary are VR0 literals not in
lib-hosts.sh) and an unresolved host is a note, not a fail -- so pointed at VR1
the entire host-safety audit silently no-ops while the destructive step still
fires. Logged, not fixed: hard rule 1.
- CLAUDE.md hard rule 4 -- look up the tested artifact before typing a command;
a raw juju/maas/tofu/virsh invocation is the error signal that the lookup was
skipped; a missing tool is a finding, not licence to improvise.
- SKILL.md: two routing rows (any teardown/rebuild; "what tool exists for this
job") -- the table previously had a teardown row for VR0 only.
docs/repo-rebuild-project-brief.md -- the handoff prompt for the separate Chat
rebuild project, covering the layered-hardening cause of information spread, seven
evidenced failure instances from this week, the BUILDOUT vs EXECUTION distinction
as the central thesis, a required Opus 5 rebuild of CLAUDE.md / skills / hooks /
governance grounded in Anthropic's published guidance, a ten-role peer-reviewed
agent team, and an acceptance test drawn from the real failures.
Figures in the brief were re-verified rather than carried: the status authority is
7,357 lines / 647 KB (it grew from 6,938 during THIS session, mostly
agent-authored, which is itself the documented Opus 5 written-deliverable-length
behaviour feeding the problem the project exists to fix).
repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
| 2026-08-02 |

CORRECTION: the MAAS region is INSTALLED in the DC, never migrated
...
Operator correction, verbatim: "MAAS region gets installed directly on the DC,
it is not migrated. If you have read this somewhere we need to make sure it is
corrected so future deployments don't follow a regional migration path."
I told the operator a dc0 environment rebuild would 're-run the D-132 region
migration work'. That framing came from two surfaces, and both are now fixed:
SKILL.md carried a standing invariant headed 'A PER-DC MAAS REGION MIGRATION HAS
AN ORDERING THAT IS NOT THE OBVIOUS ONE'. SKILL.md is read BEFORE any runbook, so
a session planning DC3/4/5 would have followed a migration path that does not
exist. Reframed: the region is installed in the DC at standup and nodes enlist
into it first time. The dc0 migration of 2026-07-30 was a ONE-TIME remediation
for machines already enrolled in the Office1 region before D-132 q1 was ruled --
a state no future DC can enter. The block is retained as record only, explicitly
not as a workflow.
design-decisions.md's D-132 amendment says 'Per DC this means: install region +
PostgreSQL; re-enrol and re-commission 9 nodes; recreate the D-133 carve...'.
True only of dc0 and dc1, which already existed. Annotated in place per D-117's
treatment rule rather than rewritten. The region-target parameter half DOES
generalise -- a per-DC region must be addressed explicitly; the re-enrolment half
does not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

DOCFIX-208 part 3: graduate the dry-run instrument finding; harden Step 4.4
...
The juju finding belongs above a changelog -- it generalizes to any bundle
deploy, not just phase-4's gate. platform-traps.md gains a Juju section: what
--dry-run prints, what it never prints at any verbosity, the two grep decoys
(controller API addresses look like DC-band VIPs; --debug echoes the command
line so overlay filenames hit), and the three consequences.
Step 4.4, written earlier this session and never run, gets two corrections
before its first execution: the --format=json read used a direct index, so a
missing key would print a traceback into a results column -- this repo has a
logged instance of an assertion satisfied by a traceback -- now .get() with a
visible <<UNSET-OR-APP-ABSENT>> sentinel documented as a failure. And the block
now states plainly that it is authored against a measured instrument but has
never been exercised, with the instruction to report a shape correction rather
than work around one.
repo-lint 0 fail / 1 legacy warn; gauntlet ALL GREEN (98).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Queued-findings backlog: sweep F1 + F6 CLOSED, F2 diagnosed, F3/F4/F5 graduated, DOCFIX-207
...
Works the 2026-08-02 close sweep (docs/audit/queued-findings-20260802-stage5-edge-fold.txt)
with three read-only agents in parallel. Cites the SWEEP register (F1-F6); the runbook fold
register has its own F1-F12 and is untouched.
sweep F1 CLOSED -- the dc0 rack's staged deploy input matches the repo again. One gated scp
of overlays/vr1-dc0-vips.yaml, ed19d989e80da8da -> 80d861560a6b3c52. A single-file copy was
provably sufficient because the WHOLE staging dir was enumerated first: 14 files, exactly 1
diverged, 0 missing from the Step-4 deploy closure (policies/overrides.zip present at the
repo digest), 0 orphans. All 14 re-verified against CURRENT HEAD after the copy. The 0600
octavia PKI overlay is untouched (same digest/mode/mtime) and was hashed, never read.
Repo-side correctness MEASURED not inherited: 0 ULA legs, 39 GUA, render-drift 4/4 naming
the file with a proof-of-teeth case.
sweep F6 CLOSED, PASS -- the dc0 MAAS region DB is proven uncorrupted. pg_dump read every
page of every table in maasdb: 23,878,796 bytes / 37,199 lines / exit 0 / empty stderr,
completion marker asserted separately. F6's own diagnosis was wrong: snap confinement does
not reproduce as ubuntu. The discriminators are ROLE (maas, not ubuntu) and TRANSPORT (over
TCP the role is password-challenged, over the unix socket it needs no credential). Both
identity values now measured -- maasdb had been prose. Dump streamed, nothing persisted.
sweep F2 DIAGNOSED, not fixed -- reading R1 is true, R2 refuted. debmirror prints "All done."
then exits non-zero; confirmed at vendor source and re-verified independently here
(debmirror 1:2.39ubuntu2, 1615 say("All done."), 1620 exit 1 if !$ignore_small_errors).
Cause: one 500 read timeout on a jammy-backports dep11 index. TWO corrections to the sweep:
its "the log says it succeeded" quotes all came from the PASSING UCA leg; and "stays RED"
overstates it -- measured 16 runs, 7 finished, 9 failed, with four fail-then-succeed pairs
hours apart. No remedy applied: debmirror's exit code conflates "nothing mirrored" with
"mirrored minus N transient files", so any tolerance change alters what the gate attests.
DOCFIX-207 -- preflight P6 quoted "50 apps / 97 relations"; measured is 56 / 108. The same
figures were corrected in phase-01's own gate on 2026-07-10 and this copy was missed.
sweep F3/F4/F5 graduated from the audit capture to durable homes: systemctl show fabricating
Result=success for a non-existent unit -> platform-traps 5c; assert the harness case count
moved, and two scripts probing one endpoint must share the probe definition -> script-authoring.
Logged NOT fixed (hard rule 1): preflight P2 validates a merged input including
vr1-dc0-machines.yaml while phase4:527-528 says the file does not exist and Step 4 does not
pass it -- a gate grading a different artifact than the deploy consumes, on the Step-4 path.
And security-ledger SEC-029(3) calls ~/repo-stage a nine-file copy; it is fourteen.
session-ledger machine-derived block re-seeded (was the 2026-07-27 seed: 21 SEC / D-138;
now 28 SEC / D-141 / DOCFIX-208, each added SEC row verified against the register).
Gates: repo-lint 0 fail / 1 warn (legacy carve-out); run-tests-all ALL GREEN (97 harnesses,
count unchanged, so nothing moved silently); tests/preflight 43/43; tests/render-drift 4/4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Fold: open the register, close both Class-A rows (F1 phase4, F12 SKILL.md)
...
Operator ruled fold-before-exercise, start at Phase 2, all DCs come up in their own
region. Register: docs/runbook-fold-register.md, 12 rows classed A/B/C.
Measured gap: D-138 and D-139 appear in NO runbook. Nothing in the chain builds a per-DC
MAAS region, a snap proxy, or the GUA carve. Running it as written would rebuild the
pre-D-132/D-138/D-139 shape and fail where we already fixed things by hand.
F1 (Class A): phase4 asserted "Both DCs deploy from the Office1 headend by the SAME
procedure" and "Every juju and maas command runs there". The 2026-07-27 ruling it cited
was about DC ordering, not run location; D-138 reversed the location half. Replaced with
a tool-split table; superseded text struck in place, not deleted.
F12 (Class A, worse): SKILL.md -- loaded by every session before any runbook -- had ZERO
mentions of D-138 while stating Plane 2 executes on voffice1. Now carries the split, the
structural reason, and the measured test: juju controllers on voffice1 -> "No controllers
registered"; on the dc0 rack -> the live vr1-dc0-controller.
Both record the racks-have-no-repo-clone trap and the sha256-verify-staged-copies rule.
A row closes on the edit AND on being exercised by the dc1 Phase-2 rebuild; an edit alone
is a ruled-but-not-built claim.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
| 2026-07-30 |

Close sweep: queued findings F1-F10, memory review, skill invariants
...
Sweep capture docs/audit/queued-findings-20260730-dc0-region-migration.txt.
Three items lived ONLY in the transcript and would have been lost on a
clear:
F1 -- this session's permission rules exist only in the GITIGNORED
.claude/settings.local.json, so their verbatim text is now recorded,
including a broad Bash(ssh vr1-dc0-rack *) that an interactive approval
auto-added and which deserves review.
F2 -- the Office1 region STILL registers a rack controller on the dc0 rack
(vvr1-dc0/7chphy) and still holds the dc0 region VM's machine record. Not
harmful (Office1's DHCP for that VLAN is off, verified by process) but
dual-registration the operator should rule on.
F10 -- the five instrument errors of this session as one pattern, with the
rule that catches them.
F9 carries the next session's first commands and every measured input the
carve tool needs.
GA-R7 memory review: sharpened the classifier memory with the profile-token
lesson (a rule pinning 'maas admin' would have matched step 1 of the DHCP
cutover and silently missed step 3), and added an instrument-currency
memory. Both graduate to repo surfaces; neither is the only copy.
Skill swept: the per-DC region migration invariants folded in -- region
identity by rack, ids do not cross regions, the per-node ordering, batches
of three, zero SSH keys, the role-dependent carve, the role-tags gate that
passes on an incomplete tag set, and the two scripts that must not be
pointed at a new region.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

octavia-pki: a hardened, tool-driven controller-cert REISSUE path (F9's remedy)
...
Operator: 'Let's reissue now. Engineer the artifact, variable, based workflow to
reduce the chance of errors.' Five GA-R5 rulings, one exchange each, quoted in
the changelog; ruling 5 was 'Full script minter now'.
THE TOOL IS BUILT; THE MINT IS NOT YET RUN (gated operator action, 1.0-REISSUE).
scripts/octavia-pki.sh gains 'reissue <site> [--force] [--dry-run]': controller
LEAF only, signed by the EXISTING controller CA (neither CA regenerated, amphora
trust domain untouched), fresh P-256 key, zone DERIVED from the site token and
never typed, printf-built config (the F8 payoff), stage-assert-promote, and a
single-value overlay surgery that never puts the bundle in argv.
SIX GATE DEFECTS FOUND BY VALIDATION AGENTS, ALL CLOSED. Mutation testing is
what earned this: deleting the three assertions I had just written left the
harness fully GREEN every time, so they were decoration until each got a
failing-direction fixture.
- A15 keyUsage/EKU on the ISSUED cert. A cert with NEITHER minted, promoted and
passed everything. keyUsage appeared once in the file: the printf writing it.
- A16 validity. openssl x509 -req defaults to -days 30, and checkend/notAfter/
enddate appeared ZERO times in verify -- a 30-day cert passed forever.
- A17 the overlay must decode to what the workspace holds. A10 graded shape,
A1-A16 graded the workspace, nothing joined them; a desynced overlay read
PASS 0-failed and the overlay is what reaches the charm.
- The re-run guard REFUSED to fix broken certs: names-correct + stale IP SAN
gave verify FAIL and reissue exit 4 'nothing to fix'. Now gated on verify.
- A8's negative proves its instrument loads first (openssl verify exits non-zero
for 'could not load the CA' too, so a corrupt CA passed it vacuously).
- Two pre-existing overlay defects moved pre-mint, so they stop producing a
false exit-5 with the workspace already promoted.
Harness 23/23 -> 48/48. Runbook Step 1.0-REISSUE appended AFTER GEN.e
deliberately (F10 line-anchored mint-refs; creds-matrix reports no S4 findings
after the append). Skill gains two standing invariants for future DC standups:
every per-DC secret needs a ROTATION tool not just a generation recipe -- a
REFUSE-IF-PRESENT generation gate makes the dangerous path the only path -- and
a hand-pasted openssl chain is a defect class, with F8 and F9 as the receipts.
Also corrected: D-137 fork 1 is NOT open (sub-ruling 1, 2026-07-25, ruled
'Blocking in preflight' and explicitly declined the PreToolUse guard), so the
queued Q2 premise is stale in the same way Q1 was.
gauntlet ALL GREEN (89); repo-lint 0 fail; creds-matrix 5 findings, all
pre-existing and named; ledger-scan unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
| 2026-07-28 |

Close sweep: graduate three platform traps, rescue two changelog-only questions, fix a latent import
...
A session-close sweep for content that would have been lost. Four items.
(1) THREE PLATFORM BEHAVIOURS were recorded ONLY in CURRENT-STATE, which is a
status document that gets consolidated over time -- a durable trap does not
belong there alone. Graduated to references/platform-traps.md: MAAS auto-reserves
::1-::ffff:ffff on EVERY IPv6 subnet (so an explicit v6 band write is both
impossible and unnecessary, and is invisible in `ipranges read` -- only
`subnet reserved-ip-ranges` shows it); MAAS mode=static means EXPLICITLY
CONFIGURED, so creating a subnet addresses nothing and the carve is still owed;
and NetBox's live API returns scope.name as the DISPLAY name while this repo's
dumps normalise it to the slug, so a dump-written tool matches zero objects live.
(2) TWO OPEN QUESTIONS existed only in the session changelog -- which is
session-scoped scratch, explicitly not citable as status and consolidated away at
stage close. Both would have been lost: which host is authoritative for the gates
(now sharper, since executing R15(3) made preflight-on-voffice1 permanently
unpassable against P0-2's host-blind P5), and the provider-bundle-check arity gap
(now imminent rather than theoretical -- the apex holds the dual-family set the
renderer will emit, so the gate cannot validate what is about to be produced).
Also recorded: voffice1 tracks the precondition branch and must return to main at
merge.
(3) A latent import defect in netbox/dc-plane-apex-import.py: urllib.parse was
imported inside the __main__ guard while main() uses it. It worked only because
another module had already bound the submodule -- fragile, and it would NameError
if the file were ever imported. Moved to module scope.
Gauntlet ALL GREEN (84), repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
| 2026-07-27 |

Session close: GA-R4 bookend, skill sweep, ledger rotation, permission block removed
...
CLOSE SET, all executed and verified:
BOOKEND (GA-R4). The STAGE-5 GROUNDING AUDIT close bookend had already landed
while the operator was away, by design. They then returned and worked the queued
rulings, so this session's second entry is recorded as a POST-CLOSE ADDENDUM
rather than a second close -- the 2026-07-25/07-26 precedent, which states the
convention in terms: "The close bookend landed early by design; this addendum
records the work that followed it rather than re-opening the entry." Catching
that avoided leaving TWO close entries for one session. Addendum is 12 bullets
against the 15 cap.
LEDGER ROTATION (GA-R4 rule 3 / F1). The addendum took the live ledger to 321
lines against the 300 cap. One oldest-first pass left it at 307, still over, so a
SECOND pass was taken in the same close: both 2026-07-25 summaries (handoff-pack
execution, and MAAS admin-account recovery) moved VERBATIM to
docs/archive/session-ledger-rotated-20260727.md. Live ledger now 292 lines, five
entries, from the 2026-07-26 D-137 addendum onward.
SKILL SWEEP, done although NO STAGE CLOSED, on operator direction. Three new
invariants folded into SKILL.md, each earned this session:
- A FINDING IS AN OBSERVATION, NOT A CONCLUSION -- measure before putting it to
the operator. Four of the first six questions changed shape on measurement and
three were withdrawn or re-scoped.
- A CITATION IS AN EXISTENCE CLAIM; ONLY ITS CONTENT IS EVIDENCE -- both bugs
behind the v4-only lb-mgmt draft failed on reading and the conclusion inverted.
No gate reads prose, so nothing in a repo can catch this.
- RULED IS NOT BUILT -- D-134's bands and D-020's vault VIP were both properly
ruled and never implemented, and passed every gate for weeks.
Into references/script-authoring.md: the repo-lint L5 heading trap (a heading
LEADING with a D-number needs AMENDMENT or RESOLVED on the same line -- it bit
this session twice and was on no author-facing surface), the commit-gated-on-lint
pattern that then caught its second occurrence, and the space-aligned .tsv.
Into references/operating-discipline.md: the probe PATH determines the answer --
the office1 VMs read "unreachable" via voffice1 and NO-CLONE from vcloud, and only
the second is a finding.
Snapshot regenerated as openstack-cloud-ops-consolidated-20260727.md: 1695 lines,
ASCII/LF byte-verified, all five new items present. A stale snapshot is the
recorded failure mode of docs/audit/skill-divergence-20260725.md.
CHANGELOG extended with the full ruling table (14 ruled, 2 withdrawn, G18 opened)
and the sweep/skill sections, each carrying its revert.
PERMISSION BLOCK REMOVED as promised at grant time: allow 303 -> 248, ask 24 -> 7.
All 55 session-scoped allow rules deleted and the five original broad
`ssh voffice1 "maas admin <noun> *` ask rules restored in place of the 23
verb-scoped ones. JSON validated. The file is gitignored, so the scope doc's
section 6 and this changelog are its only durable record.
FINAL VERIFICATION: repo-lint 0 fail / 1 warn (the L1 legacy carve-out, expected);
gauntlet ALL GREEN (81 harnesses); ledger-scan reconciles at 21 open SEC rows and
next-free D 138 / DOCFIX 205 / BUNDLEFIX 053 -- UNCHANGED, correctly, because
fourteen rulings were recorded and nothing was remediated.
Revert: git revert this commit; the skill edits are additive, the snapshot is
derived, and the ledger rotation is reversible from the archive file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

close-out durability sweep: 4 transcript-only items landed; dc1-is-not-a-mirror warning
...
Operator-requested sweep for content that existed only in the session transcript, before
clearing. Precedent: docs/audit/queued-findings-20260726.txt. Capture:
docs/audit/queued-findings-20260727.txt.
THE CONSEQUENTIAL ONE. scripts/dc-mirror.sh's dc1 site row carried NO warning that dc1 is no
longer a mirror site, so `dc-mirror.sh install dc1` would have SILENTLY REBUILT everything the
same-day teardown removed -- units, nginx vhost, sync helper, and an ENABLED daily debmirror
timer that would start a fresh ~950G pull on a rack whose ruled artifact path is the proxy. That
is precisely the command someone would reach for after treating a failing `check dc1` as a
regression. The row now states dc1 is proxy-only per the D-135 amendment, that `check dc1` FAILS
BY DESIGN, and that `install dc1` is a deliberate strategy change and never a repair. The row is
RETAINED so its measured values survive a legitimate rebuild.
QUEUED, not built (hard rule 1): a RUNTIME guard in do_install for a site whose ruled path is
not the mirror. A comment is strictly weaker than a guard, and prose-only prevention has already
failed twice on this project (DoD item 8 missed dc1; D-137's wiring sat as prose and missed BOTH
DC standups). Recording that the interim is an interim.
A WRONG CLAIM CORRECTED. docs/audit/stage4-mirror-gate-20260727.txt said `systemctl reset-failed`
would re-arm the paused dc1 debmirror. It cannot: reset-failed on the SERVICE does not start an
INACTIVE TIMER. The real vector was a REBOOT -- `enabled` starts the timer at boot and
`Persistent=yes` fires it IMMEDIATELY because its window had passed. Correction APPENDED rather
than edited in place: the file is dated evidence, so a wrong analytical claim is superseded
openly, not quietly rewritten.
THE GENERALISABLE LESSON, previously unrecorded, is now platform-traps section 5:
5a "stopped is not dormant across a reboot" -- enabled + inactive + EMPTY NextElapse looks
harmless and is not; plus reset-failed does not re-arm, and Result=signal/status=15 on a
oneshot means it was SIGTERM'd and never wrote a terminal state (so a stale RUNNING is a
corpse, not progress).
5b a `oneshot` with RemainAfterExit=yes does NOT undo its work on stop, so stopping it proves
NOTHING about dependents -- which is exactly why the dc1 teardown test deleted the live
address and route instead of stopping the mirror net unit.
Three rows added to the verbatim-error index. Note dc0-mirror-sync.timer is still enabled with
Persistent=yes, so 5a describes dc0 today -- benign while its syncs succeed, same shape.
HARD RULE 2. dc-cache-proxy.sh stated that the two net units coexist safely on one host as
FACT. It is REASONED (idempotent replace verbs, identical measured values), never MEASURED --
the proxy has only run on dc1, where the mirror unit was then removed, so nobody has run both.
Marked accordingly, with an instruction to verify rather than trust the sentence.
VERIFICATION: dangling-reference sweep over docs/, runbooks/, scripts/ and the skill -- every
path this session introduced or cited RESOLVES; all pre-existing dangles are legitimate
(deleted-as-history, not-yet-built, or the deliberately-absent octavia PKI overlay preflight P4
fails on). ledger-scan reconciled against the machine-derived block. Gauntlet ALL GREEN (81),
repo-lint 0-fail, dc-mirror 24/24, dc-cache-proxy 20/20.
Revert: text + comments only; the capture correction is an append.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Stage 4 close-out set: GA-R2 consolidation, skill sweep, snapshot, GA-R7 review
...
Every substantive Stage 4 item is closed or split to gate row G17. This is the GA-R6 close-out
set; only the operator-gated merge to `main` remains.
GA-R2 CONSOLIDATION: the 7 stage changelogs archived to docs/archive/changelogs/ (top-level
docs/ 25 -> 18, 113 archived total) and the stage record written at
docs/archive/stage-records/vr1-stage4-record.md. It records what the stage actually delivered
(READY handoff, not deployed nodes), the three things a future session should read first, and
the false-green pattern this stage kept surfacing. 12 stale `docs/changelog-*` paths in live
surfaces rewritten to the archive -- including several already dangling from the G12 close, so
every changelog reference in CURRENT-STATE / session-ledger / security-ledger now resolves.
SKILL SWEEP: three new INVARIANTS folded in --
- per-DC ARTIFACT DELIVERY is a per-DC STRATEGY (dc0 full mirror / dc1 caching proxy, D-135
amendments); checking dc1 as a mirror fails BY DESIGN. Corollary, learned the hard way: a
per-DC utility service OWNS ITS OWN network prerequisites -- never let one strategy's
teardown be able to break the other's.
- Stage 4 hands off READY nodes, NOT deployed ones (DOCFIX-200). Any surface saying "nodes
Deployed" as a Stage-4 exit is a defect; four were found and fixed. Consequence: nothing
node-side is probeable in Stage 4, which is why G17 exists.
- "A CHECKER THAT CANNOT FAIL IS NOT A GATE", with the working rules that came out of two
measured false greens: assert on CONTENT not existence; an unrecognised state REFUSES;
"could not look" is never "nothing there"; ENUMERATE what exists rather than only auditing
what is declared, because a register cannot report the absence of a row it does not have;
and when a test asserts a literal finding string, REPLACE the assertion, never delete it.
Plus two routing rows (per-DC artifact checks; D-137 as the credential-lifecycle authority).
SNAPSHOT REGENERATED: .claude/skills/openstack-cloud-ops-consolidated-20260727.md (1589 lines,
ASCII + LF byte-verified, all 7 sections). The superseded 20260725 snapshot is REMOVED rather
than kept alongside -- a stale snapshot being uploaded is the exact failure
docs/audit/skill-divergence-20260725.md records (a failed clone + a mis-shaped handoff), and it
is a derived artifact regenerable from any commit.
GA-R7 MEMORY REVIEW: NO new memory written -- everything durable from this stage graduated to
the skill and the repo, which is the correct GA-R7 outcome, not an omission. Both existing
entries verified against the repo (the finding-class baseline they cite does exist) and extended
with the two reasoning traps this stage produced: the register cannot see a credential it has no
ROW for, so enumerate what exists; and diagnose a denied call by checking whether an existing
rule failed to MATCH before blaming the classifier (the measured 2026-07-26 root cause). Also
marked the ask-vs-allow precedence claim UNVERIFIED so no future session relies on it.
Noted, not actioned (hard rule 1): T24's finding-class baseline covers TIER 1 only, so tier-2/3
classes have no baselined red state and a future false green there would not be caught.
Gauntlet ALL GREEN (81), repo-lint 0-fail.
Revert: git revert this commit; the changelog moves are renames and the snapshot is derived.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
| 2026-07-25 |
skill: stage-close sweep keeps the authoritative dir current + regenerates the Chat snapshot
...
SKILL.md stage-close rule now states the authoritative, versioned skill is the
.claude/skills/openstack-cloud-ops/ dir (SKILL.md + references), kept current at each
stage-close sweep, and the dated single-file Chat-upload snapshot is REGENERATED from it
then (a derived artifact, never the source; the stale Chat mount is a known failure mode).
Regenerated openstack-cloud-ops-consolidated-20260725.md from the updated skill so the two
stay in sync. ASCII/LF verified; repo-lint 0 fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUjEb7onHpdqFHUnio6iVw
|
| 2026-07-23 |
G12 dc1 close-out: consolidation + skill sweep + CURRENT-STATE (G12 CLOSED)
...
- GA-R2 consolidation: 3 dc1 session changelogs (07-21 authoring, 07-22 build,
07-23 edge) archived to docs/archive/changelogs/; Stage 3 record gains a
"G12 CLOSED 2026-07-23" section + table rows
- CURRENT-STATE: G12 gate CLOSED, section-1 stage narrative updated
- skill sweep: per-DC dedicated MAAS->libvirt power key (SEC-012/SEC-016)
added as a standing DC-standup invariant
- GA-R7 memory review: clean (no changes)
Close-out set complete; ready for operator-gated merge to main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUjEb7onHpdqFHUnio6iVw
|
| 2026-07-21 |
STAGE 3 CLOSE (operator-ruled): GA-R2 consolidation + skill sweep; gauntlet 76 ALL GREEN
...
- Four execution-session changelogs -> docs/archive/changelogs/;
vr1-stage3-record.md finalized (close summary + session table)
- CURRENT-STATE stage line CLOSED (vr1-dc0 scope; dc1 = HELD G12);
workflow Stage-3 State row + runbooks/README corrected
- Skill sweep: D-131 forwarder invariant + MAAS pod traps + merge
precedent folded; no stale facts found
- Close-out evidence: gauntlet 76 ALL GREEN, repo-lint 0 fail, GA-R7
memory review done (changelog items 11, 17)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013XRtm5sDQgUsZyTJk3k7dz
|
Incident docs: 2 appendix-A entries, platform-traps MAC-regen corollary, LP draft
...
Closes the remaining documentation queue from the 2026-07-21
commissioning incident. Appendix-A gains the MAC-drift and
agent-resolver symptoms (verbatim, with checks + recorded fixes);
platform-traps 1e gains the unpinned-MAC regeneration corollary + index
row; LP draft ready for the operator to file (sanitization warning
included). Still queued: stale pod cleanup at stage close.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STHXiHfoxHqq8fGRVvb66G
|
| 2026-07-19 |
Batch 5: skill sweep -- GA-R1..R7 implemented in openstack-cloud-ops (single commit)
...
New GA-rulings section reconciled against the RATIFIED text (draft
skill-additions cross-checked; the one draft-only item, the
reconciliation circuit-breaker, kept and marked as discipline). Stale
facts demoted to CURRENT-STATE delegation: the header's "current phase =
VR0 testcloud" and the deploy loop's "opentofu UNVALIDATED / no tofu
binary has ever run" (false since 2026-07-13). Session bookends + stage
close rewritten per GA-R4/GA-R6; change-delivery per GA-R2/D1; routing
rows re-pointed. Read-through checklist committed:
docs/audit/skill-sweep-checklist-20260719.md (every ratified rule ->
disposition). G3 cell updated same commit. repo-lint 0 fail.
Revert: git revert this commit (restores the pre-sweep skill).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZbUeRE8weySLSGY1odrCi
|

Batch 4 C6: docs disposition -- 24 history docs archived, 16 retained, refs rewritten
...
DISPOSITION MANIFEST (archive, one-line justifications): clientdocs-
workflow-review (v1 tenant-docs era) / D-057-DECIDED-append +
D-057-REVIEW-ITEMS + README-D057-PACK + D-058-renumber (executed v1
decision packs) / D-068-openbao-assessment-DRAFT (supplementary; the
cited analysis retained) / DOCFIX-064-phase08-changelist + docfix-draft-
20260702 (executed/draft) / handoff-20260703 + handoff-20260705
(superseded handoffs) / incident-20260712-triplefault (trap routed to
platform-traps 1b/1c long ago) / netbox-vip-queue + phase-00-maas-
standup-notes + script-quality-findings + session-findings-2026-07-02 +
tenant-cidr-overlap-PLAN + v1-pre-deploy-fixes (v1-era executed/dated) /
repo-lint-nextfree-bug-FINDING (absorbed DOCFIX-105/107; repo_lint.py
pointer updated) / upstream-bug-draft-dashboard-tls (draft, dated) /
model-a-fallback-plan (D-123 Model B adopted; fallback preserved in
archive) / stage3-adversarial-review-20260716 (R3-F register,
dispositioned in 2.10) / dc-dc-replication-DR-seed (absorbed per
workflow) / dc-dc-ipv6-charm-research + dc-dc-netem-and-ula-gua-proposal
(stage 4+ research, refs updated). RETAINED 16 top-level, justified in
the session changelog. Live-surface refs to all moved files rewritten to
archive paths (18 files). G3 cell + ledger + changelog updated same
commit (the L10 fire on a Status-line path rewrite is satisfied by the
CURRENT-STATE touch -- the check worked as designed).
Revert: git revert this commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZbUeRE8weySLSGY1odrCi
|

Batch 4 C1: GA-R4 rule-6 ledger rotation -- 1187 -> 131 lines (GA-F13)
...
Bodies rotated VERBATIM to docs/archive/session-ledger-rotated-20260719.md
(Live state 2026-07-13, OPEN WORK VR1+VR0, NetBox write-path bugs, PINNED
rulings, standing lessons, state facts, project-completion, NetBox import
narrative). Routed-before-rotating (durable homes): juju format=line 3.6
trap -> appendix-A; guards-are-load-bearing -> operating-discipline; VR0
operational facts + admin/controller -> maas-as-built-reference.md
(lessons 1-6 verified already covered by platform-traps/L8). Live ledger
keeps header + machine-derived fence + two compact still-live blocks
(NetBox write-path bugs pointer, project-completion) + the bounded session
summaries. 131 < 300: the F1 cap is now ENFORCEABLE. Fences OK; ledger-
scan counters unchanged; repo-lint 0 fail.
Revert: git revert this commit (restores the 1187-line ledger).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZbUeRE8weySLSGY1odrCi
|

Batch 3 CLOSE: memory hygiene (GA-F05..F08 memory surfaces) + G1 review; GA-F16 registered
...
Per the recorded provenance rulings: review-after-each-step.md REMOVED
(GA-F06 -- one-time instruction, never standing) and project-posture-
closed-test.md REMOVED (GA-F07 -- "ruling" never ruled; posture NEUTRAL);
hostname -> vcloud in the Remote Control memory (GA-F05 last surface);
MEMORY.md index rebuilt (GA-F08). GA-R7 rule-6 skill line added to
SKILL.md bootstrap. First full G1 review (rules 1-4) found + fixed one
further violation: creds-folder-convention.md duplicated a repo-carried
convention and linked both removed files -- registered as GA-F16, reduced
to a pointer entry. Post-review: 3 memory files, zero violations, zero
stale refs (grep-verified). Pre-edit copies: docs/archive/memory-20260718/
(index copy ASCII-normalized, noted in the changelog). G3 cell updated
same commit (L10 satisfied).
Revert: restore files from docs/archive/memory-20260718/ (memory side);
git revert this commit (repo side).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZbUeRE8weySLSGY1odrCi
|
Batch 2 CLOSE: verification + adjudication recorded; last GA-F03 site fixed
...
Gauntlet ALL GREEN (70); repo-lint 0 fail / 1 legacy WARN (L10 at FAIL).
platform-traps:19 tofu pin (the fifth GA-F03 site) demoted; CURRENT-
STATE's known-stale-pins paragraph rotated (all sites fixed, memory copy
= Batch 3). G3 cell: Batches 0-2 CLOSED. Extractor adjudication + the
criterion-mechanics FINDING recorded in the session changelog (live-
surface vocab flags 0; residual groups history-class or mechanical;
literal ZERO-on-counted lands with Batch 4 + Batch 6.1 by construction).
Revert: git revert this commit (docs + one skill row).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZbUeRE8weySLSGY1odrCi
|

GA-F11/GA-F12: version rows + broken refs on live surfaces adjudicated (2.9)
...
Version groups (juju/lxd/maas/netbox): the single live-surface claim row
(skill environment.md:29 "Juju 3.6, MAAS 3.7.2") demoted to the two pin
authorities (appendix-B for VR0, CURRENT-STATE section 7 for VR1). The
remaining group members are extractor false positives (step numbers
"2.1"/"3.1", charm channel "2024.1", the LXD ">= 6.7" incompatibility
THRESHOLD) or history-class rows (register/ledger/changelogs -- Batch 4).
Broken path refs: 192 non-history rows adjudicated -- ALL false positive
(provider-repo doc paths in skill references, glob/range forms like
phase-00..08, slash-phrases like "netbox/tailscale", wrapped names) or
refs inside VR0-era history docs whose routing is Batch 4; zero real
broken refs on live operational surfaces (spot-checks in the session
changelog). Batch 2 item 2.9.
Revert: git revert this commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZbUeRE8weySLSGY1odrCi
|
GA-F04: skill OPNsense 26.1 rows repointed/requalified (live edge is 26.7)
...
platform-traps: pin-table row points at CURRENT-STATE section 7; section-4
header + fresh-image prose requalified as traps that ENTERED at 26.1 and
persist on later releases (all three traps re-verified applicable to 26.7
per the 2026-07-18 D-129 review). environment.md drops both its OPNsense
pin AND a discovered stale tofu-1.12.3 pin (GA-F03 class, same surface)
for the CURRENT-STATE pointer. Batch 2 item 2.3.
Revert: git revert this commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZbUeRE8weySLSGY1odrCi
|
GA-F05: jumphost name corrected to vcloud on always-loaded surfaces
...
CLAUDE.md:4 + SKILL.md:46 per the finding, plus a fourth surface
discovered in the same sweep: references/environment.md:43 (same defect
class, same measured fact -- hostname -> vcloud, env snapshot). The
memory-file instance is Batch 3's item per the plan. Batch 2 item 2.4.
Revert: git revert this commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZbUeRE8weySLSGY1odrCi
|
| 2026-07-17 |
Gap register #20: durable host->site reachability CLOSED (Office1); skill carries the per-DC discipline
...
- docs/dc-dc-deployment-workflow.md: new tooling-gap item 20 -- the durable
host->isolated-site base leg had no owning tool; CLOSED for Office1 via
scripts/site-baseleg.sh (installed+enabled 2026-07-17; boot-race unproven until
a reboot). Documents the DC generalization so the gap does not reopen per-DC.
- SKILL.md: operating-model invariant extended -- durable host->site reach is
standing per-SITE tooling (DCs included): at each DC standup MEASURE the reach
and, if the DC net is ISOLATED, add a MEASURED site-baseleg.sh leg row + install
as part of standup DoD (never an inferred DC IP; harness enforces it).
Doc-only follow-up to 1958063. Nothing merged to main (stage-close).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMKdXCquMnh1jKH2sWXPMg
|

D-126 base-leg oneshot (site-baseleg.sh) + D-128 operating model; post-restart sweep
...
vcloud post-restart health sweep all-green (host on 6.8.0-136, patch+reboot done;
libvirt/pools/autostart clean; Office1 stack self-recovered end-to-end). Sweep
surfaced a recurring break: office1-local is an ISOLATED libvirt net, so the host's
base L3 leg (10.10.0.10/24 on the bridge + compose route 10.10.1.0/24 via 10.10.0.20)
lives nowhere in the XML and drops on every reboot -- the un-persisted foundation
D-126 assumed but never owned.
- NEW scripts/site-baseleg.sh + tests/site-baseleg/ (24/24): root Type=oneshot unit,
After=libvirtd (+virtnetworkd for modular hosts; ordering MEASURED here = monolithic
libvirtd), re-adds the leg at boot. Bridge DISCOVERED from the stable net name (never
a baked virbrN, PATTERN-1); apply idempotent + root-guarded; check = session-verify.
INSTALLED + enabled this session; boot-race UNPROVEN until an actual reboot.
- docs/design-decisions.md: D-126 amendment (base-leg durability) + NEW D-128 (operating
model -- Claude on the vcloud jumphost; Plane-1 substrate on vcloud / Plane-2 executes
on voffice1; workstation tailnet is the human path; D-107 untouched).
- Skill invariant + routing row (SKILL.md), Cross-cutting bullet (dc-dc-deployment-workflow.md),
session-ledger + changelog.
Gauntlet ALL GREEN (67 harnesses), repo-lint 0-fail. Nothing merged to main (stage-close).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMKdXCquMnh1jKH2sWXPMg
|
| 2026-07-16 |

D-125: Model B per-DC ISP egress (bridge-in, single-NAT) -- resolves OBS-3 design gap
...
OBS-3: Model B nesting put vr1-dc0-wan (a libvirt NAT) inside vvr1-dc0, whose
only leg is the SEC-010 FORWARD-dropped transit -- so the per-DC simulated ISP
had no egress. Operator ruled bridge-in (single-NAT) over double-NAT.
- modules/wan-bridge (NEW): libvirt forward=bridge onto a pre-existing host
bridge (bridge={name}, schema measured from the provider, not guessed). No
subnet/NAT. Sibling to site-wan so every NAT user stays untouched. Covered by
opentofu-validate S3 (standalone PASS).
- OUTER root: vcloud ISP NAT vr1_dc0_uplink (the single NAT) + a 2nd IP-less
uplink NIC on vvr1-dc0 + the br-vr1-dc0-wan bridge declared in netplan (transit
stays NIC1 so SEC-010/--transit-if keep keying on it). New HELD var
vr1_dc0_uplink_cidr (no default; office1-netbox apex, hard rule 2).
- INNER root: vr1_dc0_wan site-wan(NAT) -> wan-bridge onto br-vr1-dc0-wan;
OPNsense WAN re-addresses into the vcloud ISP /24 (edge config, D-113, HELD).
- Bootstrap (site-headend-install.sh): --uplink-if/--wan-bridge; --check verifies
the bridge exists + uplink ENSLAVED (fail-open guard, same class as the transit
check); SEC-010 rationale corrected (bridge-in has no inner NAT) + br_netfilter
CONSTRAINT (keep the FORWARD-drop interface-scoped, NEVER global). Harness 49->54.
Records: D-125 (new); D-122 amendment (forward-pointer -- WAN realization
superseded, intent stands); SEC-010 (br_netfilter + hardened --check);
model-a-fallback B->A delta item 8 + revert; platform-traps 1g (bridge +
br_netfilter + nested-foreign-MAC trap).
NOT closed: the L2-NAT-of-foreign-MAC path is a DEPLOY-TIME gate (bridge-in
equivalent of the depth-4 boot gate); fallback = double-NAT. HELD: vr1_dc0_uplink_cidr
+ OPNsense WAN re-address. Both roots validate; gauntlet ALL GREEN (63); repo-lint
0 fail. Present-only; NOT pushed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ck6xh3jWQi5b3Su8Dx1LEH
|
| 2026-07-15 |
stage-close discipline: add the operating-skill sweep as part of definition-of-done
...
Per operator: the skill-update sweep must itself be part of every stage close, so it
is never skipped. At stage close (alongside the branch->main merge + branch retire),
sweep .claude/skills/openstack-cloud-ops/ for any new INVARIANT the stage established
and reconcile divergence from repo HEAD. Recorded in both the workflow doc's
Cross-cutting discipline and the skill's own VR1 deploy-loop stage-close note.
repo-lint 0 fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015WgVrZE2ryY9aXP7JiBuwj
|
skill: add the standing merge-at-close discipline + the DOCFIX-195 apex pointer
...
The operating skill delegates facts (stage status, IPs) to the repo by design, so
Stage-2-merged/D-120/etc. need no skill edit. But two things are the skill's own
domain and were missing:
- The new INVARIANT: branch-per-stage + MERGE AT STAGE CLOSE (merge commit,
operator-gated, retire the branch, next stage off post-merge main). Added to the
VR1 deploy loop as part of a stage's definition-of-done. Precedent: Stage 2 -> PR #1.
- A DOCFIX-195 pointer on the IPAM routing row: during VR1 the WORKING apex is
office1-netbox (netbox.baldurkeep.com is a read-only v1 reference, write-back
deferred). This exact ambiguity cost time early this session.
repo-lint 0 fail (L8 guide<->skill coupling pins clientdocs, not this skill -- unaffected).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015WgVrZE2ryY9aXP7JiBuwj
|
| 2026-07-14 |

D-117 (PROPOSED): VR1 site naming collision blocks the NetBox import
...
Measured live against the apex: prod NetBox binds 2602:f3e2:f02::/48 -> site
VR1 DC0, but dc-dc-prefixes-import.py maps --dc dc1 -> slug vr1-dc1 and treats
f02::/48 as dc1. The addressing agrees; the labels are off by one. Because that
importer has NO --commit (it WRITES BY DEFAULT), running it as documented against
a prod-seeded NetBox would silently bind DC1's prefixes to the other DC's site.
This is the known G5 gap, promoted to a decision because it blocks the import.
Options A/B/C recorded; A (rename the two skeletal apex sites) recommended, with
the slug-collision rename ORDER called out as a trap.
Also records the upstream-NetBox User-Agent WAF trap in platform-traps.md: the
same token that works from curl gets 403 from urllib/requests/pynetbox. It will
bite pynetbox and looks exactly like an auth failure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZBxzAbmqwLW7jEmMG23Xw
|
| 2026-07-13 |

Office1 headend LIVE: MAAS 3.7.2 + LXD 5.21 on voffice1. D-114 PROVEN END TO END.
...
MAAS composed, PXE-booted and COMMISSIONED its first VM inside voffice1. The model works.
vcloud (L1 -- itself a KVM guest, measured)
+- voffice1 (L2) MAAS 3.7.2 region+rack + PostgreSQL 16.14 + LXD 5.21.5
+- office1-netbox (L3) LXD VIRTUAL-MACHINE, MAAS-composed, MAAS-PXE-booted,
leased 10.10.1.100 from MAAS, status READY, 2c/4096MiB detected
THE L3 PROOF IS NOW DEFINITIVE. Earlier today we could only say nested KVM was AVAILABLE
(/dev/kvm + svm present). Now a guest has actually BOOTED, PXE'd, COMMISSIONED and reported its
hardware three levels deep. D-114's Office1 gate is CLEARED. This reproduces VR0's `lxd` +
`tailscale` pattern exactly -- and the Juju-created LXD CONTAINERS remain invisible to MAAS, as in VR0.
THE DHCP SPLIT IS STRUCTURAL, NOT CONVENTIONAL:
10.10.0.0/24 (site LAN) -> Kea on the OPNsense edge MAAS dhcp_on=False
10.10.1.0/24 (lxdbr0) -> MAAS MAAS dhcp_on=True
Separate L2s; lxdbr0 is never bridged onto the site LAN, so they cannot see each other. The
installer asserts after configuring DHCP that MAAS owns NO other subnet, and hard-fails otherwise.
NEW: scripts/site-headend-install.sh + tests/site-headend-install/ (18/18 PASS). It is the sequence
we ACTUALLY EXECUTED, codified -- not written from docs -- and reusable verbatim for DC1/DC2. Its
--check mode was run against the live voffice1 and reports every item OK.
FOUR TRAPS, ALL HIT FOR REAL TODAY, ALL NOW ENCODED AND HARNESS-ASSERTED:
1. `lxd init --auto` FAILS on a MAAS host. It brings up lxdbr0 with LXD's own DHCP+DNS, which starts
a dnsmasq; dnsmasq binds the WILDCARD 0.0.0.0:53 and MAAS's bind9 already holds :53. The error
names the BRIDGE address ("failed to create listening socket for 10.10.1.1"), which sends you
hunting the wrong thing. `ipv4.dhcp=false` + `dns.mode=none` are NOT enough -- LXD still spawns
dnsmasq. The fix is raw.dnsmasq="port=0". Verified after: dnsmasq holds ZERO :53 and ZERO :67.
2. `lxc` READS CONFIG FROM STDIN. Piped to `bash -s` over ssh, the first unredirected `lxc` call
EATS THE REST OF THE SCRIPT as YAML and everything after it silently vanishes. Every lxc call now
ends in </dev/null. Same class as the OPNsense tcsh trap.
3. LXD snap track changes are ONE-WAY -- install DIRECTLY onto 5.21/stable, never via `latest`.
MAAS 3.6/3.7 is incompatible with LXD >= 6.7. (Bonus: core.trust_password exists in 5.21, gone in
6.x -- the pin also preserves the scriptable registration path.)
4. MAAS DHCP on the compose network ONLY. --compose-cidr is REQUIRED with no default so it can never
be picked by accident.
DISCIPLINE: every MAAS/LXD flag was read from the tool's own --help ON THE BOX before use (maas init,
createadmin, vm-host compose), not trusted from docs. All secrets (DB, admin, API key, LXD trust) are
generated on the target, stored 0600 under /root/maas-secrets/, and never printed into the session.
10.10.1.0/24 IS AN UNBLESSED ALLOCATION -- chosen deliberately (DC1=10.12.x, DC2=10.13.0.0/19, tenant
pool=10.20.0.0/16 per D-016; the obvious 10.20.x would have COLLIDED with tenant space), but NetBox
has not assigned it. Register once NetBox exists -- it is one of the VMs this headend composes.
Also: the ops skill is now tuned to this exact stack (new references/platform-traps.md with a
verbatim-error -> cause index; the OPNsense section that still routed sessions into the DELETED
config-ISO path is replaced). Upstream corroboration found for two of our own findings: the libvirt
provider's release notes document that an "in-place" domain update undefines and re-defines the
domain (our measured guest-bounce), and there is an upstream issue for the AppArmor denial.
Gauntlet ALL GREEN (53 harnesses). repo-lint 0 fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|