| 2026-07-14 |

D-115 office carve IMPORTED into the Office1 NetBox; DC import blocked on ORG_ULA_48
...
netbox/d115-office-carve.py (+ tests/d115-office-carve/ 20/20 PASS)
Applied to the Office1 sandbox: site vr1-off1, roles office + edge (edge is NEW),
and 8 prefixes. Idempotent (re-run: 0 created / 11 present).
10.10.0.0/16 office container the Office v4 block, /22 per office
10.10.0.0/22 office container VR1 Off1's /22
10.10.0.0/24 office active office1-local LAN (Kea on the edge)
10.10.1.0/24 office active lxdbr0 compose net (MAAS DHCP)
172.30.0.0/16 edge container NEW Edge role; mirrors v6 2602:f3e2:fe::/48
172.30.1.0/24 edge active office1-wan, the simulated ISP uplink
2602:f3e2:f01 :/56 office container mirrors VR0 Off0's e01 :/56
2602:f3e2:f01 :/64 office active office subnet (NOT deployed; v6 has no egress)
Every address was already RUNNING. D-115 blesses what is deployed -- zero
renumber. What it fixes is that three of them were SQUAT: on the wire with no
allocation behind them.
Writing upstream is GATED IN CODE: the tool refuses a non-sandbox target unless
given --yes-write-upstream. Feeding the production apex is an operator decision,
never a side effect of running an import.
BLOCKED, deliberately: dc-dc-prefixes-import.py --dc dc0 CANNOT run. It needs
ORG_ULA_48, which is still UNASSIGNED (gap G3; the scope doc recommends
fd50:840e:74e2::/48 but no ADOPTED decision assigns it -- grep ORG_ULA
design-decisions.md returns nothing). Using it anyway would be an inferred value
(hard rule 2). This is a DECISION gate, not a tooling gap: the tooling is fixed,
tested and ready. Recorded in the ledger with the exact commands to unblock.
Also: docs/vr1-office1-as-built.md gains the IPAM table, and records that the
Tailscale 10.10.0.0/22 route is now ENABLED (edge GUI reachable from the
workstation with no tunnel), superseding the tunnel instructions.
repo-lint 0 fail. run-tests-all: ALL GREEN (55 harnesses).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZBxzAbmqwLW7jEmMG23Xw
|

NetBox sandbox seeded from the upstream draft (129 objects) + the two-phase tooling
...
The Office1 sandbox NetBox was EMPTY, and no seeder existed. The documented loop
(upstream draft -> sandbox -> simulate -> feed back) had no first step.
netbox/prod-draft-dump.py READ-ONLY dump of the upstream draft -> JSON artifact
netbox/sandbox-seed.py applies that JSON to a SANDBOX. Dry by default.
netbox/draft/vr1-draft.json the reviewable artifact (90 prefixes, 23 roles, ...)
TWO PHASES, deliberately, rather than one source->dest script:
- the upstream token never travels (dump runs on vcloud; seed runs on
office1-netbox, where the sandbox token already lives)
- 'the sim NEVER writes upstream' becomes STRUCTURAL: the script that writes
has no upstream credential and no upstream URL, and REFUSES to write to the
instance the draft was read from (self-configuring via the draft's _source,
so it keeps working if upstream ever moves -- a hostname denylist would not)
- the JSON is diffable: you can read what is about to be written
Applied to the sandbox: 129 created, then verified idempotent (0 created / 129
present on re-run). vr1-dc0 -> f02::/48 and vr1-dc1 -> f03::/48, matching the
apex exactly, which is what D-117 exists to guarantee.
TWO BUGS FOUND AND FIXED WHILE BUILDING IT:
1. The dry run mapped only dcim.site scopes, so REGION-scoped prefixes lost
their scope -- 17 of 90 upstream, including the ENTIRE VR1 region-scoped
draft. Caught by a spot-check printing 'site None'. Fixed + reseeded
(--update), now bound to region VR1.
2. The upstream-refusal guard sat AFTER json.load() and after the banner: it
was dead code for any malformed draft and READ as safe. Guards go first.
The harness now proves it actually refuses.
tests/sandbox-seed/: 18/18 PASS. repo-lint 0 fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZBxzAbmqwLW7jEmMG23Xw
|

