# Session changelog 2026-07-31 (part 2) -- snap proxy apply + IPv6 scoping

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.

---

## Item 1 -- FINDING: the `dc-snap-proxy.sh` review's SIMPLIFICATION PLAN was never
## landed on any surface and is LOST

**What.** 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.

---

## Item 2 -- FIX BUG-4: `dpkg -s squid` gates replaced by a BINARY assertion, at
## BOTH sites (`check` 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.**
- **T41** (re-pointed): binary absent -> FAIL. The old case drove `dpkg.mode`.
- **T41b** (NEW): the path is present but NOT executable -- the exact `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.
- **T20b** (NEW): `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).

---

## Item 3 -- FIX BUG-1: the probe ACL was DEAD, and it is now asserted

**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.

---

## Item 4 -- BUG-3 DOCUMENTED IN THE SCRIPT, deliberately NOT "fixed"

**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.

---

## Item 5 -- OPERATOR APPROVAL RECORDED: the agent's rack permissions

**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.

---

## Gate results at time of writing

- `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`).
- Four mutations run across items 2-3; **all four killed**; script restored and
  sha256-verified byte-identical after each.

---

## Item 6 -- THE dc0 SNAP PROXY IS INSTALLED, RUNNING AND GATE-VERIFIED

**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.

---

## Item 7 -- BUG-3 RESOLVED BY MEASUREMENT: the deny shape is CONFIRMED, no
## assertion changed

**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.

---

## Item 8 -- MY BUG-1 FIX WAS WRONG, AND THE MEASUREMENT REFUTES IT (FINDING 9a)

**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`.

---

## Item 9 -- FOUR FURTHER FINDINGS FROM THE INSTALL, logged not fixed

1. **The script header's CDN aside is WRONG (finding 8a).** It records that `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.
2. **`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.
3. **The packaged unit rests `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).
4. **>>> NO JUJU CLIENT EXISTS ON THE JUMPHOST, AND THE dc0 JUJU VM REJECTS THE
   CONFIGURED KEY (9d). <<<** `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.

---

## Item 10 -- D-139 RULING B's DECIDING REASON IS REFUTED. Ruling FLAGGED, not amended.

**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.**
1. jammy's `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.)
2. glibc 2.35's `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.

---

## Item 11 -- D-139 ruling B RECONSIDERED and CONFIRMED; the refuted rationale is STRUCK

**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.

---

## Item 12 -- THE "JUJU CLIENT BLOCKER" IS NOT ONE. Correction to item 9's finding 9d.

**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:
- **G4 -- the `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.
- **G3 -- preflight CANNOT PASS on vcloud by construction.**
  `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-`fail`s 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.
- **G2 -- the D-138 client host's deploy input has NO PROVENANCE.** `~/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.
- **G1 -- `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.

---

## Item 13 -- BUILD: the D-139 apex carve tool, DRY-RUN ONLY (nothing pushed)

**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 `/64`s -- 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.

---

## Item 14 -- D-139 RULING A IS NOT ACHIEVABLE AT THESE CHARM REVISIONS. Ruling B unaffected.

**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.

---

## Item 15 -- RULING: D-139 ruling A amended to a bounded experiment + upstream fix ("B plus C")

**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.

## Item 16 -- I SCOPED THE EXPERIMENT WRONG. Ceph couples `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.

## Item 17 -- REVIEW PASS on `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.

---

## Item 18 -- IPv6 VERIFIED WORKING on the dc0 node planes; G17's role-node half captured

**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.

---

## Item 19 -- BUILD G19: `scripts/dc-node-v6-verify.sh`, the IPv6 plane gate

**FOLDED 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.

### (folded) G19 build -- agent-authored text, verbatim

**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.

---

## Item 20 -- next steps executed: nodes released, D-139 execution list replaced, LP draft written

**(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:
- **SILENT FAILURE (the reason this could not wait):** `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.
- **UNSATISFIABLE:** "remove v4 LAST, after each is proven" -- per-plane conversion is atomic,
  so no such state exists. It read like caution and could not be executed.
- **UNSAFE STEP FUSION:** apex CREATE and RETIRE in one step, with 52 dependent ip-addresses
  (the v6 VIP legs) inside the retiring ULA `/64`s.

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.

---

## Item 21 -- POST-BOOKEND: both DC containment VMs 416 -> 480 GiB; 128 GiB swap; P8 drift gate

**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"*.

## Item 22 -- BUILD: preflight gate P8, substrate drift

**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.
