| 2026-08-03 |

reconcile provider-bundle-check harness to D-141 (sweep F1 CLOSED)
...
The 2026-08-03 deploy session reverted the dc0 VIP overlay to IPv4-only
under D-141 (3e691cd) but shipped it without the companion harness update,
leaving run-tests-all RED at 1/98 (provider-bundle-check, 4 dual-family
cases T19/T21/T25/T45 asserting a shape the deploy input no longer has).
Reconcile by re-pointing, never deleting (the checker's dual-family path is
still live code and D-141 rule-3 promotes v6 later):
- new synthetic dual.yaml fixture on the MEASURED all-GUA legs of the
pre-revert deploy input (3e691cd^), present in the current apex -- NOT the
stale DUAL6 ULA constant D-139 deprecated
- T19 assertion REPLACED with the v4-only invariant (0 dual-family)
- T21/T25/T45 re-pointed to dual.yaml (dual-family PASS + apex-refusal
controls preserved)
- corrected the now-vacuous v4only.yaml comment
scripts/provider-bundle-check.py is UNCHANGED -- it is family-agnostic (a
vip is a v4 triple OR a dual-family sextet); this is a harness reconcile only.
Verified: provider-bundle-check 55/55 ALL PASS (count unchanged 55->55, pure
re-point); new T19 failing-direction proven; full gauntlet GAUNTLET: ALL
GREEN (98 harnesses) (was 1/98 FAIL); repo-lint 0 fail / 1 legacy warn.
CURRENT-STATE updated same commit (GA-R1 C1). Three follow-ons LOGGED to the
close sweep (FN1 stale DUAL6 ULA; FN2 generated overlay header; FN3 dual.yaml
builder can silently no-op at promotion).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
| 2026-08-02 |

retire-v6-ula harness + the defect it found in my own by-hand check
...
Operator direction: "Proceed with 1 and 2". Item 2 (the repo tool for the MAAS half) is
delivered. Item 1 (the held ULA subnet) is NOT executed, and the tool is why.
THE TOOL'S FIRST LIVE RUN CAUGHT A DEFECT IN THE BY-HAND CHECK USED AN HOUR EARLIER, and
that is the more important finding here. It reported `interface links: subtle-grouse:enp1s0`
on subnet 2. My by-hand check had reported ZERO links for all five subnets, because the
subnet id was interpolated as sid="'$id'" -- the comparison ran against "'2'" and could
NEVER match. A checker that could not fail, written in the same session that landed exactly
that rule into script-authoring.md.
CONSEQUENCE: the earlier claim that the four deletes were "proven safe twice" was HALF
FALSE. The allocation count was sound; the link half was inert, so link status was UNKNOWN
at the time of those deletions.
THE OUTCOME WAS SAFE ANYWAY, MEASURED NOT ASSUMED: all nine role nodes read v4=6 v6=6, so
every v6 link they carry is GUA and the four deleted ULA subnets genuinely held none.
enp2s0 unlinked on all nine is EXPECTED (provider-public carries no L3 link; the static
lands on br-ex, D-133/D-100). subtle-grouse (the controller, Deployed) reads v4=2 v6=1, its
single v6 link being the ULA ::5 -- independently confirming the hold was correct. Right
outcome, unsound verification; recorded as two separate facts.
The mode lives in dc-plane-ipam.sh rather than a new script because dc-region-topology.sh's
header already assigns v6 plane subnets to it. It refuses on an allocation OR an interface
link, NAMES the holder, deletes one at a time (never looped, hard rule 3), and READS BACK
that the row is gone -- R6 proves that assertion has teeth by faking a delete that returns
success and leaves the row.
Harness 25 -> 32 (R1-R7), delta checked. R4 is the case that matters: an interface link
with NO allocation still refuses -- the exact blind spot the broken by-hand check had.
ITEM 1 NOT EXECUTED: retire-v6-ula REFUSES on fd50:840e:74e2:220::/64, naming ::5
(juju-vr1-dc0), ::6 (MAAS) and the link subtle-grouse:enp1s0. Deleting it needs the juju
controller's and region's v6 re-homed to GUA first -- re-addressing the DEPLOY CLIENT
immediately before the deploy, and this project has already lost three bootstrap attempts
to under-carved controller networking. Not covered by an approval given about four empty
subnets, and NOT deploy-blocking: an extra ULA subnet in MAAS harms nothing.
Gates: gauntlet ALL GREEN (98); repo-lint 0 fail; dc-plane-ipam 32/32.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

D-139 step 6 tool built + adversarially reviewed; 4 defects fixed. Apex NOT yet written.
...
Implements the two 2026-08-02 rulings ("Full step 6 first, then deploy" / "Deprecate both,
delete nothing"): CREATE 26 GUA VIP addresses -> read-back verify -> deprecate 26 ULA
addresses + 9 ULA prefixes. NO delete path anywhere, asserted against the artifact.
Dry run (live apex): CREATE 26 | ALREADY 0 | DEPRECATE-ADDR 26 | DEPRECATE-PFX 9. The 26
CREATE targets diff EXACTLY against the 26 GUA VIP legs in overlays/vr1-dc0-vips.yaml --
every address written is one the deploy configures. --commit has NOT been run.
AN ADVERSARIAL REVIEW RETURNED "FIX FIRST" AND WAS RIGHT ON ALL FOUR COUNTS
(docs/audit/d139-step6-tool-review-20260802.txt). Mapping logic was correct; the gaps were
preconditions and coverage.
DEF-1 CRITICAL -- --dc vr1-dc1 would ORPHAN-CREATE. Targets were computed arithmetically
and never checked to exist. Measured: dc1's GUA carve is incomplete (four provider-public
rows under 2602:f3e2:f03::/48, no :20::/64, no :21::/64), so dc1 planned 26 creates into
non-existent prefixes then deprecated dc1's only authoritative rows, rc=0, no warning. dc0
hid it because all sixteen of its targets happen to exist. Reachable via the other valid
value of a required flag. FIXED + verified live: dc1 refuses, dc0 unchanged at 26/26/9.
DEF-2 CRITICAL -- the apex-IDENTITY guard was gone. It lives in d139-gua-carve.py's main()
(:159-163) and importing a module never runs its main(), so subclassing C.NB inherited the
TRANSPORT and left the SAFETY POSTURE behind: netbox.baldurkeep.com (the v1 reference)
would have connected fine and taken writes. FIXED: identity checked before any network call.
DEF-3 HIGH -- silent under-count. One missing ULA /64 row gave CREATE=13/DEPA=13/DEPP=8 at
exit 0. FIXED: any ULA address claimed by no prefix row refuses. My first fix was itself
wrong and RUNNING it caught that -- it scanned the whole retired /48 and flagged dc1's 26
VIPs while planning dc0; the /48 is SHARED (dc0 :22x, dc1 :32x). A /60 parent deliberately
does not count as coverage: the reviewer's scenario was a missing /64 whose /60 survived.
DEF-4 HIGH -- main() had ZERO coverage; the reviewer hoisted the deprecate loops above the
create phase and the suite reported ALL PASS. FIXED: T16-T18 drive main() through a fake
client that records CALL ORDER, proven by re-running that exact mutation on a copy (T18
goes RED).
TWO OF MY ASSERTIONS COULD NOT FAIL and the review killed both. T13 asserted the ABSENCE of
a string, so a traceback satisfied it -- it passed against a tool file that did not parse;
it now requires a positive, well-formed, DIFFERENT target, and new T15 asserts the tool
parses. T14 grepped ONE file, so adding a delete to the IMPORTED d139-gua-carve.py left it
green; it now covers both.
Corrected in the ruling record (GA-R1 C2): the amendment said the GUA records would be
status=active. Measured: the live ULA VIP records are "reserved", and
dc-plane-apex-import.py:186,200 creates addresses reserved. Also corrected my own
docstring overclaim -- the 26+9 deprecations are reversible, the 26 CREATES are not.
OPEN SCOPE QUESTION, MEASURED, not a tool defect: D-139 says retire the ULA rows "in the
apex AND in MAAS"; this tool is apex-only, so step 6 is NOT complete when it finishes. MAAS
on the dc0 region holds five ULA /64s beside six GUA; four are empty but
fd50:840e:74e2:220::/64 still holds 2 allocated entries. MAAS has no deprecated status for
a subnet, so delete-or-leave is a separate operator decision.
Gates: harness 20/20 (was 14, delta = the 6 cases added); gauntlet ALL GREEN (98, manifest
recorded deliberately 97 -> 98); repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

