GA-R2: ONE changelog per session. This is a SEPARATE session from docs/changelog-20260731-prefer-ipv6-research.md (same date, different session); that file is closed and is not appended to here.
Branch dc-dc-stage5-preconditions. Status claims live ONLY in docs/CURRENT-STATE.md; nothing here is a status claim.
dc-snap-proxy.sh review's SIMPLIFICATION PLAN was neverWhat. No artifact change. A finding, recorded because the next action in the prior session's close-out cannot be executed as written.
docs/session-ledger.md:296 gives the next step as "apply the review cuts, re-run the mutation pass (S8 invalidates it)", and docs/audit/queued-findings-20260731-stage5-deploy.txt:145-146 (N2) says "The dc-snap-proxy simplification plan is NOT applied. Applying it REQUIRES re-running the mutation pass -- S8 invalidates M32-M39, M42-M47, M49-M52 as evidence."
MEASURED: the plan those S-numbers refer to exists nowhere in the repo. A repo-wide grep for \bS8\b over *.md / *.txt / *.sh returns exactly two live hits -- the ledger line and the N2 line above, both of which CITE the plan without containing it -- plus three unrelated 2026-07-07 archive hits (docs/archive/script-quality-findings-20260707.md, an S1-S8 consolidation proposal set about KEYSTONE_VIP and D-003 FIP bounds, a different subject entirely). The mutation IDs it invalidates ARE recorded (docs/audit/stage5-snap-proxy-measurements-20260731.txt:505,516,523); the simplification items themselves are not.
So N2's wording understates it. "NOT applied" reads as deferred work that is waiting somewhere. It is not deferred -- it lived only in the review agent's transcript and was lost at the session boundary. This is the repo's own top-tier failure mode (a finding that exists only in a transcript), landing on a finding ABOUT losing findings.
DECISION, and the reasoning stated so it can be overturned: the simplification is NOT re-derived. It was quality-only work on a tool that is about to be applied for the first time; re-deriving a lost plan and then re-running a 53-mutation pass to restore evidence for it is precisely the record-churn the 2026-07-30 standing operator directive shuts down ("Further hardening, auditing, or record repair is OUT OF SCOPE next session unless it directly blocks the deploy"), and it is the GA-F06 circuit-breaker case. Consequence that follows and is NOT a loss: because S8 is never applied, its invalidation of M32-M39/M42-M47/M49-M52 never fires, so the prior session's 53/53 mutation evidence STANDS unchanged for every assertion this session did not touch.
Revert. Nothing to revert (no artifact changed). To reverse the decision, re-derive the plan from scratch against the current file and re-run the full mutation pass -- the old S-numbers are not recoverable and must not be cited.
dpkg -s squid gates replaced by a BINARY assertion, atcheck AND install)What. scripts/dc-snap-proxy.sh (the check package gate and do_install's prereq) + tests/dc-snap-proxy/run-tests.sh (the dpkg PATH fake removed, a real binary seam added, T41 re-pointed, T41b and T20b NEW).
Why. BUG-4 from the independent review (docs/audit/queued-findings-20260731-stage5-deploy.txt:64-66): dpkg -s squid exits 0 for Status: deinstall ok config-files, so a PURGED squid reports as present. The existence-vs-content class this repo already has a scar from. [ -x /usr/sbin/squid ] is shorter and strictly stronger.
The review named ONE site; there are TWO. The same gate sat at do_check AND at do_install's prereq. Fixing only check would have left install false-passing its prereq on a purged package and then dying confusingly at squid -k parse or at unit start -- a worse failure than the one being fixed, because it surfaces further from its cause. Both are fixed.
Written through the EXISTING test seam, not around it. The assertion is [ -x "${SYSROOT}/usr/sbin/squid" ]. SYSROOT is empty in every production invocation, so the live path is byte-identical to /usr/sbin/squid; the harness points it at its mktemp tree. This is the script's own documented idiom (DC_SNAP_PROXY_SYSROOT, with in-repo precedent in maas-region-power-key.sh, creds-audit.sh, cloud-snapshot.sh) and it keeps the assertion drivable in BOTH directions, which the dpkg fake also was -- the fix does not cost a failure direction.
Test cases, and why each exists.
dpkg.mode.deinstall ok config-files residue shape. This is the case the OLD assertion could not fail on, i.e. the one that makes the fix mean something. Without it the fix would be untested in its own direction.do_install is never EXECUTED by this harness (it needs root and a real rack), so its half of the fix would otherwise carry NO assertion at all. Asserted structurally, following T18's established precedent of scoping a grep to a function body: zero dpkg -s gates anywhere in the file, AND INSTALL_BODY carries the [ ! -x ... ] form.Mutation-proven (this repo's rule: an assertion nobody can turn red is decoration). Three mutations, each neutering a PREDICATE, script restored and sha256-verified byte-identical after each:
check's binary predicate -> if true : T41 AND T41b both red (53/2).install's prereq -> if ! dpkg -s squid ... : T20b red (53/1).install's prereq -> if false : T20b red on its second branch (53/1).Revert. git checkout -- scripts/dc-snap-proxy.sh tests/dc-snap-proxy/run-tests.sh (reverts items 2-4 together; they are edits to the same two files).
What. gen_squid_conf's acl snap_probe src 127.0.0.1/32 -> src ${LISTEN}/32, its comment block rewritten to describe the real mechanism, + T23b NEW.
Why. BUG-1: squid binds ${LISTEN}:${PROXY_PORT} only, so nothing can ever arrive on loopback -- the rule matched nothing. The probes were actually admitted by snap_clients (because LISTEN lies inside CLIENT_CIDR), and the comment misdescribed its own mechanism. The hazard was live, not cosmetic: anyone narrowing CLIENT_CIDR would break check, and the rule they would reach for to fix it would not have helped.
Why the stronger fix was taken rather than a comment-only fix. The obvious minimal action was to correct the comment and leave the dead rule, on the grounds that changing generated content pulls fixtures with it. Measured, it does not: the harness renders its fixtures by calling the script's OWN generators (render gen_squid_conf, run-tests.sh:312-318), never by copying config text into the test file, so generated-content changes cost nothing in fixture maintenance. That removed the only argument for the weaker fix.
Safe in both directions by construction -- stated because the source-selection step is REASONED, not yet measured on the rack (hard rule 2). A host connecting to one of its own local addresses is expected to select that address as the source, so ${LISTEN}/32 is the expected match; if that is wrong, the probe still falls through to snap_clients, because LISTEN is inside CLIENT_CIDR. So the change cannot regress check, and the first live run's access log will say which rule actually admitted the probe.
T23b (NEW) asserts the RENDERED artifact -- the probe ACL is the LISTEN address and is NOT a loopback rule -- never the comment that describes it. BUG-1's real defect was that a comment and its code disagreed, so an assertion on prose would repeat the defect.
Mutation-proven. Reverting the ACL to 127.0.0.1/32 turns T23b red (54/1); script restored sha256-identical.
What. A new block in scripts/dc-snap-proxy.sh's header, under the existing "READ THIS BEFORE TRIAGING A REFUSE" section. No behavior change.
Why not fixed. BUG-3 is that the DENY probe's expected string CONNECT tunnel failed, response 403 was MEASURED from a port deny (deny CONNECT !SSL_ports), never from the dstdomain deny (deny !snap_store) that the probe actually exercises. It cannot be fixed in the repo, because the correct value is a fact about a live squid that nobody has yet observed -- inventing one would be exactly the fabricated-value failure this repo calls the cardinal sin. Guessing a second plausible string would be worse than the bug.
Why it is not merely cosmetic. If squid answers a dstdomain denial differently, that branch falls to refuse -> exit 3, and check can then NEVER PASS. Under GA-R6 a gate that cannot PASS blocks a stage close exactly as hard as one that cannot fail, so this is a real gate defect and not a probe nicety.
The protocol is now IN THE FILE rather than in a queued-findings entry the next reader will not open: a REFUSE on the deny probe at first run is the EXPECTED, HANDLED path; read the actual output; re-point the assertion to the MEASURED shape; keep the three-outcome structure so an unrecognised result still REFUSES; prove the new assertion can fail. This is also the answer to the close-out's "confirm BUG-3's deny shape once" -- it is a byproduct of the first real check, not separate work.
BUG-2 NOT FIXED, unchanged from N4. | grep -q under set -o pipefail is INHERITED and present identically in dc-mirror.sh:278,281 and dc-cache-proxy.sh:234,252,255. A three-script sweep is out of scope under hard rule 1 and under the standing directive. Its direction is fail-closed (a false MISS, never a false OK), which is why it can wait.
What. No artifact change. Recorded because it relaxes the scope of hard rule 3 for one task and must not be inferred later from silence.
Presented to the operator, verbatim as put: .claude/settings.local.json carries Bash(ssh vr1-dc0-rack *) in allow -- an unrestricted wildcard onto the dc0 rack, auto-added by an approval on 2026-07-30 and flagged in that session's sweep (F1) as "worth review". Its consequence, stated at the time of asking: a background agent can mutate the dc0 rack (including sudo bash -s -- install dc0) with NO permission prompt reaching the operator, so hard rule 3's individual gating does not fire for rack commands. The operator was offered a narrowing.
Operator answer, exact utterance: "The agent permissions are approved".
CONSEQUENCE, scoped deliberately narrowly. This authorises the snap-proxy install agent to operate on the dc0 rack under the existing allow rule. It is NOT a general standing relaxation of hard rule 3, it does not extend to dc1, and it does not cover the bundle deploy -- the agent's mandate explicitly stops before add-model / deploy / any juju model-config. A future session must not read this as blanket approval for unsupervised cloud mutation.
Not a GA-R5 ruling and deliberately not recorded as one: it is a harness permission decision, OPS under GA-R3 (no architectural consequence beyond this stage, no Roosevelt delta), so it takes no D-number. The gitignored settings.local.json is unchanged by this session.
Revert. Remove Bash(ssh vr1-dc0-rack *) from allow in .claude/settings.local.json and replace it with a targeted ask rule pinned to the invocation shapes actually needed. Note the 2026-07-30 lesson that applies directly here: quoting style and the profile token are BOTH part of the match, so a narrowing rule that pins the wrong quote form silently fails to match and falls through to the classifier.
bash tests/dc-snap-proxy/run-tests.sh -- 55 passed, 0 failed (52 -> 55: T20b, T23b, T41b added; T41 re-pointed).bash scripts/run-tests-all.sh -- GAUNTLET: ALL GREEN (94 harnesses). HARNESS-MANIFEST NOT re-recorded and deliberately so: no harness was added or removed, only cases within an existing one, so the manifest is unchanged and the R15(2) re-record reason does not apply.bash scripts/repo-lint.sh -- 0 fail, 1 warn (the expected L1 legacy non-ASCII carve-out in docs/design-decisions.md).What. No repo artifact changed. A LIVE MUTATION on the dc0 rack, executed by a delegated agent under the operator approval recorded in item 5. Capture: docs/audit/dc0-snap-proxy-install-20260731.txt (739 lines, ASCII/LF).
NAMED GATE, GA-R6: dc-snap-proxy.sh check dc0 = PASS, EXIT 0, 16 assertions. RE-RUN INDEPENDENTLY from the main session after the agent reported, rather than taken on its word: squid 6.14 bound to 10.12.8.4:3129 SPECIFICALLY (not wildcard), both units active AND enabled, packaged squid.service neither active nor enabled, all five owned artifacts matching their generators BY CONTENT, and all three behavioral probes green (store API with channel-map + sha3-384, CDN tunnel allowed, non-allowlisted destination DENIED).
The before-state was captured and it FAILED (exit 1) on a real rack -- so the gate is proven able to fail somewhere other than a fixture. It was NOT uniformly failed: three OK lines (virsh-resolved bridge, rack route table) prove the instrument was on the RIGHT HOST. That distinction is the recorded wrong-host trap for these per-DC scripts, where a jumphost run reports every item MISS.
MAAS's squid was untouched throughout -- same pid (1871633) before and after, still holding *:3128 / *:8000. Net-layer idempotence with dc0-mirror-net.service verified: virbr2 addresses and the default route are byte-identical before and after, which is the D-135 coexistence property this script was built to honour.
A REAL SNAP PAYLOAD WAS FETCHED THROUGH THE PROXY -- HTTP 206, and the first bytes are hsqs, the squashfs magic of the genuine core snap. 4 bytes of ~110 MB read and discarded; nothing installed. This is what moves the tool from FIXTURE-green to end-to-end proven, and it is the specific claim the prior session was careful to say it could not make.
STILL NOT PROVEN, stated because the deploy depends on it: no charm LXD container was tested. All nine dc0 nodes are shut off (measured via virsh), so the "containers source from metal-admin" premise underpinning CLIENT_CIDR was NOT re-proven and is carried forward from the prior session's measurement. snapd's own consumption is unproven and out of scope (the juju keys are unset, so node's /etc/environment assertion FAILS as expected). dc1 untouched.
Revert. ssh vr1-dc0-rack 'sudo systemctl disable --now dc0-snap-proxy.service dc0-snap-proxy-net.service', then remove the five generated artifacts named in the script header. The packaged squid remains installed and disabled; MAAS's squid is unaffected either way.
What. No change. The confirmation BUG-3 was waiting for.
The dstdomain deny (deny !snap_store) produces a shape BYTE-IDENTICAL to the port deny the assertion was originally written from: curl exit 56, CONNECT tunnel failed, response 403, http_code 000, with squid logging TCP_DENIED/403 ... HIER_NONE. Measured by hand with stderr both merged and unmerged, not inferred from check's own say-line -- which matters, because reading the gate's verdict to confirm the gate's assumption is circular.
MECHANISM, so this is not re-opened later: curl reports the CONNECT response status, and squid renders 403 for ANY http_access deny regardless of which ACL fired. The two shapes coincide by construction, not by luck.
CONSEQUENCE: BUG-3's protocol steps (b) and (c) were correctly NOT performed, and scripts/dc-snap-proxy.sh + its harness are sha256-identical to their pre-install state. The header block added in item 4 stays as the record of why the question existed and how it was settled. check dc0 can now be cited as a gate.
What. tests/dc-snap-proxy/run-tests.sh T23b gains a prominent in-file annotation. scripts/dc-snap-proxy.sh deliberately NOT changed.
Item 3 changed acl snap_probe src 127.0.0.1/32 to src ${LISTEN}/32 and stated the source-selection step as REASONED, NOT MEASURED. The measurement says it is wrong. Measured on the dc0 rack, and re-run independently from the main session:
$ ip route get 10.12.8.4 local 10.12.8.4 dev lo src 10.12.8.2
.4 is a SECONDARY on virbr2 (primary .2), and Linux does not auto-select a secondary address as a source. Squid's own access log attributes every rack-side probe to 10.12.8.2. So the rule is STILL DEAD -- BUG-1's fix corrected the mechanism DESCRIPTION and landed on the WRONG ADDRESS.
Nothing is broken. The probes are admitted by snap_clients exactly as before, and check dc0 passes. What remains unconnected is the rule's stated PURPOSE: that narrowing snap_clients must not silently break check.
NOT FIXED NOW, and the reason is not laziness. check diffs the generated config against the rack's LIVE file, so any edit to gen_squid_conf turns the live dc0 gate RED until install is re-run. That makes it a LIVE MUTATION on a just-verified surface, out of scope under hard rule 1 and against the standing directive. The correct value is the rack's primary metal-admin leg (dc0 10.12.8.2; dc1's 10.12.68.2 is already recorded MEASURED in lib-hosts.sh), and it wants a site-keyed row. Fix the script and T23b TOGETHER at the next install -- dc1 is the natural point, so both DCs land in one pass.
T23b is annotated rather than deleted or re-pointed, per the standing rule that a test asserting a finding must be REPLACED with the new invariant and never deleted to go green. Editing its expected string to 10.12.8.2 today would turn the harness red against the live config; leaving it unannotated would let a future session "fix" the test to match a value that is itself wrong. The annotation states the measurement, the correct value, and why the change is deferred.
GENERALISABLE, and it is the reusable half: a secondary IPv4 alias is never the kernel's chosen source address. Any ACL, firewall rule or allowlist keyed to a service's ALIAS address -- which is exactly what the D-134 utility .4 band is on both racks -- will not match that host's own outbound traffic. This applies equally to dc-mirror.sh and dc-cache-proxy.sh, which share the .4 alias pattern.
Revert. git checkout -- tests/dc-snap-proxy/run-tests.sh.
core's download URL "came back on api.snapcraft.io, not a CDN host -- so the CDN entry may be unused." That measured only the FIRST HOP: the URL 302-redirects to canonical-bos01.cdn.snapcraftcontent.com. The .snapcraftcontent.com entry is LOAD-BEARING, not speculative, and the same run proves squid's dot-prefix wildcard admits a subdomain appearing in NO probe and NO config line. DOCFIX material against the header.apt-get install squid leaves a FAILED but still ENABLED unit (9c) that would fight for 3128 at the next boot -- the header's predicted FATAL: Unable to open HTTP Socket ... listening port: 3128 was observed VERBATIM. install closes it; an operator who installs the package and stops there does not.failed+disabled, not inactive (9e). check is right to pass, but systemctl --failed shows red on a healthy rack, which is quiet triage-poisoning. systemctl reset-failed squid is the remedy; NOT applied (a live mutation outside the mandate).ssh vr1-dc0-juju -> Permission denied (publickey). Not a permission-harness refusal and NOT retried in an altered shape. This blocks the next phase, which is add-model + spaces + deploy under D-138 (cloud-facing clients run from INSIDE the DC). Owed before that phase starts.What. docs/audit/gai-conf-rfc6724-verification-20260801.txt (NEW) + docs/CURRENT-STATE.md. No script, no cloud change, and no edit to D-139 itself.
Why it was checked at all. D-139 ruling B was recorded WITH its own unverified premise named in the entry: "UNVERIFIED and owed before the carve is executed: the deployed jammy image's own /etc/gai.conf, which can override the RFC 6724 default table that ruling B's deciding reason rests on." Closing that owed item is what produced this. The discipline of naming an unverified premise at ruling time is the only reason this was caught before the carve rather than after it.
MEASURED, in two steps, neither inferred.
libc-bin 2.35-0ubuntu3 ships /etc/gai.conf with ZERO active lines -- every line commented. So the node image applies NO override and glibc's compiled-in table governs. (noble/vcloud measured identically.)default_precedence[], fetched in full from sourceware at tag glibc-2.35 and read rather than recalled, carries glibc's OWN comment /* See RFC 3484 for the details. */ and is: ::1/128 -> 50, 2002::/16 -> 30, ::/96 -> 20, ::ffff:0:0/96 -> 10, ::/0 -> 40. There is NO fc00::/7 entry.THE INVERSION. ULA has no precedence entry, so it falls through to ::/0 = 40 -- identical to GUA. IPv4-mapped is 10. So on the node image ULA = GUA = 40, both outranking IPv4 = 10. Ruling B was declared on "ULA loses to IPv4 (3 vs 35), GUA wins (40 vs 35)" -- the RFC 6724 table. RFC 6724 obsoletes RFC 3484 and does define those values; glibc 2.35 does not implement it. The claim is correct about the RFC and wrong about the software this cloud runs.
The fc00::/7 entry that DOES exist is in default_labels[] (label 6), beside fec0::/10 (5) and 2001::/32 (7), under glibc's comment "The next two entries differ from RFC 3484". Labels drive SOURCE-selection rules 5/6 -- a different mechanism from destination precedence, and not what the ruling cited. Recorded so the two are not later conflated into "glibc does handle ULA specially, so the argument stands".
This is the citation class again, third instance. A citation is an EXISTENCE claim; only its CONTENT is evidence -- and here the citation (RFC 6724) was real, current and correctly quoted, yet still wrong for this deployment because nobody had checked whether the implementation follows the RFC. That is a NEW variant worth naming: it is not enough to open the source and check its status and dates; for a standards citation you must also check that the software actually implements it.
WHAT SURVIVES, and why the ruling is not simply withdrawn. Three of ruling B's four reasons are untouched: conformance with Willamette (a REAL site) and VR0 DC0, both full GUA; MINIMIZE DELTA TO ROOSEVELT, whose 2602:f3e2:103::/48 is GUA with no ULA and no plane carve yet; and isolation being unaffected either way because containment lives at the FORWARDING layer. Those are the governing-constraint arguments and are independent of the refuted one.
FLAGGED, NOT AMENDED, and this is deliberate. GA-R5 makes a ruling the operator's. They answered a question whose stated DECIDING reason no longer holds, so the honest action is to put the refutation back to them -- not to silently keep the ruling because I still like the answer, and not to silently reverse it. D-139's text is UNCHANGED; CURRENT-STATE carries the refutation and the flag. Nothing of D-139 is executed, so nothing is half-built in either direction and the reconsideration costs nothing but the exchange.
Revert. git rm docs/audit/gai-conf-rfc6724-verification-20260801.txt and drop the CURRENT-STATE paragraph. Note that reverting does not un-refute the measurement.
What. docs/design-decisions.md -- D-139's deciding-reason paragraph replaced by a STRUCK block, plus a new ### RULING NOTE 2026-08-01 -- D-139: at the end of the entry + docs/CURRENT-STATE.md (flag cleared). No script, no cloud change.
Question as presented. The RFC 6724 precedence argument given as ruling B's DECIDING reason is refuted for this deployment (item 10); on jammy's glibc 2.35, ULA and GUA are behaviourally EQUAL. Three reasons survive independently. Does ruling B stand on those alone? Nothing of D-139 is executed, so reversal costs only the exchange.
Operator answer, exact utterance: "Deciding to hold to gua does not cost anything operationally and the case for ULA over gua is not very strong. Let's stay with gua".
RULED: ruling B STANDS, unchanged in substance. The GUA carve table is confirmed, the ULA /48 stays RETIRED for VR1, the D-101/D-111 amendments hold, and Phase-2 execution is UNBLOCKED.
The refuted argument is STRUCK IN PLACE rather than deleted. D-139's original text is quoted inside the strike block, with the measurement that kills it and a pointer to the capture. Deleting it would have been the easier edit and the wrong one: a future session would have no way to know the argument had been considered and refuted, and would be free to re-derive it. The strike block says explicitly DO NOT CITE IT, and separately warns against the near-miss rescue -- glibc 2.35 DOES carry fc00::/7 in default_labels[] (label 6), which drives SOURCE-selection rules 5/6 and is a DIFFERENT mechanism from destination precedence. Someone finding that entry could easily conclude the struck argument survives after all. It does not.
The rationale of record is now narrower, and that is recorded as a CONSEQUENCE rather than buried. Ruling B rests on two project-constraint arguments -- conformance (Willamette and VR0 DC0 are already full GUA; VR1 was the outlier) and MINIMIZE DELTA TO ROOSEVELT -- plus the operator's own reasoning that GUA costs nothing operationally. Made explicit because it was NOT visible before: on glibc 2.35, GUA buys no address-selection advantage on the two dual-stack planes; ULA would have won equally at precedence 40. A later session reasoning "GUA was chosen so v6 would beat v4" is reasoning from the struck argument.
NEW STANDING RULE, added to D-139's note because it generalises past this decision. The repo's citation rule is "a citation is an EXISTENCE claim; only its CONTENT is evidence" -- open it, check STATUS and DATES. That was FOLLOWED here and was NOT ENOUGH: the citation was real, current, correctly quoted and correctly understood, and still wrong, because nobody checked whether the IMPLEMENTATION follows the standard. For a STANDARDS citation, add a third check: confirm the deployed software implements it, at the deployed version. An RFC is not a description of your system.
Revert. git checkout -- docs/design-decisions.md docs/CURRENT-STATE.md restores both the original deciding-reason paragraph and the flag. Reverting does not un-refute the measurement, and the ruling note quotes an operator utterance -- so a revert would DESTROY a GA-R5 record and must not be done casually.
What. docs/audit/stage5-juju-client-blocker-20260801.txt (NEW, 391 lines) + docs/CURRENT-STATE.md. No script, no cloud mutation, no key moved.
Item 9 recorded finding 9d as "BLOCKS the next phase". That was WRONG and is corrected here. The juju client exists, is registered and is live -- on the dc0 RACK, which is exactly where D-138 places it. Stage 5 can reach add-model today. Verified from the main session rather than accepted from the agent: juju 3.6.27-genericlinux-amd64 at /snap/bin/juju; controller vr1-dc0-controller* admin/superuser on cloud vr1-maas; the controller model reading "Last connection: just now"; and client credentials listing exactly one entry.
Root cause of the ssh refusal, measured ON THE MACHINE. The alias is correct in every part. subtle-grouse's authorized_keys holds exactly two lines, both Juju's -- so the office1_svc_ed25519 the alias offers is genuinely absent, and the refusal is right. The obvious hypothesis was REFUTED rather than assumed: the machine booted AFTER the 2026-07-30 region key import and still received only Juju keys, because deploy-time cloud-config.txt carries Juju:juju-client-key alone and juju ssh-keys -m controller reports none. juju ssh -m controller 0 already works, so operator ssh is convenience, not requirement.
A NEGATIVE FINDING WAS TIED TO IDENTITY BEFORE IT WAS TRUSTED -- machine 0's two addresses, hostname and host key were all matched to what ssh -v actually reached. This repo's standing instrument rule is that a confident negative needs proof the instrument was pointed at the right thing; here it was, and it is recorded.
SEC-026 control (1) discharged by measurement on BOTH sides: the rack lists ONE credential while voffice1 lists two, so the forbidden whole-store copy did not happen. No new credential residency and NO new ledger row is owed -- every location the fix touches is already in creds-manifests/vm-secret-locations.
Only hygiene is owed: a dangling current-model: admin/vr1-dc0 (the model destroyed 2026-07-31) makes bare juju status error; juju switch controller clears it. Measured NOT to be an add-model precondition -- the runbook's own guard shape works regardless. NOT EXECUTED (hard rule 3; it is a state change on the live client, presented not taken).
FOUR GAPS LOGGED NOT FIXED, in consequence order:
openstack CLI is MISSING ON THE RACK (three instruments agree). A D-138 definition-of-done gap that blocks phase-03+. Same item as F1 from the 2026-07-30 sweep, now re-confirmed on the D-138 host rather than on voffice1 -- so it has survived two sweeps and should stop being re-discovered.overlays/vr1-dc0-octavia-pki.yaml is gitignored PKI material (0600 on the rack, absent from the vcloud tree) and scripts/pre-flight-checks.sh:161 hard-fails on it. Code-cited, not executed. This is a gate that cannot pass on one host and can on another -- the host-dependence class this repo already records for dc-mirror.sh and P5/P7.~/repo-stage on the rack is a hand-staged 9-file copy with no .git; 8 of 9 are byte-identical to the repo by sha256, so it works today. It is a THIRD copy beside the two clones the 2026-07-31 entry verified at c58bf95 before trusting any gate output, and the 2026-07-27 stale-clone hazard is precisely why that verification existed.runbooks/dc-dc-phase4-juju-bundle-per-dc.md:379,398 still say "voffice1" for the add-model and spaces steps, stale against D-138. Measured: voffice1 HAS the binary but juju controllers returns "No controllers registered", so the runbook as written FAILS. DOCFIX owed, deliberately not numbered mid-flight.DECLARED UNMEASURED, with the reason: whether MAAS user juju-vr1-dc0 has an imported ssh key -- the DISTAL half of the root cause. There is no usable maas CLI profile anywhere (vcloud has none; voffice1's ~/.maascli.db is ZERO BYTES with no profiles table, the inert residue already recorded 2026-07-30). It does not change the fix, because the proximate cause was measured on the machine directly. The PreToolUse guard refused a command that would print the MAAS API key (DOCFIX-016) and it was NOT retried in an altered shape -- so ~/vr1-dc0-creds/ and the rack's profile state are unenumerated, and SEC-028's distribution claim is carried from the ledger rather than re-measured. Both the guard firing and the non-retry are the correct outcomes and are recorded as such.
Revert. git rm docs/audit/stage5-juju-client-blocker-20260801.txt and drop the CURRENT-STATE paragraph. Nothing live was changed, so there is nothing else to undo.
What. netbox/d139-gua-carve.py (232 lines) + tests/d139-gua-carve/run-tests.sh (151) + tests/d139-gua-carve/stub-apex.py (100, an offline NetBox stand-in) + docs/audit/d139-carve-dryrun-20260801.txt (97) + tests/HARNESS-MANIFEST 94 -> 95.
Why. D-139 ruling B execution step 1. Dry-run is the DEFAULT and the write path is explicitly opted into; nothing was written to NetBox in this session.
Live dry run, both DCs: CREATE 22 | EXISTS 6 (untouched) | RETIRE-REPORT 18. The f0X:10::/60, f0X:10::/64 and f0X:11::/64 provider-public rows already exist and are correctly skipped rather than recreated.
THE REPORT-ONLY PASS EARNED ITS KEEP: it surfaced 52 DEPENDENT ip-addresses living inside the retiring ULA /64s -- 26 per DC, in :220/:221 and :320/:321 -- which a later delete would ORPHAN. Those are the v6 VIP legs. 0 dependent ip-ranges. Retirement is REPORTED, never executed, so this is a finding rather than an incident.
Proof no write occurred, asserted on CONTENT rather than on the tool's own say-so: apex prefix count 139 before and 139 after, and none of the 22 CREATE rows exists in the apex.
Mutation-proven, 5 mutations, each turning a NAMED test red; tool restored and sha256-verified byte-identical after each: storage hextet 0x40->0x41 (T07/T08 red, and the /60-alignment guard REFUSEs); UA = "curl/8.5.0" removed (T15 red, plus 42 others -- the stub 403s exactly as the real apex does); the SANDBOX_HOSTS refusal neutered (T04 red, "did not refuse the baldurkeep upstream"); the retire predicate widened from the ULA /48 to a GUA /36 (T10 red, all 9 rows); read-back gone = [] (T17 red).
Credential hygiene held: only vr1-netbox-sandbox.env was sourced, never printed, and the tool hard-REFUSES any host outside {localhost, 127.0.0.1, 10.10.1.10}, naming DOCFIX-195 when it does. The two NetBox instances stayed separate throughout.
Charter compliance, since it was a graded requirement: 232 lines, of which a 12-line docstring and 5 comment lines in the entire file (longest block 2 lines). Zero re-argued rationale, zero worked examples, zero alternatives-considered. The length is code -- refusals, conflict detection, read-back, the write path -- not prose. One soft criterion missed and declared: main() is ~90 lines, so "one screen per function" does not hold there.
Harness 65/65; gauntlet ALL GREEN (95); repo-lint 0 fail.
Name deviation, deliberate: d139-gua-carve.py, not -import.py. The D-NNN family in netbox/ is 3/3 without the suffix (d115-office-carve.py, d120-compose-bands.py, d124-transit-seed.py); -import belongs to the dc-* family.
Revert. rm netbox/d139-gua-carve.py; rm -rf tests/d139-gua-carve then bash scripts/run-tests-all.sh --record-manifest. Nothing in the cloud or the apex changes either way.
What. docs/audit/v6-only-charm-viability-20260801.txt (355 lines) + docs/CURRENT-STATE.md. No artifact changed, nothing actioned.
Method, and it is the one this repo learned the hard way: the charms were DOWNLOADED and READ. prefer-ipv6 was researched the same way on 2026-07-31 and the research inverted its own premise; the same discipline is why this result is trustworthy rather than plausible.
RE-VERIFIED INDEPENDENTLY by the main session against the shipped ceph-osd rev 953 artifact, because it is the claim that decides the ruling -- hooks/utils.py:203-217: get_host_ip() returns get_ipv6_addr()[0] when prefer-ipv6 is set, else runs 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, which is what makes it a block rather than a config question: prefer-ipv6: false -> inet_aton raises on a v6 literal -> NXDOMAIN on an IN A lookup (LP #2109798, New, whose reporter's only workaround is "forcing the ipv4 cidr"); prefer-ipv6: true -> bare get_ipv6_addr() (also ceph_hooks.py:547, WITHOUT dynamic_only=False) which returns DYNAMIC/SLAAC addresses only, while every VR1 node holds a MAAS static v6. That is LP #2061836 -- Fix Committed on charm-ceph-mon, New and never fixed on charm-ceph-osd, confirmed absent from rev 953.
The other two families are reported from the capture and NOT re-verified here -- recorded as strong sourced observations, not as measured-here facts: metal-internal's mysql/mysql- router bare user:pw@addr URIs (invalid for an unbracketed v6 literal) and hacluster's corosync ip_version: ipv4, hard-written in 2.4/stable rev 166 released 2026-06-29 even though LP #2111852 is Fix Committed (Fix Committed is not Fix Released -- a distinction this repo has been bitten by before); and OVN documenting the encap column as "The IPv4 address of the encapsulation tunnel endpoint" with zero v6 encap values anywhere in OVN 24.03's test suite.
Two things counted locally to size it: metal-internal appears 273 times in bundle.yaml across ~44 applications, so it is the most pervasive binding in the deployment; and rabbitmq-server is deployed by the bundle but ABSENT from PREFER_IPV6_CHARMS -- the only mismatch across all 33 bundle pairs, on which invariant 9a would emit a factually false message.
CONSEQUENCE, stated as a decision rather than actioned: ruling A's five-plane v6-only scope is blocked by UPSTREAM CHARM DEFECTS, not by anything in this deployment. Ruling B is ORTHOGONAL and STANDS -- GUA-versus-ULA is independent of whether a plane keeps v4 -- so item 13's tool is unaffected and a GUA DUAL-STACK carve is achievable today.
THREE ITEMS OWED. (a) The foundational measurement is STILL NOT TAKEN: does network-get return a v6 address on a v6-only bound space? Whatever survives of the storage/replication verdict rests on it. (b) D-139's "remove the v4 subnets LAST, after each is proven" is UNSATISFIABLE as written -- per-plane conversion is atomic, so no state exists in which a plane is proven v6-only while still holding v4. That clause needs rewriting under any ruling. (c) The rabbitmq-server mismatch above.
PROCESS FAILURE, OWNED, AND THE MOST REUSABLE PART. This research was written to /tmp/v6research/FINDINGS.txt and would have been lost at the session boundary. The agent was scoped read-only and given NO repo path for its findings -- my error in writing the mandate, not the agent's. Rescued and sha256-verified identical. Standing consequence: every agent mandate must NAME A REPO PATH for its findings. A read-only mandate still produces a durable artifact; "read-only" constrains what it may CHANGE, not where it may RECORD.
Revert. git rm docs/audit/v6-only-charm-viability-20260801.txt and drop the CURRENT-STATE paragraph. Nothing was actioned, so there is nothing else to undo.
Operator answer, exact utterance: "B plus C". Options as presented: (a) carve GUA dual-stack now and defer v6-only; (b) v6-only on replication alone as a bounded experiment; (c) pursue the upstream fix (LP #2061836's ceph-osd change is ONE WORD, already merged on ceph-mon). (a) not selected.
INTERPRETATION STATED SO IT CAN BE CORRECTED: ruling B's GUA carve is unaffected and proceeds; ruling A's five-plane v6-only scope narrows to the experiment; metal-internal, data-tenant and lb-mgmt stay DUAL-STACK pending upstream fixes. Recorded as an interpretation, not as the utterance.
storage and replication.MEASURED against shipped ceph-osd rev 953, ceph_hooks.py:544-553:
if config('prefer-ipv6'):
cephcontext['ms_bind_ipv4'] = False # GLOBAL
cephcontext['ms_bind_ipv6'] = True
dynamic_ipv6_address = get_ipv6_addr()[0]
if not public_network: cephcontext['public_addr'] = dynamic_ipv6_address
if not cluster_network: cephcontext['cluster_addr'] = dynamic_ipv6_address
prefer-ipv6 is ONE switch setting 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 as "smallest blast radius" -- that is not achievable, and the real experiment is storage + replication as a PAIR, with a correspondingly larger blast radius than I represented.
A PATH THE EARLIER ANALYSIS MISSED, and it partly rescues v6-only for Ceph. The buggy bare get_ipv6_addr() (LP #2061836, dynamic/SLAAC-only, never fixed on ceph-osd; 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 that call is never used for address selection -- the dynamic-only defect becomes AVOIDABLE BY CONFIG rather than blocking. Same lever LP #2109798's reporter pulled in the opposite direction ("forcing the ipv4 cidr for the ceph-cluster-network and ceph-public-network options").
CONFIDENCE, stated because it changes what may be built on it: this is a CODE READING, NOT A MEASUREMENT. It has not been run, and it does NOT address the get_mon_hosts() / get_host_ip race (LP #2109798), which fires in any window where ceph-public-address is not yet published. Still owed before the experiment: does network-get return v6 on a v6-only bound space? -- still not taken.
netbox/d139-gua-carve.py (independent second agent)What. netbox/d139-gua-carve.py + tests/d139-gua-carve/run-tests.sh edited; docs/audit/d139-carve-review-20260801.txt (NEW, 364 lines). Per the operator's standing instruction that every new script gets a review-and-streamline pass after delivery.
Verdict: not "no change needed", but close. The tool was already at charter prose weight (5 comment lines in 232). 4 real defects FIXED, 4 reported-not-fixed, 1 declared charter miss UPHELD.
D1, the consequential one, and it is the mandate's own named class "a refusal that does not refuse": an unreachable apex produced a raw traceback and EXIT 1 -- which the tool's own header defines as a write/read-back error -- on a dry run that wrote nothing. _req caught only HTTPError, so URLError walked past the handler that exists to make this REFUSE. Measured pre-fix (ConnectionRefusedError [Errno 111], RC=1); two-line except OSError -> REFUSE(2).
D2 a comment asserted ruling B was "FLAGGED for operator reconsideration" -- already FALSE at commit time (bbc0330 confirmed it 35 minutes earlier). Replaced with a bare citation. This is the charter's "cite, never re-argue" earning itself: prose in a script rots against governance state; a citation cannot. D3/D4 replaced two greps over the tool's OWN SOURCE TEXT (existence, not content) and two vacuous-negative assertions with behavioural coverage.
main() decomposition DECLINED, and the argument is reusable: the if not a.commit: return 0 guard sits ADJACENT to the write block; extracting commit_writes() would split that safety proof across two places, for a script whose entire claim is that the write path is opted into. Line count is not the criterion -- whether a reader verifies a safety property in fewer places is. The charter's "one screen per function" is a heuristic for that, not the goal itself.
R1 IS A REAL SECURITY FINDING, MEASURED NOT REASONED: urllib carries the Authorization header across a CROSS-HOST redirect -- proven with a loopback probe in which the target received the token verbatim. SANDBOX_HOSTS gates the SUPPLIED url, not a redirect FROM it. LOW severity (requires the approved apex to redirect hostilely) and a redirect handler is untested code on a control path, so logged not executed. R2 unwrapped read-back, R3 errs double-count (tally only), R4 subnet_of equality (measured not to occur live).
Verification: harness 65/65 -> 71/71. ALL 5 ORIGINAL MUTATIONS RE-RUN on the edited tool and all still stand, plus 3 new (M6 reverting the D1 fix -> T18 red; M7 zero-scope guard; M8 a print-format drift invisible without the new guard). Tool restored from a pristine snapshot rather than git checkout (shared clone), sha256-verified after each. Gauntlet ALL GREEN (95). Dry-run body DIFFED BYTE-FOR-BYTE against the committed capture -- asserted on content, not counts -- apex prefix count 139 before and after, --commit never passed.
QUALIFICATION THE REVIEWER DECLARED RATHER THAN LEFT IMPLICIT: a byte-identical dry run proves the READ/PLAN path unchanged, because a dry run executes zero POSTs by construction. D1 also changed the WRITE path's failure semantics -- a connection error during a POST is now caught and counted, so the loop CONTINUES instead of aborting, and errors=N is reachable with nothing transmitted. Deliberate, out of scope to undo, UNTESTED.
PROCESS FAILURE REPEATED AND OWNED. The review mandate REQUIRED a docs/audit/ capture and FORBADE touching docs/CURRENT-STATE.md -- jointly unsatisfiable against repo-lint L10. The agent correctly left the tree RED and SAID SO rather than working around it, which is the right behaviour. Same class as the /tmp loss earlier today. Standing fix, now twice learned: an agent mandate must name a repo path for findings AND leave the committer able to satisfy L10.
Revert. git checkout netbox/d139-gua-carve.py tests/d139-gua-carve/ restores the pre-review tool; note that D1 was a real defect and reverting reinstates it.
What. docs/audit/g19-ipv6-node-plane-verify-20260801.txt (NEW, 97 lines) + docs/CURRENT-STATE.md. A live experiment: operator-approved MAAS deploy of TWO storage nodes (t7ymp6, fg6gxm, jammy) purely to open a boot window, issued INDIVIDUALLY rather than in a loop. Region asserted before any MAAS call -- vr1-dc0-region -> one rack controller hot-kid; admin -> the three Office1 racks. maas-profile-assert.sh is MISSING on voffice1 (its clone is behind HEAD), so the same property was asserted inline rather than skipped.
A1 -- addresses come up. PASS. Six global v6 addresses live on the NICs, matching MAAS exactly, 0 tentative / 0 dadfailed, on-link /64 routes present, no v6 default (expected). Asserted on the INTERFACE, never on MAAS, and this is the first time it has been checked on a ROLE NODE at all.
A2 -- the plane carries v6 between two nodes. PASS on all six, 0% loss, every neighbour REACHABLE. Designed to catch one specific thing and it did the opposite: the risk I flagged is REFUTED for this path. Every plane bridge measures multicast_snooping=1 / multicast_querier=0, a known source of ND failure. The neighbour table was COLD, so the first solicitation had to go out as MULTICAST -- and resolved on all six planes. HONEST RESIDUAL: this proves ND from cold; it does not prove behaviour across long idle periods where snooping entries age out. A minutes-long test cannot show that.
A3 -- G17's role-node half, OPEN since 2026-07-30, is captured. PASS. The earlier dc0 G17 capture was taken on the CONTROLLER VM and said so. From a role node: the mirror's real package path returns exit 0 / HTTP 200 / 269219 bytes with all four body fields. And the 2026-07-31 deploy blocker is confirmed FIXED FROM A REAL NODE -- all four suites answer 200, jammy-backports included, where it was 404 and put 22 units into hook failed: "install". The D-135 backports amendment is BUILT, not merely ruled.
A4 -- THE HEADLINE, and it inverts the framing this whole document has used: the node's time source is ALREADY IPv6. ServerName = fd50:840e:74e2:220::6, ServerAddress decodes as family 10 = AF_INET6, and timesync-status shows Stratum 3, root distance 57.327ms, poll interval backed off to 2min 8s -- a healthy sustained sync -- with ntp.ubuntu.com unused as fallback. G17 assertion (2) passes (MAAS-served, not the DC edge, per D-129(iv)), and separately IPv6 is not a future state here: it is a live operational dependency that predates every line of this verification work. Every prior surface discusses IPv6 as something to be built; one of it is already carrying production traffic.
G17's own text re-confirmed defective: it names chronyc sources and chrony is NOT on the MAAS jammy image, so the gate as written can only ever REFUSE. DOCFIX still owed.
F7 CLOSED IN PASSING: the 2026-07-31 sweep recorded "STILL UNVERIFIED: that a redeploy applies the new hostname". It does -- both machines report their ruled names from hostname on the running OS, not merely in the MAAS record.
SCOPE, stated not glossed. These are ULA addresses, the PRE-D-139 carve; D-139 moves every plane to GUA, so the ADDRESSES are superseded by design. The PROPERTY tested is family-agnostic and transfers unchanged. Nothing here tests a charm, a container, network-get, or Ceph -- those remain the untested rungs, and this result must not be read as covering them.
THE NODES ARE DELIBERATELY LEFT Deployed. The G19 gate script is still being built, and leaving the window open lets it run against a LIVE node instead of shipping fixture-green -- the snap proxy's harness was green for a week before anything was proven end to end. Release is owed once the gate has run.
Revert. maas vr1-dc0-region machine release t7ymp6 and ... fg6gxm return both to Ready (enable_disk_erasing_on_release=false, so it takes minutes); git rm the capture and drop the CURRENT-STATE paragraph. Nothing else was mutated.
scripts/dc-node-v6-verify.sh, the IPv6 plane gateFOLDED IN FROM docs/changelog-20260801-g19-node-v6-verify.md, which the build agent wrote as a separate file and which is REMOVED here. GA-R2/D1 is ONE changelog per SESSION, never per topic -- two files means two review surfaces. The session crossed midnight, which is why the agent dated a new one; that does not start a new session. Original text preserved verbatim below the rule.
What. New named executable gate answering two DIFFERENT questions: do the DC nodes actually bring their IPv6 statics UP on every plane (asserted on the INTERFACE, never on MAAS), and does a plane CARRY IPv6 between two nodes (reachability to a peer plus the ip -6 neigh state that ping alone cannot distinguish from a lie). Plus a rack-side diagnostic that reports each plane bridge's multicast snooping/querier state so a red data-path result can be triaged in one step.
Files added
| Path | Role | ||
|---|---|---|---|
scripts/dc-node-v6-verify.sh |
the gate: plan \ |
node \ |
bridges |
tests/dc-node-v6-verify/run-tests.sh |
55-case harness, both directions driven | ||
docs/audit/g19-ipv6-plane-gate-build-20260801.txt |
build capture, live readings, mutation results |
File changed: tests/HARNESS-MANIFEST (+1 line, dc-node-v6-verify). R15(2) requires a deliberate re-record with a stated reason: a new harness directory was added. Without it the gauntlet's drift gate FAILS with "present but UNPINNED" -- the gate working correctly.
Governing decisions. D-139 (VR1 goes IPv6-only east-west; whole plane carve moves to GUA), D-101 (IPv6 unless IPv4 is necessary), D-134 (node octets). Rationale lives there and in the capture, not in the script.
The prefix constraint, and how it is resolved. D-139 retires the VR1 ULA /48 and moves every plane to GUA, so a site table of prefix literals would be correct today and wrong on carve day. There is no v6 prefix literal in this tool. plan derives the expectation from live MAAS and emits a per-machine SPEC; node takes that SPEC as input and knows no prefixes of its own. Harness T3 enforces the absence on non-comment lines; T14 drives the same green plan case with a ULA fixture and a GUA fixture.
Scope of a green, stated in the script header too: this asserts the NIC against MAAS, not against D-139's ruled GUA table. MAAS-vs-ruling belongs to dc-node-v6-carve.py check and tests/d139-gua-carve.
Gates. Harness 55/55. Gauntlet ALL GREEN (96 harnesses). repo-lint: 1 fail, 1 warn. The warn is the pre-existing legacy D-001..018 non-ASCII carve-out. The fail is L10 only -- this change set adds a docs/audit/ file without touching docs/CURRENT-STATE.md, which this session was instructed not to edit because the operator owns the G19 gate row. It clears when that row lands, in the same commit.
Mutation pass (each predicate neutered alone, harness re-run, restored, sha256sum verified identical -- 3ea87ced...c251ca before and after; the pass was re-run in full after the last message edit):
| Mutation | Predicate | NAMED test that went red |
|---|---|---|
| M1 | node contradiction branch [ "$prc" -eq 0 ] |
T37, T38 |
| M2 | node extra-global catcher [ -n "$extra" ] |
T31 |
| M3 | plan plane-set identity s != modal |
T15 |
Live, read-only, this session. bridges was run over the intended ssh <rack> 'bash -s -- bridges <site>' path on BOTH racks: all six plane bridges plus the WAN bridge read multicast_snooping=1 multicast_querier=0 on dc0 AND dc1, independently confirming and extending the dc0 measurement this gate was commissioned against. plan was exercised on vr1-dc0-maas and correctly returned REFUSE / exit 3 (no MAAS profile for the ssh user) -- the three-outcome contract proven on a real host. No cloud state was mutated.
Two open checks, both in the capture, neither red today. (a) The sole-global-per-NIC predicate would FAIL on whichever node holds a v6 HA VIP if D-139's ruled provider-public VIP /64 lands as a secondary on the same netdev as the node static. No exemption was built -- there is nothing to measure one against. (b) LOWER_UP on an OVS-internal br-ex is the one ip output shape this build never saw; no reachable host carries a br-ex. One ip -o link show dev br-ex on the first node that has one settles it.
Carried, not closed. No node currently carries these addresses up, so assertion (2) has never run against real traffic; this is a fixture-and-diagnostic green, not a plane that has been proven to work. And plan's use of machines read -> interface_set[].links[].subnet.cidr is corroborated by carve-host-interfaces.sh:120,293 but not yet seen live -- one plan run from a shell with a logged-in profile closes it. Both are in the capture.
Also logged, NOT executed (hard rule 1): dc-node-v6-carve.py pivots on "the interface already carries a v4 static", which selects nothing on the five planes D-139 strips of IPv4. It will silently under-carve after the v4-removal step. G19 does not inherit the defect -- plan pivots on the link's subnet being v6.
Revert. git rm scripts/dc-node-v6-verify.sh tests/dc-node-v6-verify/run-tests.sh docs/audit/g19-ipv6-plane-gate-build-20260801.txt docs/changelog-20260801-g19-node-v6-verify.md then bash scripts/run-tests-all.sh --record-manifest. Nothing else consumes these files: no runbook, no preflight.sh sub-gate and no other script references them, so removal is self-contained.
(1) Test nodes RELEASED. t7ymp6 / fg6gxm -> Ready / owner=None, released individually and read back. Nothing stranded. G19 was run live before the window closed, which was the whole reason for holding them.
(2) docs/design-decisions.md -- D-139 gains a CORRECTION NOTE 2026-08-01 replacing its EXECUTION list. Neither ruling is touched. The original list is preserved verbatim inside the note rather than silently overwritten -- the same reason the struck RFC 6724 rationale was kept: a later reader must be able to see what was wrong, or they will re-derive it.
Three defects, in consequence order:
dc-node-v6-carve.py assigns v6 "on every plane where it already carries IPv4", keys the subnet off "the SAME MAAS vlan as that v4 link", derives the host part from the v4 octet, and its loop body is if not v4: continue. Run after v4 removal it carves four fewer planes per node and exits clean. The original list's "remove v4 LAST" wording actively invited that ordering./64s.Replacement is 7 steps: CREATE-only apex push; MAAS GUA carve ALONGSIDE the ULA; node statics re-carved while v4 is still present; Octavia SAN reissue + lib-net.sh; lb-mgmt carve; re-home the 52 VIPs THEN retire; and v4 removal as a SEPARATE experimental step, scoped to storage + replication TOGETHER per "B plus C" -- together because ceph-osd's prefer-ipv6 sets ms_bind_ipv4=False globally. Step 7's prerequisite (network-get on a v6-only bound space) is stated as unmet.
(3) docs/audit/lp-draft-20260801-ceph-osd-ipv6-static.md (NEW). The "C" half of the ruling. A COMMENT on the existing LP #2061836, not a new bug -- the ceph-mon task is already Fix Committed and only ceph-osd remains New. Every code claim quoted from the downloaded rev-953 artifact, not from a repo note. Claude does not post to Launchpad; the operator files it, per the lp-draft-20260721 precedent.
Two things deliberately written INTO the draft's operator notes rather than left implicit: do not cite LP #1590598 (Fix Released, about IPv6-only host interfaces not being bridged -- citing it would be the inverted-citation class this repo has twice been bitten by); and this bug does not block us locally, because setting ceph-public-network / ceph-cluster-network to the v6 CIDRs bypasses the buggy call. Overstating our dependence on an upstream fix would be a bad-faith way to raise its priority.
Revert. git checkout -- docs/design-decisions.md docs/CURRENT-STATE.md (restores the defective execution list -- note that reverting reinstates a silent-failure ordering); git rm docs/audit/lp-draft-20260801-ceph-osd-ipv6-static.md. The node release is reversed by re-deploying, not by git.
NOTE ON ORDERING: the GA-R4 bookend (4b8ba3c) was committed BEFORE this work, so the ledger's close summary and the sweep do NOT describe items 21. Recorded here and in CURRENT-STATE; the ledger line is amended in the same commit rather than left stale.
Why now, operator's reasoning: "If there was any time to add more resources is now while the cost and risk is very low. Once we bring systems online within the host then the risks begin to expand." Correct, and the measurement agreed -- dc0's rack was running its inner guests at 402 GiB in a 409 GiB host, 98.3%, leaving 7 GiB for the rack OS, mirror, snap proxy and page cache.
SIZING RULED BY OPERATOR: +64 GiB each ("Option 2"), i.e. 416 -> 480 GiB per DC, WITH swap added first. I had recommended +48 and was overruled with a reason -- tenant testing is coming. The arithmetic I put up was the deciding input: at +64 each, allocation is 994 GiB against a 1007.4 GiB host, leaving 13.4 GiB while the host's OWN measured footprint is 18.0 GiB -- a 4.6 GiB deficit if every guest went fully resident, with only 1 GiB of swap free. Swap is what makes +64 defensible, which is why it was sequenced first.
SWAP (operator-run; sudo -n is NOT available on vcloud, so this half could not be automated). /swap2.img 128 GiB, 600 root:root, swapon live, fstab line added. Verified: 135 GiB total swap (8 + 128), 0 used. Effective cushion 13.4 + 135 GiB.
TOFU IS THE ONLY CORRECT PLACE FOR THIS CHANGE. var.vvr1_dc0_memory_mib / vvr1_dc1_memory_mib (main.tf:415/:542). A virsh setmaxmem would have been reverted by the next apply. The derivation now lives IN the variable comment -- 402-in-409, overhead 32 -> 96 GiB -- so the config explains its own number instead of the reason living only here.
PLAN ASSERTED ON CONTENT, not eyeballed, because of the 2026-07-20 precedent (an in-place apply silently regenerated 9 node MACs). Per DC: 1 in-place, 0 create/destroy/replace, 0 MAC changes, and memory the only changed attribute.
A THIRD RESOURCE APPEARED IN THE PLAN AND WAS RESOLVED BEFORE APPLYING, NOT AFTER. module.office1_opnsense showed ~ id = 2 -> 11. The distinction that settles it is tense: "has changed outside of OpenTofu" (drift OBSERVED) versus "will be updated in-place" (action PLANNED). libvirt's domain id is a runtime value that changes on every guest restart. office1-opnsense was NOT touched by either apply.
dc1 FIRST AS THE CANARY (idle; only its edge running) -- the repo's standing pattern. It answered the open question: the in-place update BOUNCES the guest. Domain id 9 -> 12, qemu restarted, guest uptime 0 min. Exactly the recorded "in-place bounces the guest" trap. Guest sees 472.2 GiB (480 minus firmware reserve).
>>> A FINDING THAT WOULD HAVE MADE THE dc0 BOUNCE LOOK CATASTROPHIC, CAUGHT BY CHECKING FIRST: vr1-dc0-maas-01 (the MAAS region) and vr1-dc0-juju-01 (the juju controller) have autostart=disable. <<< Only the edge auto-recovers. Had I applied dc0 without checking, the rack would have returned with no region and no controller. Both were started by hand and verified. The rack's own services (dc0-snap-proxy, dc0-snap-proxy-net, dc0-mirror-net, nginx) are all enabled and did return unaided. LOGGED, NOT FIXED (hard rule 1): a host reboot leaves dc0 with no MAAS region and no juju controller. That is a standing exposure, not a consequence of this change.
RESULT, MEASURED. Both VMs at 480 GiB; each rack now has ~71 GiB for its OS instead of 7. Host: used 809 -> 63 GiB, available 197 -> 944 GiB -- because restarting both guests also released the stranded RSS (vvr1-dc1 354 -> 12 GiB, its 6.6 GiB of swap freed). Recovery verified end to end: MAAS region API 000 -> 502 -> 200 (a genuine boot progression, not a flat failure), snap proxy listening, mirror 200, and juju models reporting the controller model with "Last connection: just now".
What. scripts/preflight.sh (new P8) + tests/preflight/run-tests.sh (a tofu fixture seam + T34-T38). No new harness, so HARNESS-MANIFEST is unchanged.
Why. Answering the operator's question -- does tofu need updates logged, or is it dynamic? -- produced a measurement: preflight.sh and pre-flight-checks.sh contained ZERO tofu plan checks, and a real drift had been sitting unseen until this session's plan surfaced it. Substrate changes are genuinely RARE, which is exactly why nobody looks.
THE DESIGN POINT: -detailed-exitcode returns 2 for BOTH a pending change and a harmless observation. The exit code alone cannot decide, so P8 asserts on CONTENT -- a pending ACTION FAILS, an observed-only drift WARNS. A gate that is permanently red on benign drift gets ignored, which is the failure being prevented.
It REFUSES rather than fails when it cannot look. The outer root lives on vcloud (D-128 Plane 1), so "not the substrate host" is a legitimate state, not a missing gate -- P4/P5's fail-closed shape would be wrong here. Absent tofu, or absent outer-root state, WARN.
Mutation-proven, three mutations, each killing a NAMED test; script restored and sha256-verified byte-identical: neutering the pending-action predicate -> T34 red; making the unrecognised-shape branch pass -> T36 red; making the cannot-look branch silent -> T35 AND T38 red. Note M1/M2 turned tests red on the MESSAGE while the exit code was still right -- content-checking earning its place.
T38 EXERCISES THE ABSENCE THE SAFE WAY, and the comment says why. It removes the fixture's STATE, never the fake tofu from fakebin -- the real tofu is on the system PATH, so a "closed" PATH would fall through to it and touch the LIVE substrate. That is this session's ninth instrument-currency instance applied prospectively rather than after the fact.
PROVEN LIVE, NOT FIXTURE-GREEN: run against the real outer root, tofu exit=0, pending-actions=0, observed-drift=0 -> P8 verdict [ok] zero diff. The substrate is fully converged after both applies, and the office1-opnsense drift is gone because the applies refreshed state.
Harness 33 -> 38 PASS, 0 FAIL. Baseline proven by stashing: the 7 failures my first draft caused were MINE, not pre-existing (PASS=33 FAIL=0 with the change stashed).
Revert. git checkout -- scripts/preflight.sh tests/preflight/run-tests.sh. For item 21: set both variables back to 425984 and re-apply (this bounces both guests again); sudo swapoff /swap2.img && sudo rm /swap2.img and drop the fstab line.