D-117 ADOPTED (B, DC-only): fix the off-by-one site binding + make the importer dry-by-default
...
Operator ruling: rename the DC numbers to match the NetBox apex; the office
keeps its number (Office1 -> new site vr1-off1, so nothing deployed changes).
netbox/dc-dc-prefixes-import.py:
--dc dc1|dc2 -> --dc dc0|dc1, bound to the APEX slugs measured live:
dc0 -> vr1-dc0 (2602:f3e2:f02::/48) dc1 -> vr1-dc1 (f03::/48)
It previously mapped dc1 -> vr1-dc1 while treating f02::/48 as dc1, i.e. it
would have written VR1 DC0's prefixes to the OTHER DC's site.
Now DRY BY DEFAULT with --commit (it used to write with no flag at all).
A dry run against a FRESH NetBox now plans the site too (_PlannedSite) rather
than bailing with 'no site id' -- previewing an empty apex is the whole point.
DC2_V4_SUPERNET is REJECTED BY NAME (retired) rather than silently ignored.
Every 'DCn' in prose is region-qualified: 'DC0' meant VR0's rehearsal DC in
some sentences and VR1's first DC in others. No sed could have caught that.
tests/dc-dc-prefixes-import/: 73 checks ALL PASS, incl. regression guards that
the dry run creates NOTHING, that --dc dc0 never touches vr1-dc1, and that the
invented vr1-dc2 site is never created.
FOUND, RECORDED, NOT YET FIXED (D-117 amendment): scripts/lib-net.sh carries a
SECOND, colliding $DC namespace where dc0 already means VR0's DC0. Renaming
VR1's dc1->dc0 there would collide with VR0. Proposed fix: region-qualify the
shell selector to the apex slugs (vr0-dc0/vr1-dc0/vr1-dc1). Does not block the
import; the two selectors are NOT interchangeable meanwhile.
repo-lint 0 fail. run-tests-all: ALL GREEN (53 harnesses).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ZBxzAbmqwLW7jEmMG23Xw
|
| 2026-07-11 |

D-111: align VR1 v6 subcarve to deployed NN mnemonic; fix ULA-gen doc bug
...
Basing VR1 NetBox on the real netbox.baldurkeep.com modeling (operator
directive). A read-only live export confirmed VR0-DC0/Willamette use an NN
net-byte mnemonic with /60-per-plane + /64-active; dc-dc-prefixes-import.py
did not match it (arbitrary contiguous indices, /64-only).
DOCFIX-182 / D-111: carve_v6 replaces carve_ula/carve_gua -- provider :10
(+API-VIP :11) GUA, metal /60 shared by admin :20 / internal :21, data :30,
storage :40, repl :50, all /60+/64 ULA; the ULA /56 is indexed to the GUA site
nibble so the 4th hextet reads DC.NN in both families. Direct-math _sub_at (no
subnet enumeration -- preserves the DOCFIX-181 no-hang property). Harness
40 -> 53 PASS. D-111 ADOPTED in design-decisions.md; scope-doc sub-decision #1
ratified.
DOCFIX-183: fix the invalid-IPv6 ULA-generation sed in the netem/ULA proposal
doc (4+6 -> 2+4+4 hextets, caught by actually running it); ratified VR1 ULA is
fd50:840e:74e2::/48.
repo-lint 0 fail; tests/dc-dc-prefixes-import 53/53.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Merge origin/main (netbox uploads 057c31b) into stage1 branch -- brings the NetBox buildout scope doc + roles-aggregates tool alongside the carve_gua fix
|
|
|
| 2026-07-10 |
DOCFIX-181: fix carve_gua subnet-enumeration hang (gauntlet was stalling)
...
netbox/dc-dc-prefixes-import.py::carve_gua() list()-ed every /64 subnet of the
GUA prefix (2**24 = ~16.7M for D-101's /40 example) only to use the first ->
hung/OOM. Real import runs would hang identically, not just the test. Fixed to
take the first /64 lazily via next() (O(1)).
Added a faulthandler watchdog to tests/dc-dc-prefixes-import/test_logic.py so a
future blocking regression self-aborts with a traceback after 30s instead of
silently stalling the whole run-tests-all.sh gauntlet -- the false-green that
let the ledger claim "40/40" for a test that never actually completed.
tests/dc-dc-prefixes-import: 40/40 in 0.05s (was: hung indefinitely).
Full gauntlet: ALL GREEN (52 harnesses). repo-lint 0 fail.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
DOCFIX-160: adversarial review pass finds and fixes 3 real issues
...
Ran a fresh-eyes review subagent across tonight's full deliverable set
(lib-net.sh/lib-hosts.sh selectors, the NetBox DC-DC pipeline, all seven
Stage 1-7 runbooks). Found and fixed:
- A real Python control-flow bug in tests/dc-dc-prefixes-import/
test_logic.py: a `return` inside `try` made the `else` clause
unreachable, silently leaving 3 happy-path assertions uncounted.
37/37 -> 40/40 after the fix (independently reproduced before trusting
the finding).
- A self-contradictory pair of comments in dc-dc-prefixes-import.py about
/19-vs-/22 subnet arithmetic (no functional bug, wrong prose).
- Stale self-referential text in the Stage 4 runbook, written before
Stage 3 existed and never reconciled once it did.
Corrected the 40/40 count in the workflow tracker and session ledger.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|
DOCFIX-152: NetBox multi-DC/dual-stack import pipeline (gap #3 mechanism)
...
Adds netbox/dc-dc-prefixes-import.py, extending the v1 single-site IPv4-only
NetBox import to VR1's two-DC dual-stack model (D-101). DC1's v4 planes are
hardcoded (D-101: inherited from DC0 unchanged, this is decision text, not
an inference); DC2's v4 supernet, the org ULA /48, and the per-DC GUA carve
are all required env vars with no defaults, failing loud rather than
inventing any of them. 37/37 tests against a fake in-memory NetBox client
(no live NetBox reachable this session). Closes the tooling half of gap #3;
the real literals still need operator/NetBox assignment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QrcJx8TUar7pYAvpGJw57A
|
| 2026-07-03 |
|
| 2026-05-22 |
|