dc0 edge REBUILT, egress restored (8/8); SEC-032; two dc-egress-check defects fixed
...
Agent-executed rebuild. tofu -replace asserted on `tofu show -json`: exactly 2 non-no-op
changes, both delete,create, both naming vr1_dc0_opnsense, 0 not naming it -- and the
POSITIVE half asserted too, because a plan replacing only the DOMAIN would have reattached
the corrupt qcow2 and passed a "nothing extra" check. Artifact verified, not the log.
Convergence re-plan clean. pfctl -s nat explicitly verified to show real outbound NAT --
the exact defect dc1 is sitting in.
MY BRIEF CARRIED A WRONG BASE-IMAGE PATH. I cited the module default; d124-inner.auto.tfvars
overrides it to a path on voffice1. The apply destroys the volume before recreating, and
neither edge disk has a backingStore, so a wrong path would have left no disk and no
rollback. The agent verified before mutating. Same guess-instead-of-look failure the
operator called out earlier today, in a brief written to prevent exactly that.
dc-egress-check: A4's snap probe omitted `Snap-Device-Series: 16` (400 without it through
the proxy AND direct; 200 with) -- a false FAIL on a healthy proxy. A3 now names the
forwards-without-translating signature when A2 passed. Harness 14 -> 16; T15 asserts on
the CODE after a first cut passed on the comment explaining the header.
Three findings needing attention pre-Stage-5: the rebuilt edge runs dnsmasq on udp4 *:67
over vtnet0 with a stale 192.168.1.x range on the segment carrying juju .5 and MAAS .6
(measured; inertness reasoned, not measured); SEC-021(a) answered -- the off-jumphost copy
exists; and a rebuilt edge invalidates the rack's known_hosts, which no runbook mentions.
Gauntlet ALL GREEN (97); repo-lint 0 fail / 1 warn. Open SEC 28.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Close both egress gaps: dc-egress-check.sh + preflight P9, wired into restart + phase-4
...
Gap 1: nothing tested DC egress. dc-rack-net.sh check PASSED through a 19-hour outage
because it asserts legs and units -- a leg is not a path -- and the mirror answered 200
from its own nginx, which says nothing about upstream.
Gap 2: the post-reboot check set was my judgement, and it omitted egress.
New scripts/dc-egress-check.sh: site-keyed, runs on the rack, LAYERED (route -> edge
answers -> traffic leaves, ICMP and TCP -> the three upstreams), reporting the FIRST
failure as the cause while the rest SKIP. A4 asserts the UPSTREAM the local artifact
services sync FROM, which is independent of whether they serve.
Proven live on two different failure modes: dc0 fails at A2 naming the dead edge; dc1
passes A2 and fails A3/A4 -- traffic not leaving a healthy edge.
preflight P9 WARNs off-rack with the exact command, FAILs on the rack when broken, WARNs
if the checker is absent. Gap 2 is closed by the WIRING: restart procedure Stage 0
(before anything fetches) and phase-4 Step 3.9 (before the deploy), both stating P9 is
not a substitute for running it on the rack.
Mutation pass caught a decorative test of mine: T11 never reached A4's unrecognised-code
branch, so flipping it to ok() left the suite green. T14 added to exercise A4 alone.
tests/dc-egress-check 14/14; tests/preflight 39 -> 43/43; 5 mutations across both, each
killing a named case, scripts restored sha256-identical. Gauntlet ALL GREEN (97),
manifest 96 -> 97; repo-lint 0 fail / 1 warn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

dc-node-v6-carve: v6 FAMILY guard + a `replace` action for the D-139 step-3 migration
...
Two defects and one gap, all on the critical path.
FAMILY GUARD. v6_by_vlan[vid] = s was last-writer-wins. Once step 2 put a GUA /64
alongside every ULA /64 on the same vlan, that decided the family of all 54 node statics
by MAAS's JSON array order. Measured live: it picked GUA -- luck, not a contract, and the
FOURTH copy of the defect the 2026-07-29 chain audit found in the apex readers. Now keyed
by (vlan, family): --v6-family selects, ambiguity without it REFUSES naming the flag, and
two subnets of the SAME family on one vlan still refuse.
REPLACE ACTION. The nodes hold pre-D-139 ULA statics and `apply` correctly refuses to add
a second address -- two globals on a NIC break G19's sole-global predicate, which has no
exemption. `replace` unlinks the old-family link then links the new one, per interface,
with read-back. It refuses a same-family change (that is an address edit, not a
migration), refuses when a NIC carries more than one v6, and refuses when the old link
carries no link id -- without an id there is no unlink, and falling through would ADD a
second global. Unlink failure does NOT attempt the link: a half-done interface is worse
than an untouched one. Real MAAS links were measured to carry ids (326, 410).
GAP FOUND BY WRITING THE TEST: `want in have` short-circuited to skipped++, so a NIC
carrying the wanted address AND a stray second global counted as "already correct" --
silently masking exactly the half-done migration G19 forbids. Now an error. F8's
assertion was REPLACED with that new invariant, not deleted, and F9 added to cover the
refusal path F8 was originally written for.
Harness 14 -> 23. Three mutations each kill named cases; restored sha256-identical.
Gauntlet ALL GREEN (96); repo-lint 0 fail / 1 warn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
| 2026-08-01 |

D-139 step 2 (input half): VIP overlay onto GUA; both apex readers learn v6 FAMILY
...
overlays/vr1-dc0-vips.yaml is now fully GUA -- 39/39 v6 VIP legs, zero fd50:. The
renderer reproduces it byte-for-byte and provider-bundle-check PASSES with 13
dual-family VIPs.
Both apex readers had to learn family first. Step 1 creates GUA alongside ULA and step
6 retires ULA, so mid-transition every v6-only plane has two /64s under one (role,kind)
key and both readers could only REFUSE -- measured live: derive --dual-family rc 2, and
the gate "cannot evaluate barbican's dual-family vip". render-dc-overlays gains
--v6-family (refuse on ambiguity kept); provider-bundle-check resolves per application
AND per leg, needing no flag at any call site.
A lossy path was found and NOT taken: a full derive drops the 13 per-app comment fields
(4368 -> 3593 bytes). The values file was edited surgically instead: 26 changed overlay
lines, every comment intact. derive being lossy against its own values file is logged.
PROPERTY TRADED, recorded as a loss not a win: the gate no longer catches a wrong-FAMILY
leg -- a ULA leg in the GUA dc0 overlay now PASSES, graded against the ULA band that
exists until step 6. Necessary (dc1 is legitimately ULA) but it leaves D-139 family
conformance checked by nothing. A ruled-table conformance gate is OWED.
Two bugs I introduced were caught by the harness, not review: family inferred once from
the provider leg (GUA in both worlds -- 7 dc1 cases red), then bands resolved once per
bundle instead of per application.
tests/render-dc-overlays 18 -> 23/23 (3 mutations, restore sha256-identical);
tests/provider-bundle-check 55/55; gauntlet ALL GREEN (96); repo-lint 0 fail / 1 warn.
vr1-dc1 untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

d139-gua-carve: add the OOB plane (:f0::/60 + :f0::/64) per the 2026-08-01 ruling
...
CARVE gains (0xf0, "oob", "oob", True). It was absent because design-decisions.md:3066
declared lbaas-mgmt(:80), vpn(:e0) and oob(:f0) out of scope of the six-plane tool;
D-139 brought :80 back and left the other two, so the carve was incomplete against the
org standard ruling B cites as its reason. VR0 DC0 and Willamette both hold :f0::/60 +
:f0::/64 with role oob, so the v6 side CONFORMS. VPN (:e0) is the same omission and is
deliberately NOT added -- it was never ruled.
Harness 71 -> 75. Count literals REPLACED, not deleted, per the standing rule: T07 and
T12 11 -> 13, T17's three commit-path tallies 11 -> 13, and T08/T12 gained the two OOB
CIDR+role rows. stub-apex ROLES gains "oob" so the role-existence check resolves.
Mutation: removing the OOB row from CARVE turns T07 and both T08 oob cases red; tool
restored sha256-identical. Live dry run now reports CREATE 13 | EXISTS 3 |
RETIRE-REPORT 9 with both oob rows present; nothing written.
Gauntlet ALL GREEN (96); repo-lint 0 fail / 1 warn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

P8 host guard: && -> || (live false FAIL on voffice1); dc0 MAAS path restored
...
P8's guard warned only when BOTH terraform.tfstate and opentofu/.terraform were
absent. voffice1 has no state file but does have .terraform -- D-128 has it run the
INNER tofu roots, and `tofu init` leaves a provider cache -- so neither warn branch
fired, P8 ran a real `tofu plan`, and it died on a gitignored tfvar -> exit 1 -> hard
FAIL on the very host the Stage-5 runbook designates for preflight. terraform.tfstate
is the OWNERSHIP marker; .terraform is only a cache. Both must be present to evaluate.
No test caught it because T38 removes the whole opentofu/ directory, so both markers
vanish together; the fixture itself sat in the defective state and passed only because
the && was wrong. Fixture now creates both markers; new T39 covers voffice1's real
shape (cache present, state absent -> WARN). Mutation: reverting || to && turns T39
and only T39 red; script restored sha256-identical.
Also: the dc0 MAAS path was down -- the vr1-dc0-region profile points at an SSH
forward (127.0.0.1:5241) that died with the 05:48 rack reboot. Restored and proven
(plan vr1-dc0 PASS exit 0, was REFUSE exit 3). Not an SEC-010 puncture: an SSH forward
originates on the rack. voffice1 pulled to HEAD. Preflight re-run: 12 [FAIL] lines,
11 = the ruled-accepted P5 set compared by identity, 1 = P8.
tests/preflight 38 -> 39/39; gauntlet ALL GREEN (96); repo-lint 0 fail / 1 warn.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Both DC VMs 416 -> 480 GiB; 128 GiB swap; new preflight gate P8 (substrate drift)
...
POST-BOOKEND: the GA-R4 bookend (4b8ba3c) was committed BEFORE this work, so the
ledger close summary and the 08-01 sweep do not cover it. The ledger line is
AMENDED in this commit rather than left stale.
Operator: "Option 2 look sgood me. I approve the sequencing, process as
autonomously as possible" -- +64 GiB to EACH DC with swap first, over my
recommendation of +48. The deciding arithmetic, put up before the ruling: at +64
each, 994 GiB allocated against a 1007.4 GiB host = 13.4 GiB residual while the
host's OWN measured footprint is 18.0 GiB, with 1 GiB swap free. Swap is what
makes +64 safe, hence the sequencing.
PROBLEM FIXED, MEASURED: dc0's rack ran 402 GiB of inner guests in a 409 GiB host
(98.3%), leaving 7 GiB for the rack OS, mirror, snap proxy and page cache. Each
rack now has ~71 GiB.
SWAP: operator-run -- sudo -n is NOT available on vcloud so that half could not be
automated. /swap2.img 128 GiB, 600 root:root, live + fstab. 135 GiB total.
CHANGED THROUGH TOFU, the only correct place: a virsh setmaxmem would have been
reverted by the next apply. The derivation now lives IN the variable comment.
PLAN ASSERTED ON CONTENT (2026-07-20: an in-place apply silently regenerated 9
node MACs): per DC 1 in-place, 0 create/destroy/replace, 0 MAC changes, memory the
only changed attribute. A THIRD resource appeared and was resolved BEFORE
applying: module.office1_opnsense ~ id = 2 -> 11 sits under "has changed outside
of OpenTofu" (drift OBSERVED), not "will be updated in-place" (action PLANNED) --
libvirt's domain id is a runtime value. It was not touched.
dc1 FIRST AS CANARY, and it answered the open question: the in-place update
BOUNCES the guest (domain id 9 -> 12, uptime 0 min). Guest sees 472.2 GiB.
>>> FINDING: vr1-dc0-maas-01 (MAAS region) and vr1-dc0-juju-01 (juju controller)
have autostart=disable. <<< Only the edge auto-recovers. Checking first is the
only reason the dc0 bounce did not come back looking catastrophic. Both started by
hand and verified. LOGGED NOT FIXED: a host reboot leaves dc0 with no region and
no controller -- a standing exposure independent of this change.
RESULT: host used 809 -> 63 GiB, available 197 -> 944 GiB; the restarts also
released the stranded RSS (vvr1-dc1 354 -> 12 GiB), discharging 08-01 sweep F1 as
a side effect. Recovery verified end to end: MAAS region API 000 -> 502 -> 200 (a
real boot progression), snap proxy listening, mirror 200, juju controller model
"Last connection: just now".
GATE P8 -- substrate drift. Built because answering "does tofu need updates
logged" produced a measurement: preflight.sh and pre-flight-checks.sh contained
ZERO tofu plan checks and the opnsense drift had sat unseen. DESIGN POINT:
-detailed-exitcode returns 2 for BOTH a pending change and a harmless observation,
so P8 asserts on CONTENT -- pending ACTION FAILS, observed-only drift WARNS,
unrecognised shape REFUSES. A gate permanently red on benign drift gets ignored.
It WARNS rather than fails when it cannot look, since the outer root lives on
vcloud and "not the substrate host" is a legitimate state.
Harness 33 -> 38 (T34-T38). Baseline proven by stashing: my first draft's 7
failures were MINE, not pre-existing. Three mutations each killed a NAMED test,
script restored sha256-identical. T38 exercises the absence by removing the
fixture STATE, never the fake tofu from fakebin -- the real tofu is on the system
PATH and a "closed" PATH would fall through to the LIVE substrate. PROVEN LIVE:
tofu exit=0, 0 pending, 0 drift -> [ok] zero diff.
SCOPE: tofu owns the SUBSTRATE only. MAAS carves, rack services and the v6 node
carve are script-driven by design (Model B / D-123); P8 does not check them.
gauntlet ALL GREEN (96); repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

G19 BUILT and PASSED LIVE; finds a latent defect in D-139's execution list
...
scripts/dc-node-v6-verify.sh (270 lines, 24-line header) + tests/dc-node-v6-verify
(55 cases); manifest 95 -> 96. All three subcommands exercised LIVE this session:
plan vr1-dc0 PASS exit 0 (9 machines, v6 plane set IDENTICAL across all 9, 6
planes, prefixes DERIVED); node vr1-dc0 <spec> on storage-01 against live peer
storage-02 PASS exit 0 with 12 assertions -- six address-presence, six
peer-reachability, every plane "up, global, DAD complete, sole global on the NIC"
and "replies, neighbour DELAY"; bridges vr1-dc0 reporting 7 bridges. Harness
55/55, gauntlet ALL GREEN (96), three mutations each turning a NAMED test red.
RUN AGAINST A REAL NODE while the boot window was open specifically so it would
not ship fixture-green -- the snap proxy's harness was green for a week before
anything was proven end to end.
NO v6 PREFIX LITERAL EXISTS IN THE FILE, and that was a constraint not a nicety:
D-139 retires the ULA /48 for GUA, so a baked table would be right today and wrong
on carve day. plan derives from live MAAS and emits a SPEC; node consumes it and
knows no prefixes. A harness case drives the same green path with a ULA fixture
AND a GUA fixture, so the property is EXECUTED, not asserted.
>>> LATENT DEFECT IN D-139's OWN EXECUTION LIST <<<
D-139 names dc-node-v6-carve.py to "re-carve 54 node v6 statics". VERIFIED against
that script's source: it assigns v6 "on every plane WHERE IT ALREADY CARRIES
IPv4", picks the v6 subnet "on the SAME MAAS vlan as that v4 link", derives the
host part from "the last octet of the node's own v4 address", and its loop body is
a bare `if not v4: continue`. Under D-139 five planes lose v4 and lb-mgmt never
had a v4 twin, so after v4 removal it would SILENTLY CARVE FOUR FEWER PLANES PER
NODE rather than fail. The v6 carve MUST run BEFORE v4 removal, or the tool must
be rewritten -- compounding the already-recorded fact that D-139's "remove v4
LAST" clause is unsatisfiable. LOGGED, NOT FIXED. My own agent mandate stated this
tool's input source WRONGLY (apex/D-136; it is live MAAS); the agent checked
rather than inheriting the error, which is why the defect surfaced.
THREE MORE FINDINGS: (i) the multicast reading is ESTATE-WIDE -- all six plane
bridges plus the WAN bridge at snooping=1/querier=0 on dc0 AND dc1, and the
jumphost's uplinks too, i.e. the default here, not a chosen setting; the
subcommand tags it SUSPECT and asserts no Linux behaviour, correctly, since
cold-start multicast ND was already measured working across it. (ii) plan's PEER
SELECTION IS DEPLOYMENT-BLIND -- it picked control-01, which is not deployed, so
the derived SPEC cannot pass in a PARTIAL deployment; worked around by
substituting the live peer, logged not fixed. (iii) a harness bug caught by the
instrument-currency rule, NINTH of its kind: absent-ip/absent-virsh REFUSE cases
were driven with PATH=/usr/bin:/bin, which still contains both, so they were red
for the WRONG REASON.
TWO OPEN CHECKS SETTLED on the live nodes: every NIC carries exactly ONE global
(sole-global predicate holds today), and an OVS-internal br-ex does report
LOWER_UP (predicate safe).
GA-R2: the build agent wrote its own changelog; folded VERBATIM into this
session's single changelog and removed. Crossing midnight does not start a new
session. repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

RULED "B plus C": D-139 ruling A narrowed to an experiment + upstream fix
...
Operator, exact utterance: "B plus C". Ruling B's GUA carve is unaffected and
proceeds; ruling A's five-plane v6-only scope narrows to a bounded experiment;
metal-internal, data-tenant and lb-mgmt stay DUAL-STACK pending upstream fixes.
That last sentence is recorded as an INTERPRETATION, not as the utterance.
>>> I SCOPED THE EXPERIMENT WRONG AND THIS CORRECTS IT. <<<
MEASURED against shipped ceph-osd rev 953, ceph_hooks.py:544-553: prefer-ipv6 is
ONE switch that sets ms_bind_ipv4 = False GLOBALLY, so it governs Ceph's PUBLIC
network (storage) and CLUSTER network (replication) TOGETHER. There is no state
in which Ceph runs cluster on v6 and public on v4. I recommended "replication
alone, smallest blast radius" -- not achievable. The experiment is storage +
replication as a PAIR, with a larger blast radius than I represented.
A PATH THE EARLIER ANALYSIS MISSED: the buggy bare get_ipv6_addr() (LP #2061836,
dynamic/SLAAC-only, never fixed on ceph-osd, while our nodes hold MAAS statics) is
reached ONLY under `if not public_network:` / `if not cluster_network:`. Setting
ceph-public-network and ceph-cluster-network to the v6 CIDRs means it is never
used for address selection -- the defect becomes AVOIDABLE BY CONFIG. This is a
CODE READING, NOT A MEASUREMENT, and it does not address the get_mon_hosts()/
get_host_ip race (LP #2109798).
REVIEW PASS on netbox/d139-gua-carve.py (independent second agent, per operator
standing instruction). 4 real defects fixed, 4 reported, 1 charter miss upheld.
D1 is the mandate's own named class "a refusal that does not refuse": an
unreachable apex gave a raw traceback and EXIT 1 -- the tool's own header defines
exit 1 as a write error -- on a dry run that wrote nothing. _req caught only
HTTPError so URLError walked past the REFUSE handler. Fixed, REFUSE(2).
D2: a comment asserted ruling B was "FLAGGED" -- already false at commit time.
Replaced with a citation; prose in a script rots against governance state, a
citation cannot. D3/D4: two greps over the tool's OWN SOURCE and two vacuous
negatives replaced with behavioural coverage.
main() decomposition DECLINED on a reusable argument: the `if not a.commit:
return 0` guard is ADJACENT to the write block, and extracting commit_writes()
would split that safety proof in two. Line count is not the criterion.
R1 IS A REAL SECURITY FINDING, MEASURED: urllib carries the Authorization header
across a CROSS-HOST redirect (loopback probe -- the target received the token
verbatim). SANDBOX_HOSTS gates the supplied url, not a redirect from it. LOW
severity, logged not executed.
Harness 65/65 -> 71/71; all 5 original mutations RE-RUN and still standing, plus
3 new; gauntlet ALL GREEN (95); dry-run body diffed BYTE-FOR-BYTE identical to the
committed capture; apex 139 prefixes before and after. QUALIFIED: a dry run
executes zero POSTs, so D1's change to the WRITE path's failure semantics is
UNTESTED and is declared, not implied.
PROCESS FAILURE REPEATED AND OWNED: the review mandate required a docs/audit/
capture and forbade touching CURRENT-STATE.md -- jointly unsatisfiable against
L10. The agent correctly left the tree RED and said so rather than working around
it. Standing fix, now twice learned: an agent mandate must name a repo path for
findings AND leave the committer able to satisfy L10.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

D-139 ruling A NOT achievable at these charm revisions; apex carve tool built
...
TWO agent results, both recorded; nothing actioned, nothing pushed to NetBox.
>>> RULING A (v6-only on five planes) IS BLOCKED BY UPSTREAM CHARM DEFECTS <<<
RULING B (full GUA) IS ORTHOGONAL AND STANDS -- GUA-vs-ULA is independent of
whether a plane keeps v4, so the carve tool is unaffected and a GUA DUAL-STACK
carve is achievable today. This needs an operator ruling.
RE-VERIFIED INDEPENDENTLY against the shipped ceph-osd rev 953 artifact, because
it decides the ruling. hooks/utils.py:203-217 get_host_ip() returns
get_ipv6_addr()[0] when prefer-ipv6 is set, else socket.inet_aton(hostname) and
on failure dns.resolver.query(hostname, 'A') -- an IPv4-ONLY lookup -- under the
charm's OWN comment "...just let it kill the hook". So ceph-osd fails on a
v6-only plane in BOTH directions: false -> inet_aton raises on a v6 literal then
NXDOMAINs an IN A lookup (LP #2109798, New); true -> bare get_ipv6_addr() (also
ceph_hooks.py:547, WITHOUT dynamic_only=False) returns DYNAMIC/SLAAC only while
every VR1 node holds a MAAS STATIC v6 (LP #2061836 -- Fix Committed on ceph-mon,
New and never fixed on ceph-osd, confirmed absent from rev 953).
Reported from the capture and NOT re-verified here, recorded as such: mysql /
mysql-router bare user:pw@addr URIs invalid for an unbracketed v6 literal;
hacluster 2.4/stable rev 166 (released 2026-06-29) still hard-writes
ip_version: ipv4 though LP #2111852 is Fix Committed -- FIX COMMITTED IS NOT FIX
RELEASED; OVN documenting encap as "The IPv4 address of the encapsulation tunnel
endpoint" with zero v6 encap values in OVN 24.03's test suite.
Counted locally: metal-internal appears 273 times in bundle.yaml across ~44 apps;
rabbitmq-server is DEPLOYED by the bundle but ABSENT from PREFER_IPV6_CHARMS --
the only mismatch across all 33 bundle pairs, where invariant 9a would emit a
factually false message.
APEX CARVE TOOL (netbox/d139-gua-carve.py, 232 lines) + tests/d139-gua-carve
(65 cases, offline stub apex); manifest 94->95. Live dry run: CREATE 22 |
EXISTS 6 untouched | RETIRE-REPORT 18, and it surfaced 52 DEPENDENT ip-addresses
inside the retiring ULA /64s that a later delete would orphan. Proof no write
occurred: apex prefix count 139 before and after. 5 mutations, each turning a
NAMED test red, tool restored sha256-identical.
OWED: (a) the foundational measurement is STILL not taken -- does network-get
return v6 on a v6-only bound space? (b) D-139's "remove v4 LAST, after each is
proven" is UNSATISFIABLE as written; per-plane conversion is atomic.
(c) the rabbitmq-server mismatch.
PROCESS FAILURE, OWNED: the research was written to /tmp and would have been LOST
-- I scoped the agent read-only and gave it no repo path. Rescued, sha256-verified.
STANDING CONSEQUENCE: every agent mandate must NAME A REPO PATH for its findings;
"read-only" constrains what it may CHANGE, not where it may RECORD.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

D-139 ADOPTED: VR1 goes IPv6-only east-west, whole carve moves to GUA
...
TWO GA-R5 rulings, taken in SEPARATE exchanges, recorded separately.
A -- family matrix. Operator: "IPv6 on all planes except for metal-admin and
provider-public which will remain dual stack". metal-internal, data-tenant,
storage, replication and lb-mgmt become IPv6-ONLY. Amends D-101's matrix twice:
metal-internal's "datastore east-west stays v4-bound" is superseded, and lb-mgmt
(ruled v6-only by D-101 but NEVER carved anywhere) becomes a first-class plane.
B -- addressing model. Operator: "Full GUA on every plane (Recommended)". Every
plane carves from its DC's GUA /48 on the :10/:11/:20/:21/:30/:40/:50/:80 octet
map VR0 DC0 and the Willamette site ALREADY use -- conforming to an existing org
standard, not inventing one. The ULA /48 fd50:840e:74e2::/48 is RETIRED for VR1.
Amends D-101 and D-111. Deciding reason is measurable: RFC 6724 ranks IPv4-mapped
at precedence 35 and ULA at 3 while GUA falls under ::/0 at 40, so on a dual-stack
plane a ULA leg LOSES address selection to IPv4 and is decorative. SEC-010, D-052,
D-125 and D-107 unchanged -- containment lives at the forwarding layer.
THE RECORDED ROOT CAUSE OF THE v4-ONLY CONTAINERS WAS WRONG AND THIS REPO CARRIED
IT (GA-R1 C2 -- measurement corrects the document). CURRENT-STATE said MAAS "has
nothing to give for v6". Measured, and re-verified independently: subnet
statistics on fd50:840e:74e2:220::/64 returns available_string "100%",
num_available 18446744069414584320. Zero ipranges rows means zero RESTRICTIONS,
not zero availability. dc-plane-ipam.sh:368-372 has carried the correct behaviour
since 2026-07-27, in the tree, contradicting the authoritative doc the whole time.
The real mechanism is juju-side and there is NO knob: EthernetDeviceForBridge
(tag v3.6.27) takes addrs[0] from an UNSORTED query and derives one CIDR -> one
LinkSubnetArgs. gomaasapi documents "Any number of STATIC links can exist on an
interface", so MAAS would accept both families; the limit is juju's. LP #1723240
is this exact symptom, Triaged/Low since 2017, and the theory this repo recorded
is the one its reporter rebutted in-thread. Consequence: on a container plane
DUAL-STACK IS NOT EXPRESSIBLE while v6-only IS -- ruling A is the achievable
configuration, not merely the desired one.
SNAP PROXY: dc0 INSTALLED, RUNNING, check dc0 PASS exit 0 (16 assertions,
re-verified independently). A REAL SNAP PAYLOAD WAS FETCHED THROUGH IT -- HTTP 206,
first bytes hsqs. No longer fixture-green. BUG-3 confirmed by measurement, no
assertion changed. BUG-4 fixed at BOTH sites (the review named one); T20b/T41b NEW.
OWNED: my BUG-1 fix was WRONG. ip route get 10.12.8.4 -> src 10.12.8.2; .4 is a
SECONDARY and Linux never auto-selects a secondary as a source, so the rule is
still dead. Not fixed now -- editing the generator turns the LIVE gate red until
install re-runs. T23b annotated so nobody "fixes" the test to a wrong value.
Generalisable: any ACL keyed to a service ALIAS (the whole D-134 utility .4 band)
misses that host's own traffic -- same exposure in dc-mirror.sh, dc-cache-proxy.sh.
OWED, unverified: the deployed jammy /etc/gai.conf, which can override the RFC 6724
default table that ruling B rests on. Nothing of D-139 is executed.
repo-lint 0 fail; tests/dc-snap-proxy 55/55; 4 mutations killed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
| 2026-07-31 |

Snap proxy tool BUILT and REVIEWED (not yet applied); 4 real bugs logged
...
Ruling 2's repo side. squid 6.14 as a dedicated instance on each DC rack at the
D-134 utility .4:3129, CONNECT-only, restricted to Canonical's documented store
and CDN hosts, consumed via juju snap-http-proxy/snap-https-proxy. Design cited
to vendor sources, not preference: every snap endpoint is :443 so a CONNECT
proxy is mandatory and CACHING IS IMPOSSIBLE (squid cannot read a CONNECT
tunnel), which removes the apt-style caching argument entirely; squid is in
main, tinyproxy is universe with six unevaluated CVEs and defaults to allowing
all CONNECT ports.
MEASUREMENTS THAT CHANGED THE DESIGN:
- the failing apps are LXD containers sourcing from METAL-ADMIN (10.12.8.122),
NOT the 10.12.12.116 juju status displays. An ACL on the displayed address
would have denied every client the proxy exists for.
- ports 3128 AND 8000 are already held wildcard-bound by MAAS's own squid on
both racks -- hence 3129 and a dedicated instance.
- no new D-134 octet needed: .4 is already aliased on both racks.
- LOGGED NOT ADOPTED: MAAS's squid ALREADY CONNECT-proxies api.snapcraft.io,
measured working from the failing container. snap-https-proxy pointed at
10.12.8.6:8000 may unblock the deploy today with nothing built. Rejected as
the shipped design (per-revision path, NO destination restriction, the
hidden-coupling shape D-135 already has a scar from) but it is a real
operator option, not a dismissal.
QUALITY: 53 mutations, 53 killed, 0 survived -- including the dc-mirror.sh
existence-vs-content false-green class reproduced and caught. One survivor was
a MALFORMED mutation that broke bash -n and therefore tested nothing; recorded
as malformed and redone rather than counted. Three of the harness's own cases
were found too weak and were STRENGTHENED, not left green.
INDEPENDENT REVIEW: ruled design HONOURED on all four counts -- utility band, no
direct node egress (in fact stronger: destination-allowlisted, and that scope is
flagged as a build-time choice NOT a ruling), site-keyed both DCs with the dc1
limit stated honestly, and it OWNS its own network prerequisites rather than
borrowing another service's -- with the coupling anti-pattern now under test
(M09/M10) rather than only under comment.
FOUR REAL BUGS LOGGED, NOT FIXED (hard rule 1 -- the file was frozen for review):
BUG-1 dead snap_probe ACL; the probes are actually admitted by snap_clients,
and the comment misdescribes its own mechanism
BUG-2 | grep -q under pipefail -- INHERITED from dc-mirror.sh and
dc-cache-proxy.sh alike; a three-script sweep, not a local fix
BUG-3 the deny probe's 403 shape was measured from a PORT deny, never a
DSTDOMAIN deny. If squid answers differently the gate REFUSES
PERMANENTLY -- and a gate that cannot PASS blocks a GA-R6 close as hard
as one that cannot fail. Requires a one-time apply-time confirmation.
BUG-4 dpkg -s reports a deinstall-ok-config-files package as present
SIZE: 636 lines / 320 code vs dc-mirror 385/227 and dc-cache-proxy 396/200. The
reviewer's landing zone is ~535/~250 and it RECOMMENDS AGAINST forcing 385 --
two extra generators (75 lines) are measured-forced because the packaged squid
cannot be repointed, and further cuts would delete mutation-proven assertions.
~50 lines of the excess is header prose, which carries zero bug risk.
TWO BLOCKERS BEFORE THE PLAN IS APPLIED: the S8 consolidation invalidates
M32-M39/M42-M47/M49-M52 as evidence, so the mutation pass MUST be re-run; and
BUG-3 must be confirmed once against the built proxy before check is cited to
close anything.
NOTHING IS PROVEN END TO END -- no proxy installed, no snap fetched through one.
The harness green is FIXTURE green and says so.
repo-lint 0 fail; gauntlet ALL GREEN (94).
OWNED: my earlier teardown commit e57ad09 used 'git add -A' while the agent was
writing and swept its audit capture and CURRENT-STATE paragraph into a commit
about the model teardown. This commit is staged EXPLICITLY by path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Build the (b) ruling: prefer-ipv6 emitted for NO application; 9b re-pointed
...
Renderer: the emission condition is DISABLED, not deleted -- when the v6
completion work lands (allocatable ranges on the six v6 plane subnets, rack-side
v6 legs, a v6 default route, v6-reachable services) that one line turns it back
on, and PREFER_IPV6_CHARMS is still the correct set to gate it by.
Both overlays re-rendered: exactly 7 lines removed per DC and nothing else --
all 13 apps keep 6-leg dual-family vips at both DCs.
INVARIANT 9b RE-POINTED, not deleted: it required the option and the v6 legs to
travel together on a declaring charm (the L3-9 defect), which would now fail on
the ruled artifact. The rule is the option must be ABSENT everywhere. L3-9 cannot
recur while it is absent -- there is nothing to keep while legs are dropped. It
returns to the coupling form when v6 is operational.
HARNESS re-pointed rather than deleted, and three stale fixtures fixed:
- T20 was 'option without v6 legs FAILS'; now 'option on a DECLARING charm FAILS'
- T21 was the inverse; now the POSITIVE CONTROL -- v6 legs WITHOUT the option
must PASS, so 9b cannot be satisfied by rejecting everything
- T22/T23/T30 each set prefer-ipv6 because the OLD coupling required it
alongside v6 legs. Under the absence rule 9b fires FIRST and short-circuits,
so their v6-band assertions would never have been reached and they would have
passed for the wrong reason. The option is removed from those fixtures.
Mutation-proven: disabling the 9b absence check kills T20; re-enabling renderer
emission kills render-drift. Source restored byte-identical after each.
Gauntlet ALL GREEN (93); repo-lint 0 fail; harness 55/55.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Build the D-135 amendment: jammy-backports at source, T10 re-pointed
...
scripts/dc-mirror.sh's generated sync script gains jammy-backports in --dist=,
with a comment at the generator recording why it is not optional: the MAAS
jammy image ships backports in its sources.list and juju's apt-mirror
model-config rewrites every suite to this mirror, so a mirror without it 404s
and every charm install hook fails. That cost 22 units today.
FIXED AT SOURCE, not only on the live rack -- a live-only edit would leave the
generator regenerating the old scope at the next install or at any future DC
standup.
tests/dc-mirror T10 asserted the old dist string verbatim. RE-POINTED to the
new invariant with the reason recorded in-file, never deleted to go green.
Mutation-proven: reverting the generator's dist line turns T10 red (23/1);
restored byte-identical, 24/24.
Gauntlet ALL GREEN (93); repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Option D half 1: explicit base on all 56 apps, gated by new invariant 12
...
PROVEN AGAINST THE REAL MODEL, NOT INFERRED. The earlier one-app test only
showed the error MOVING to the next app. The fully-based bundle was staged as a
throwaway on the rack and --dry-run against the LIVE 23-application model --
the same command that errored on barbican 265-vs-261 -- exits 0.
Subordinates are in scope, measured: mysql-router is 1154 at 22.04 and 1178 at
24.04. hacluster sharing rev 166 across both bases is not a property to depend
on.
TWO SILENT UNDER-MATCHES, both caught by cross-checking the edit against the
parsed file rather than by review:
(1) a first pass keyed on the block form inserted 44 of 56 -- the twelve
-hacluster apps are single-line FLOW MAPPINGS. The inserter asserts its
app list equals what yaml.safe_load sees, and refuses otherwise.
(2) overlays/dc-ha-scaleup.yaml DEFINES vault-hacluster, an application that
exists nowhere in bundle.yaml, and it had no base. That overlay is a LATER
deploy step, so this would have re-opened the trap AFTER the deploy
succeeded. Found by the new gate on its FIRST run, in a file the fix was
not looking at -- the same later-step-overlay class as the ceph-mon miss
earlier today.
INVARIANT 12 (NEW): every application carries an explicit base equal to the
bundle's OWN default-base -- keyed on that value, not a literal. A bundle with
no default-base REFUSES rather than passing vacuously. Without this, deleting
the 56 lines is a byte-identical PASS and the trap returns at the next partial
failure, at any DC.
Five cases, each mutation-proven individually (harness 50 -> 55/55): neutering
nobase kills T49/T50/T52, wrongbase kills T51, the refuse branch kills T53.
Gauntlet ALL GREEN (93); repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Fix a defect in my own build: invariant 9a was VIP-scoped and the charm list was short
...
Found by a repo-wide grep, not by any gate. Two faults, one cause -- I
measured and gated the population the QUESTION named (the 13 VIP charms)
rather than the population the INVARIANT covers (every application juju
validates).
(1) PREFER_IPV6_CHARMS was WRONG. Re-measured across ALL 33 charms in
bundle.yaml: TWELVE declare the option, not seven. The five missing are
ceph-mon 491, ceph-osd 953 (squid/stable), nova-compute 894, hacluster 166
(2.4/stable) and mysql-innodb-cluster 164 (8.0/stable).
(2) 9a was written INSIDE the VIP loop, so an application with no vip never
reached it -- and the repo ALREADY has that case:
overlays/dc-dc-ipv6-family-matrix.yaml sets prefer-ipv6 on ceph-mon, which
carries no VIP. That overlay is a LATER deploy step, so the miss would have
passed attempt 2 cleanly and surfaced at the step after, looking like a new
fault.
9a now runs over EVERY application, before the VIP loop; the VIP loop skips an
app 9a already flagged so the diagnosis is not doubled. ceph-mon's own in-file
claim of the option is now INDEPENDENTLY VERIFIED from the artifact rather
than taken from the comment.
Harness 48 -> 50/50. T47 (non-VIP app whose charm lacks the option FAILS) and
T48 (ceph-mon PASSES) both mutation-proven: re-scoping 9a back to the VIP loop
kills T47; dropping ceph-mon from the list kills T48.
Gauntlet ALL GREEN (93); repo-lint 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Build the 2026-07-31 ruling: renderer + invariant 9 replaced, overlays re-rendered
...
Three parts, all mutation-proven.
(i) provider-bundle-check.py gains PREFER_IPV6_CHARMS -- the measured authority,
keyed by CHARM name, carrying the revisions and a re-measure warning -- and
invariant 9 is REPLACED, never deleted:
9a prefer-ipv6 on a charm that does NOT declare it FAILS. Asserted on
PRESENCE, not truthiness: 'prefer-ipv6: false' is the same fatal
'unknown option' to juju as true.
9b on a DECLARING charm it still travels with the v6 legs -- the L3-9
protection is retained unchanged for those seven.
9c arity.
The old text's mechanism claim ("prefer-ipv6 makes HAProxy bind :::port") is
refuted by this session's measurement and is recorded as such in-file.
(ii) render-dc-overlays.py emits the option only for a declaring charm, READING
the set from provider-bundle-check with ast rather than restating it (the same
rule APP_OCTET already uses). render() keeps its purity property: the set is a
PARAMETER and the default REFUSES rather than defaulting to empty, because an
empty set renders a plausible artifact with the option nowhere. Both overlays
re-rendered: exactly SIX lines removed per DC and nothing else -- every vip
string byte-identical, so all v6 legs are retained.
(iii) tests/provider-bundle-check 44 -> 48/48. T43 reproduces the attempt-1
defect; T44 proves presence-not-truthiness; T45 is the positive control; T46
asserts the DIAGNOSIS.
MUTATION PASS -- six mutations, every one killed tests. And one NEW assertion
was found to be DECORATION and was REPLACED rather than kept: T46's first form
could not fail under any mutation, because a non-declaring charm with no option
reaches neither branch. Re-written to catch the fold-9a-into-9b simplification
and re-proven.
Gauntlet ALL GREEN (93); repo-lint 0 fail. The fixed dc0 deploy input now
sweeps CLEAN on option names: 75 assignments, 20 schemas, 0 unknown.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

dc-node-carve: convert the commissioning auto link, found by a live dry-run
...
The first live plan refused enp1s0 on every machine: it holds an `auto` link on
metal-admin out of enlistment, and the "wrong address on the right subnet is a
FAILURE" rule was too broad. A commissioning link is the expected starting
state, not a conflict -- so the tool could not carve the metal-admin leg at all.
Narrowed BY MODE: auto/dhcp/link_up gets unlinked and re-created as STATIC
(MAAS will not convert a link's mode in place); a `static` link with the wrong
address is still refused, because something deliberate put it there.
The `raw` fixture modelled the auto link correctly -- what was missing was any
assertion that `apply` over the raw state EXITS 0. T21-T24 only grepped for
planned strings, so a refusal on the very first NIC read as green. T26/T27 now
cover both directions and both are mutation-proven.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
| 2026-07-30 |

dc-node-carve.sh: the v4 node interface carve, mutation-proven 40/40
...
The last no-tool gap between the dc0 region migration and the Juju deploy:
60 NIC re-homes, 9 OVS br-ex bridges, 54 statics. The Stage-4 equivalent was
done ad-hoc and logged outside the repo, so there was nothing to re-run when
D-132 q1 forced a re-enrolment.
Proved the four-call MAAS cycle on ONE live interface (canary 6q4syf enp3s0)
before writing the tool -- nothing had exercised `interface update vlan=` on a
link_up-only interface on an auto fabric, which is the state all 60 NICs are in.
Measured in passing: link-subnet REPLACES a link_up link, so only the br-ex
member needs an explicit unlink.
No default profile; maas-profile-assert.sh before any read or write; every id
resolved live by stable key (boot MAC / CIDR / interface name); every mutation
read back and compared. Role asymmetry derived from the host token, and the
controller's deliberately-uncarved NICs are asserted ABSENT.
Mutation pass found the br-ex static compare could not fail -- Pattern B was
being caught by the type/parent assertions instead. Added the brexip fixture
and re-proved the kill. Also found that neutering an assertion's MESSAGE only
proves it exists, not that its predicate works.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

lib-hosts: the node carve is ROLE-DEPENDENT, not uniform
...
Measured from the pre-migration capture. Role nodes carve all six planes
plus an OVS br-ex parented on enp2s0 carrying the provider-public static,
with enp2s0 itself holding no L3 link. The juju controller carves only TWO
planes -- enp1s0 metal-admin and enp2s0 provider-public as a raw NIC WITH
the gateway -- has no br-ex, and leaves enp3s0..enp6s0 on their
auto-created VLANs.
Evidence: 7n87bt (controller) reads enp3-6 on vlans 5200-5203, which are
auto fabrics; 8mtpxq (storage-02) reads enp3-6 on 5190-5193, the named
planes, plus br-ex.
This matters because assuming uniformity fails in both directions: it would
either strand the controller without a default route -- the under-carve that
cost three bootstrap attempts on 2026-07-30 -- or give it four legs it must
not have. The node carve has no repo tool yet, so this was about to be
re-derived by hand.
dc-selector 95 checks; the depths-must-differ guard proven able to fail, and
the controller-presence assertion checked inside a per-DC subshell because
the main shell still holds the flat VR0 fleet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

lib-hosts: record the MEASURED VR1 node NIC -> plane order
...
The substrate roots pin six MACs per node and the module attaches them in
list order, so macs[i] is enp<i+1>s0. Verified end to end on
vr1-dc0-control-01 against the live carve: enp1s0 metal-admin (PXE),
enp2s0 provider-public (br-ex parent), enp3s0 metal-internal, enp4s0
data-tenant, enp5s0 storage, enp6s0 replication.
This is NOT lib-net.sh's PLANE_CIDRS order, which starts with
provider-public. Anything walking PLANE_CIDRS positionally to place NICs
swaps enp1s0 and enp2s0 and puts the PXE leg on provider-public, stranding
commissioning on a plane with no DHCP. The node carve has no repo tool yet,
so this ordering was about to be re-derived by hand; recording it measured,
with a guard that the two orders differ, is the cheap half of that tool.
dc-selector 88 checks; the trap guard proven able to fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

dc-node-v6-carve.py: honour MAAS_PROFILE; refuse instead of tracebacking
...
The script was env-blind -- default 'admin', --profile only, no environment
read anywhere. Every bash tool here honours MAAS_PROFILE, so
`export MAAS_PROFILE=vr1-dc0-region` left THIS tool silently targeting the
Office1 region. During the per-DC region migration that is the
unrecoverable direction: against the wrong region a carve is an idempotent
no-op that prints PASS while the region you meant stays untouched.
It now reads MAAS_PROFILE, keeps --profile as an override, and prints which
region it targets and where that came from -- a capture that does not name
the profile cannot be audited for wrong-region work.
An absent maas CLI raised FileNotFoundError and exited 1 with a traceback,
indistinguishable from a real failure and contrary to this script's own
REFUSE contract. It now refuses with exit 2.
Harness 14/14 (was 9); both fixes proven able to fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

dc-region-topology: RENAME the auto fabric instead of moving the subnet
...
Moving a subnet between fabrics does NOT bring interface links along. The
first apply created vr1-dc0-provider-public and moved 10.12.4.0/22 onto it,
leaving the region VM's enp2s0 holding a static 10.12.4.6 on a VLAN whose
subnet had left -- the region's provider-public leg, which it needs for
image sync, and the same under-carve class that cost three bootstrap
attempts on the Juju controller. Live connectivity was unaffected because
the OS netplan is independent of MAAS's model, so every other assertion
passed 39/39 with the model inconsistent.
Renaming the auto fabric that already carries the subnet moves nothing. A
MOVE is kept only for the case where the fabric carries other planes, and
now prints a warning that links do not follow.
Adds the assertion that would have caught it: every interface link must be
on its subnet's VLAN. Harness 47/47; the rename path and the stranded-link
check each proven able to fail.
Live remediation already applied to the dc0 region: subnet returned to
vlan 5002, the wrongly-created fabric deleted, fabric-1 renamed, space
re-bound. enp2s0 now reads fabric=vr1-dc0-provider-public with its static
intact.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|
dc-region-topology: explicit dry-run placeholders instead of empty ids
...
An empty `vlan=` in a dry run reads like a defect and would hide a real
one. On a from-scratch build the target fabric does not exist yet when the
MOVE and space-bind lines are planned, so their ids cannot resolve; they
now print <vlan-of-NAME> / <fabric-for-PLANE>. Four assertions added.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

dc-region-topology.sh: the plane-topology builder the repo never had
...
A read-only survey found THREE dc0 carve components with no repo tool at
all -- the named plane fabrics, the six v4 plane subnets, and the site
placement tag were created ad-hoc in the Stage-4 window, with the mutations
landing only in ~/as-executed/2026-07-23-stage4-carve.log, which is not in
the repo. When D-132 q1 required rebuilding that carve on a new region
there was nothing to re-run. This closes the gap so dc1 and every Roosevelt
DC standup rebuild from a tool rather than a transcript.
check/apply over: five named plane fabrics, six spaces, six v4 plane
subnets on the right VLANs with the ruled gateways, VLAN->space bindings,
and the openstack-<site> tag. Expected values derive from lib-net.sh;
fabric names derive from the site token. Numeric MAAS ids are resolved
per-run by name or CIDR and never persisted -- the pre-migration capture's
id= values are database row ids, not 802.1Q tags, and nothing reads them.
metal-admin deliberately keeps MAAS's own auto-created fabric: it is a
discovery artifact created when the rack registers, Juju binds spaces
rather than fabrics, and renaming it would fight MAAS for ownership of its
own object for no functional gain. The check asserts only that it does not
share a fabric with a named plane.
Every mutating path is gated by maas-profile-assert.sh first -- against
the wrong profile this apply is an idempotent no-op that prints PASS.
--profile has NO default, because the repo-wide default 'admin' resolves
to the Office1 region.
Harness 35/35; 4 mutations each killed tests (region gate removed, profile
defaulted to admin, space-binding assertion, fabric-placement assertion).
Fixtures corrected against a live read: MAAS reports an unbound VLAN as
the string "undefined", not null, so the first fixtures graded a payload
MAAS never emits.
Live read-only check against the new dc0 region: 5 passed, 22 failed,
exit 1 -- the expected shape of a region that has only auto-discovered its
own two NICs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Per-region power address, region power-key tool, and two red gates fixed
...
lib-hosts.sh: MAAS power ops originate from the REGION, so the reachable
rack address depends on which region dials. Measured from vr1-dc0-maas-01:
the rack's transit leg 172.31.0.2:22 is CLOSED, its metal-admin leg
10.12.8.2:22 is OPEN. Adds VIRSH_POWER_ADDRESS_FROM_{OFFICE1,DCREGION} for
both DCs (dc1's 10.12.68.2 measured on the dc1 rack, not inferred from
dc0's .2). VIRSH_POWER_ADDRESS still aliases the Office1 form so existing
callers are byte-for-byte unchanged. dc-selector 81 checks; 4 mutations
each killed tests.
New scripts/maas-region-power-key.sh (harness 44/44) installs and verifies
the per-DC MAAS->libvirt key inside a region's snap, ending on a real virsh
call rather than on config. Runbook prerequisite 5 carried this as prose,
and prose is not a gate -- it must also be re-asserted after every snap
refresh. LIVE at dc0: 9/9 exit 0, virsh enumerates 12 domains. The key is
the SEC-012 dc0 key already authorized on that rack, so per-DC isolation
holds.
Three defects its own testing found: ssh-keygen -lf reads an adjacent .pub
and reports ITS fingerprint; a snap's data root (/var/snap) and exec root
(/snap) are not interchangeable; the snap's virsh must run inside the snap
runtime or it fails on missing libraries.
tests/node-vm T8/T9 were RED at HEAD before any of this session's work
(proven by stashing and re-running): commits 086c827 + 447315f added the
region VMs, moving the dc0 root to 11 macs lists / 66 MAC literals against
assertions reading 10 / 60, and the gauntlet was not re-run. Re-pointed to
the new invariant, reason recorded in-file, re-proven able to fail.
HARNESS-MANIFEST re-recorded (91) for the two new harnesses.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|

Region-resolution gate: maas-profile-assert.sh + named dc0 region profile
...
The wrong-REGION hazard is the migration's unrecoverable failure mode: 13
repo scripts default to MAAS_PROFILE=admin, which on voffice1 resolved to
the OFFICE1 region. Against the wrong profile a carve recreate is a silent
no-op that prints PASS, and machine delete destroys the real record.
New scripts/maas-profile-assert.sh asserts which region a profile resolves
to by rack-controller IDENTITY (a machine count is not proof -- two regions
can hold the same number). Harness 20/20; mutation pass found one of its own
assertions could not fail (nameless rack entries were caught by a different
branch) and it was made branch-specific.
Live both directions: admin -> 23 machines / office1 trio (exit 0);
vr1-dc0-region -> 0 machines / hot-kid (exit 0); admin asserted as hot-kid
(exit 1). Capture docs/audit/dc0-region-profile-assert-20260730.txt.
Tunnel voffice1 -> rack -> 10.12.8.6:5240 because no single host has repo +
CLI + new-region reach + NetBox apex reach. Application-layer and
rack-originated, so SEC-010 is not punctured; nothing was rerouted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HvCyrwvYTTcDYnRErfMsNf
|