diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 5da1f01..d79a39d 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -2015,6 +2015,82 @@ D-139 cites as the standard it conforms to -- each also carry a `role=Cloud` `/56`, a `:e0::/60` VPN and `:f0::/60`+`/64` OOB. VR1 DC0/DC1 carry NONE of these**, holding only the `/48` container. D-139's table does not rule them, so the tool does not create them. + **>>> RULED 2026-08-01 (GA-R5) -- D-139 RULING A IS AMENDED: THE v6-ONLY SCOPE NARROWS TO A + BOUNDED EXPERIMENT, PLUS AN UPSTREAM FIX. Operator answer, exact utterance: "B plus C". <<<** + Options as presented were (a) carve GUA dual-stack now and defer v6-only entirely; + (b) attempt v6-only on `replication` alone as a bounded experiment; (c) pursue the upstream + fix, since LP #2061836's `ceph-osd` change is ONE WORD (`dynamic_only=False`) and is already + merged on `ceph-mon`. **(a) was NOT selected.** **INTERPRETATION STATED SO IT CAN BE + CORRECTED RATHER THAN ASSUMED:** ruling B's GUA carve is UNAFFECTED and proceeds (it is a + separate, already-confirmed ruling); ruling A's FIVE-plane v6-only scope is narrowed to the + experiment; `metal-internal`, `data-tenant` and `lb-mgmt` therefore stay DUAL-STACK pending + upstream fixes. If that is not what was meant, this paragraph is the thing to correct. + **>>> THE EXPERIMENT AS I SCOPED IT IS NOT POSSIBLE, AND I GOT THIS WRONG WHEN I + RECOMMENDED IT. <<<** I proposed `replication` alone as "the least-bad plane, smallest blast + radius". **MEASURED against the shipped `ceph-osd` rev 953 artifact (`ceph_hooks.py:544-553`): + `prefer-ipv6` is a SINGLE switch that sets `ms_bind_ipv4 = False` and `ms_bind_ipv6 = True` + GLOBALLY**, so it governs Ceph's PUBLIC network (`storage`) and CLUSTER network + (`replication`) together. **There is no state in which Ceph runs cluster traffic on v6 and + public traffic on v4.** The experiment is therefore `storage` + `replication` TOGETHER, as a + PAIR, or not at all -- and its blast radius is correspondingly larger 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`) is + reached ONLY under `if not public_network:` / `if not cluster_network:`. **Setting the + charm's `ceph-public-network` and `ceph-cluster-network` options to the v6 CIDRs means that + call is never used for address selection**, so the dynamic-only defect is AVOIDABLE BY + CONFIG rather than blocking. That is the same lever LP #2109798's reporter used in the + opposite direction ("forcing the ipv4 cidr for the ceph-cluster-network and + ceph-public-network options"). **CONFIDENCE: 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 RUNS:** the foundational measurement -- does + `network-get` return a v6 address on a v6-only bound space? -- remains NOT TAKEN, and + D-139's "remove v4 LAST, after each is proven" clause is still UNSATISFIABLE as written. + **D-139 APEX CARVE TOOL REVIEWED AND STREAMLINED (independent second agent, per operator + standing instruction that every new script gets a review pass).** Capture + `docs/audit/d139-carve-review-20260801.txt` (364 lines). Verdict: the tool is well built and + was already at charter prose weight; **4 real defects FIXED, 4 reported-not-fixed, and the + builder's one declared charter miss UPHELD as correct.** **D1 is the consequential one and + is the mandate's own named class, "a refusal that does not refuse": an unreachable apex + produced a raw traceback and EXIT 1** -- which this 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`, `RC=1`), fixed with a 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, + which is precisely why the charter says CITE, never re-argue. D3/D4 replaced two source-text + greps (existence, not content) and two vacuous-negative assertions with behavioural ones. + **`main()` decomposition DECLINED on a good argument, recorded because it is reusable:** the + `if not a.commit: return 0` guard currently sits ADJACENT to the write block, and extracting + a `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. + **REPORTED NOT FIXED, and R1 is a real security finding, MEASURED rather than reasoned: + urllib carries the `Authorization` header across a CROSS-HOST redirect** (proven with a + loopback probe -- the target received the token verbatim). The `SANDBOX_HOSTS` gate covers + the SUPPLIED url, not a redirect FROM it. LOW severity (it requires the approved apex itself + to redirect hostilely) and a redirect handler is untested code on a control path, so it is + logged rather than executed. R2 unwrapped read-back, R3 `errs` double-count (tally only), + R4 `subnet_of` equality (measured not to occur live). + **VERIFICATION AFTER THE REVIEW:** 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 turns + T18 red; M7 the 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) and + sha256-verified identical after every one. **Gauntlet ALL GREEN (95); dry-run body DIFFED + BYTE-FOR-BYTE IDENTICAL to the committed capture** (asserted on content, not on the counts), + apex prefix count 139 before and after, `--commit` never passed. + **ONE QUALIFICATION THE REVIEWER DECLARED RATHER THAN LEFT IMPLICIT, and it is the honest + limit of the above: 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 now reachable with nothing transmitted. + Deliberate, out of scope to undo, and **UNTESTED**. + **PROCESS FAILURE REPEATED, OWNED, AND NOW FIXED AT THE SOURCE:** the review mandate + REQUIRED a `docs/audit/` capture and FORBADE touching `docs/CURRENT-STATE.md` -- jointly + unsatisfiable against repo-lint L10, so the agent correctly left the tree RED and said so + rather than working around it. Same class as the `/tmp` loss earlier today: **an agent + mandate must name a repo path for findings AND leave the committer able to satisfy L10.** + Both are resolved by this paragraph landing in the same commit as the captures. - Project: Omega Cloud, VR1 DC-DC rehearsal -- a two-DC + Office1-headend virtual rehearsal on KVM (vcloud host), rehearsing the future bare-metal Roosevelt deployment (D-100, `docs/design-decisions.md:1946`). diff --git a/docs/audit/d139-carve-review-20260801.txt b/docs/audit/d139-carve-review-20260801.txt new file mode 100644 index 0000000..8c5d83b --- /dev/null +++ b/docs/audit/d139-carve-review-20260801.txt @@ -0,0 +1,364 @@ +d139-carve-review-20260801.txt +============================== +INDEPENDENT review-and-streamline pass over netbox/d139-gua-carve.py and +tests/d139-gua-carve/, which were BUILT BY ANOTHER AGENT this session and +committed at 64cd539. Reviewer did not write the code. Host: vcloud. +Date: 2026-08-01. Governing decision: D-139 ruling B (cited, not re-argued). + +READ-ONLY with respect to the cloud and the apex: NO write of any kind was made +to NetBox. The only live traffic was the mandated re-run of the DRY RUN plus one +prefix-count read. Apex prefix count 139 before and 139 after -- the same number +the builder recorded, so nothing this pass did landed a row. + +D-139 ruling A was found NOT ACHIEVABLE at current charm revisions (item 14 of the +changelog). Ruling B is orthogonal and STANDS, so this tool remains valid. Scope +was NOT widened into ruling A. + +======================================================================= +VERDICT +======================================================================= +NOT "no change needed" -- but close to it. The tool is genuinely well built: the +refusals are real refusals, the dry-run default is structurally honest, the +read-back is load-bearing, and the prose is already at charter weight (5 comment +lines in 232). FOUR defects were found and FIXED; four more are REPORTED and +deliberately NOT fixed; the one criterion the builder declared as missed +(main() ~90 lines) was assessed and the miss is UPHELD as correct. + +The single most valuable finding is D1: the most likely real-world failure of +this tool -- an unreachable apex -- exited with the WRONG code and a raw +traceback. That is the mandate's named class "a refusal that does not refuse". + +======================================================================= +LINE COUNTS -- before / after +======================================================================= + netbox/d139-gua-carve.py 232 -> 233 (+1) + tests/d139-gua-carve/run-tests.sh 151 -> 166 (+15) + tests/d139-gua-carve/stub-apex.py 100 -> 104 (+4) + +The tool did not get shorter. That is the honest outcome: there was no prose to +cut. Its 232 lines are refusals, conflict detection, read-back and the write +path, exactly as the builder claimed. The +1 net is a 2-line correctness fix +minus a 1-line stale comment removal. + +======================================================================= +DEFECTS FOUND AND FIXED (4) +======================================================================= + +D1 -- REAL DEFECT (correctness + the three-outcome contract). An UNREACHABLE + apex produced a raw Python traceback and exited 1. + MEASURED BEFORE THE FIX, verbatim tail: + urllib.error.URLError: + RC=1 + _req() caught only urllib.error.HTTPError. A URLError / socket timeout / + DNS failure is not a RuntimeError, so it walked straight past main()'s + `except RuntimeError -> die(...)` -- the handler that exists precisely to + turn an unreadable apex into REFUSE(2) with the User-Agent hint. + WHY IT MATTERS beyond tidiness: this tool's own header defines rc 1 as + "write/read-back error". A DRY RUN that never reached the apex has written + NOTHING, so rc 1 told the operator the opposite of the truth, in the single + most likely failure mode (VPN down, NetBox restarting, wrong port). + An unrecognised state defaulted to a WRITE verdict instead of REFUSING. + FIX (2 lines, after the HTTPError clause -- order matters, HTTPError + subclasses URLError subclasses OSError): + except OSError as e: # unreachable/timeout is UNEVALUABLE (rc 2), not a write error (rc 1) + raise RuntimeError("%s %s -> %s" % (method, path, e)) + Now REFUSE(2), no traceback, and it names the failed read. New test T18. + +D2 -- REAL DEFECT (record accuracy + charter). The comment above the D-139 + table asserted a GOVERNANCE STATE THAT WAS ALREADY FALSE when it was + committed. It read: + # --- D-139 ruling B (FLAGGED for operator reconsideration at 633da1c -- its stated + # deciding reason was refuted by measurement; the table is D-139 as it stands today) -- + TIMELINE, measured from git (`git log --date=iso`): + 633da1c 00:11:12 ruling B's deciding reason REFUTED; ruling FLAGGED + bbc0330 00:20:47 ruling B RECONSIDERED and CONFIRMED, unchanged in substance + 64cd539 00:55:25 the tool committed, still carrying the FLAGGED comment + The reconsideration had closed 35 minutes before the tool landed. A reader + grepping the tool would conclude ruling B is pending an operator decision. + It is not: D-139's RULING NOTE 2026-08-01 confirms it, resting now on + conformance-with-Willamette/VR0 and minimize-delta-to-Roosevelt. + It is also the charter violation "a script CITES decisions; it never + re-argues them" -- the comment annotated the decision's standing rather + than citing it, which is exactly why it could rot. + FIX: replaced with a bare citation, `# --- D-139 ruling B, table verbatim ---`. + The reconsideration history was NOT written in its place; that would be + re-arguing in the other direction. It lives in design-decisions.md. + +D3 -- REAL DEFECT (a test that asserts EXISTENCE, and a guard never exercised). + Old T16 was two greps over the TOOL'S OWN SOURCE TEXT: + grep -q "instrument currency before negatives" "$S" + grep -q 'if not scoped:' "$S" + That asserts a string is present in a file. It does not prove the guard + FIRES, that it exits 2, or that the operator ever SEES the message. No stub + mode produced zero scoped prefixes, so the zero-scoped-read REFUSE -- a + guard written against this repo's own recorded negative-misread class -- + had no behavioural coverage at all. It also breaks the charter rule + "assert on CONTENT, never existence" in the harness that enforces it. + FIX: new STUB_MODE=empty-scope models the REALISTIC cause (a filtered or + failed read: rows come back, none scoped to the asked DC; the site still + exists, so only the scoped count distinguishes it from a virgin DC). T16 is + now three behavioural assertions: rc=2, the message reaches stdout, and + -- the one that matters -- NO plan is printed, proving the tool refuses + BEFORE emitting 14 confident CREATEs built on a read it does not trust. + Both source-text greps DELETED. + +D4 -- REAL DEFECT (vacuous negative assertions -- this repo's eight-time + misread class, from MEMORY "instrument currency before negatives"). + T09 and T11 assert ABSENCE against a sed-extracted section: + c="$(printf '%s' "$OUT" | sed -n '/^ CREATE:/,/^$/p')" # T09 + r="$(printf '%s' "$OUT" | sed -n '/^ RETIRE/,$p')" # T10/T11 + Nothing proved either extraction was non-empty. If a print-format change + empties $c, all three T09 "would re-create existing $cidr" checks PASS -- + vacuously. Same for T11's three scope checks over $r, which are the tests + standing between the report and sweeping the org ULA aggregate or a v4 + plane into a retirement list. + FIX: one non-vacuity assertion per extraction. PROVEN LOAD-BEARING by + mutation M8 below, which renames the CREATE section title and turns the + new T09 guard red while every other T09 check stays green -- i.e. without + the guard that mutation was INVISIBLE. + +======================================================================= +REPORTED, NOT FIXED -- findings, logged not executed (CLAUDE.md rule 1) +======================================================================= + +R1 -- MEASURED GAP in a control the mandate calls non-negotiable: the + SANDBOX_HOSTS refusal gates the URL THE OPERATOR SUPPLIES, but urllib + follows 3xx redirects by default and CARRIES THE Authorization HEADER + ACROSS HOSTS. Not inferred -- measured with a two-server loopback probe: + redirect target received Host : localhost:33755 + redirect target received Authz : YES -> Token SECRET-TOKEN-VALUE + CPython's HTTPRedirectHandler.redirect_request strips only content-length + and content-type (`CONTENT_HEADERS = ("content-length", "content-type")`), + so the token survives a redirect to an arbitrary host. + SEVERITY: LOW, and it is a gap not a hole -- exploiting it requires the + APPROVED apex itself (10.10.1.10, on the trusted LAN) to issue a hostile + redirect. It does not weaken the DOCFIX-195 two-instance protection, whose + threat is operator confusion, not a malicious apex. + NOT FIXED ON PURPOSE: a redirect handler is new, untested code on a control + path, and adding it is scope this review was not given. If the operator + wants it closed, the minimal form is an opener with a redirect handler that + re-checks hostname against SANDBOX_HOSTS -- it needs its own test. + +R2 -- The read-back `nb.get_all("/ipam/prefixes/")` in the --commit path is NOT + wrapped in try/except. After the D1 fix it raises RuntimeError instead of + URLError, so an apex that dies mid-commit still tracebacks and exits 1. + The exit code is now CORRECT (writes did happen; rc 1 is "write/read-back + error" and this is literally a read-back error) but the output is a + traceback where a sentence is owed. NOT FIXED: it is on the write path, + which cannot be exercised live under this mandate, and contriving a + mid-run stub kill to test it buys less than it risks. + + DECLARED SIDE EFFECT OF D1 ON THE WRITE PATH -- stated because "a + streamline that changes behaviour is a bug" is the standard this pass was + held to, and this IS a behaviour change, deliberate and out of scope to + undo. _req() is shared by the --commit POST loop, whose handler is + `except RuntimeError`. BEFORE the fix a connection error during a POST + raised URLError, escaped the loop, and ABORTED the run on the first + failure with a traceback. AFTER the fix it is a RuntimeError, so it is + caught, counted into errs, AND THE LOOP CONTINUES TO THE NEXT POST. + Consequences, both real: (i) against a black-holed apex all 11 POSTs are + attempted at timeout=45 each, roughly 8 minutes, plus another 45s in the + unwrapped read-back; (ii) `RESULT: errors=N` with rc 1 is now reachable in + a state where NO write was ever transmitted, which widens what that tally + means. NOT FIXED: making the loop abort on a transport failure is new, + untested code on the write path -- the same reasoning as R2 above. + THIS IS UNTESTED. The evidence in VERIFICATION below does not cover it. + +R3 -- `errs += len(gone)` double-counts a POST that both failed AND is absent + on read-back. Affects only the `RESULT: errors=N` tally; the exit code + (1 if errs else 0) is unaffected either way. Write-path edit for a + cosmetic gain -- deliberately not taken. + +R4 -- `net.subnet_of(ula)` is TRUE for the /48 itself, so an org ULA aggregate + that were ever DC-scoped would be swept into the RETIRE report. MEASURED + NOT TO OCCUR: this run's RETIRE-REPORT is 18 rows, all /60 and /64, no /48 + -- the aggregate is unscoped live, exactly as the stub fixture models it. + Observation, not a defect: the tool never deletes, so the worst case is + over-reporting, and the harness pins the correct behaviour (T11). + +R5 -- The committed capture docs/audit/d139-carve-dryrun-20260801.txt carries a + STANDING NOTE saying the --commit push "is gated on that reconsideration". + That was TRUE when written (00:18) and became STALE at 00:20 when bbc0330 + confirmed the ruling. NOT EDITED -- it is a verbatim GA-R1 record and was + accurate at capture time. Recorded here instead. The live gate today is + ordinary operator approval, not a pending reconsideration. + +======================================================================= +main() DECOMPOSITION -- assessed, and the builder's declared MISS is UPHELD +======================================================================= +The builder declared: "One soft criterion missed and declared: main() is ~90 +lines, so one screen per function does not hold there." main() is lines +140-229, i.e. 90 lines. The criterion IS missed. It should stay missed. + +The candidate extractions were connect() (env + host REFUSE -> NB) and +commit_writes() (POST loop + read-back), which together would bring main() to +roughly 55 lines. + +DECLINED, and the discriminator is not line count -- it is whether a reader can +verify a SAFETY property in fewer places or more: + - Today `if not a.commit: return 0` and the entire write block are ADJACENT. + One glance confirms "nothing writes unless --commit". Extracting + commit_writes() splits that proof across two locations, for a script whose + whole claim is that the write path is opted into. + - connect() separates the SANDBOX_HOSTS refusal from its point of use. The + refusal's value is that it visibly precedes the construction of NB(url, + token) -- so the token is never even handed to a client for a bad host. + Moving it into a helper makes that ordering a thing you must go and check. + - main() is a LINEAR pipeline with no branching complexity: parse, refuse, + read, resolve, plan, print, optionally write. Helpers each called exactly + once, in order, are scattering, not decomposition. +For a 232-line safety-critical script that reads like a runbook, linear beats +decomposed. The miss is the right trade and should be recorded as accepted +rather than silently carried. + +======================================================================= +CHARTER CRITERIA -- before / after +======================================================================= + Cites decisions, never re-argues PARTIAL -> MET (D2: the FLAGGED + annotation removed) + Header 25 lines max MET (13) -> MET (13) + No comment restates the code MET -> MET + No comment block over 5 lines MET (max 2) -> MET (max 2) + No examples / alternatives / vendors MET -> MET + ~one screen per function MISSED (main 90) -> MISSED, UPHELD as + the correct trade (see above) + Assert on CONTENT, never existence VIOLATED in the HARNESS (D3: T16 grepped + the tool's source) -> MET + PASS / FAIL / REFUSE, no default-ok VIOLATED (D1: unreachable apex exited 1 + with a traceback) -> MET + Dry-run is the DEFAULT MET -> MET (unchanged, re-proven T06) + Host hard-refusal {localhost, + 127.0.0.1, 10.10.1.10} MET -> MET (untouched; see R1) + Credentials sourced, never printed MET -> MET + User-Agent curl/8.5.0 pinned MET -> MET (untouched) + +======================================================================= +THINGS THE BUILDER GOT RIGHT THAT ARE WORTH RECORDING +======================================================================= +- get_all() RE-ANCHORS pagination to the approved host: + nxt = n.split("/api", 1)[1] if n else None + The apex's `next` URL is reduced to a PATH and re-joined to self.url, so a + crafted next cannot walk the tool off the approved host. That is a real + safety property and the builder did not claim it. (Note it does not help R1, + which is a transport-layer redirect, not an application-layer next link.) +- The sub_at() /60-alignment die() is NOT a dead assertion. Mutation M1 (storage + hextet 0x40 -> 0x41) makes it FIRE and REFUSE. An assertion that can be shown + to fail under a plausible edit is doing work. +- build_plan() runs BEFORE build_retire(), and both run BEFORE any printing, so + the zero-scoped REFUSE lands before a single plan line is emitted. Proven by + the new T16, not assumed. +- CONFLICT is treated as an UNRECOGNISED STATE that REFUSES (rc 2), not as a + no-op "already exists". That is the hard call and it was made correctly. + +======================================================================= +VERIFICATION -- real numbers +======================================================================= +HARNESS 65/65 PASS (baseline, before any edit) + 71/71 PASS (after) + Delta +6 = -2 (both T16 source greps DELETED) + +3 (T16 rebuilt behaviourally, STUB_MODE=empty-scope) + +3 (T18 unreachable apex -> REFUSE) + +2 (T09/T10 non-vacuity guards) + +MUTATION PASS. The tool was edited, so the builder's 5 results are no longer +evidence for the changed file. ALL FIVE WERE RE-RUN against the edited tool, and +three new mutations cover the new paths. Restore was by copy from a pristine +snapshot (NOT git checkout -- this clone is shared with concurrent sessions); +sha256 verified IDENTICAL after every single one. + pristine/final sha256 38484b52a5e660b3a70dc0376fc06041c97ed0e2153d4519f511af660d9bd3d3 + + M1 storage hextet 0x40 -> 0x41 RED T07, T08(x11), T05 48/71 FAIL + (the /60-alignment guard REFUSEs, which is why T05's rc=0 also goes red) + M2 UA pin line DELETED RED T15 + cascade 53/71 FAIL + M3 SANDBOX_HOSTS refusal neutered RED T04 ONLY 1/71 FAIL + M4 retire predicate ULA/48 -> GUA/36 RED T10(x10), T07, T12 12/71 FAIL + M5 read-back gone = [] RED T17(x3) 3/71 FAIL + M6 NEW: OSError catch removed RED T18(x3) 3/71 FAIL + (i.e. M6 restores the pre-review behaviour and the new test catches it) + M7 NEW: zero-scoped-read guard neutered RED T16(x2) 2/71 FAIL + M8 NEW: CREATE section title renamed RED T09 non-vacuity 12/71 FAIL + guard + T12; PROVES D4 -- without the guard this mutation silently + passed T09's three absence checks. + All 5 original mutations STILL STAND on the edited tool. None was carried + over on the builder's word. + +REPO-LINT before: 0 fail, 1 warn, 645 files (the warn is L1 + docs/design-decisions.md 239 non-ASCII bytes -- the pre-existing + legacy D-001..018 carve-out, NOT caused by this pass) + after, code + harness edits ONLY: 0 fail, 1 warn, 645 files + -- the EXPECTED result, unchanged. + + after, WITH THIS CAPTURE FILE PRESENT: 1 fail, 1 warn. + [FAIL] L10 pending change set modifies state-bearing surface(s) + [docs/audit/: docs/audit/d139-carve-review-20260801.txt] without + touching docs/CURRENT-STATE.md -- GA-R1 rule 8 (C1) + THIS IS NOT A REGRESSION AND NOT A DEFECT IN THE TOOL. It is the + mandate's own two constraints colliding: this review was REQUIRED to + record its findings in docs/audit/ and FORBIDDEN to touch + docs/CURRENT-STATE.md. L10 is working exactly as designed. + CAUSATION MEASURED, not inferred: the capture was moved aside, lint + re-run (0 fail, 1 warn, 645 files), and the capture moved back with + sha256 verified identical. The code and harness edits alone are + lint-clean; the FAIL is caused solely by this file's existence in an + uncommitted change set. + RESOLUTION IS THE OPERATOR'S: add the CURRENT-STATE.md paragraph in + the SAME commit that lands this capture, which is what rule 8 asks + for and what a reviewer forbidden to touch that file cannot do. + +GAUNTLET ALL GREEN (95 harnesses). No harness added or removed, so the + R15(2) HARNESS-MANIFEST is undisturbed. + +LIVE DRY RUN, both DCs, --commit NEVER passed: + vr1-dc0 apex prefixes scoped: 21 CREATE 11 | EXISTS 3 | RETIRE-REPORT 9 + dependent: 26 ip-address(es), 0 ip-range(s) rc=0 + vr1-dc1 apex prefixes scoped: 21 CREATE 11 | EXISTS 3 | RETIRE-REPORT 9 + dependent: 26 ip-address(es), 0 ip-range(s) rc=0 + TOTALS CREATE 22 | EXISTS 6 | RETIRE-REPORT 18, 52 dependent ip-addresses + UNCHANGED from the builder's capture -- and asserted on CONTENT, not on the + counts alone: the plan body was DIFFED against the committed capture + (lines 25-97) and is byte-for-byte IDENTICAL. + + SCOPE OF THAT CLAIM, stated precisely rather than left to be assumed: the + byte-identical dry run proves the READ AND PLAN PATH is unchanged. It + proves NOTHING about the write path, because a dry run executes zero POSTs + by construction. The write path's FAILURE semantics DID change as a + declared side effect of D1 (see R2), and that change is UNTESTED. The + happy write path is unchanged. So: "behaviour preserved" is proven for + everything --commit does not touch, and asserted-with-a-declared-exception + for what it does. + NO WRITE: apex prefix count 139, the same value recorded before and after + the builder's run. + +======================================================================= +TRANSCRIPT-ONLY NOTES -- recorded so they do not die with the session +======================================================================= +- New T16's `has "CREATE" && bad || ok` is itself a NEGATIVE assertion, and it + is non-vacuous ONLY because the preceding `[ "$RC" -eq 2 ] && has "instrument + currency..."` already forces $OUT non-empty. That is the same anchoring + argument that makes T12's negative safe. A later editor who REORDERS those + assertions would quietly re-introduce D4 inside the very test written to fix + D4. Keep the rc/message check FIRST. +- The R1 redirect probe was a throwaway two-server loopback script in /tmp with + a fake token; no repo file and no real credential was involved, and no traffic + left the host. +- The no-write verification used `curl -H "Authorization: Token $NETBOX_TOKEN"`, + which puts the token in the process table transiently. Nothing was printed and + nothing reached the transcript, but that is the SAME exposure class this + tool's own header warns about for argv. A `--netrc` or header-from-stdin form + would be strictly better next time. +- The `python3 -m scripts` / caveman compressor was NOT invoked against anything + (CLAUDE.md tooling guardrail); every edit here is a targeted hand edit. + +======================================================================= +NOT DONE, ON PURPOSE +======================================================================= +- No write to NetBox, at all. --commit was never passed. +- No git add / commit / push / checkout (concurrent sessions share this clone). + The working tree is left MODIFIED and uncommitted for the operator. +- docs/CURRENT-STATE.md, docs/design-decisions.md, scripts/dc-snap-proxy.sh and + tests/dc-snap-proxy/ were not touched. +- The committed dry-run capture was not rewritten (see R5). +- No changelog entry was written: the delivery discipline requires one for a + script change, but the changelog is a shared surface mid-session and this pass + was scoped to review. OWED: a changelog item covering D1-D4 with the revert + `git checkout netbox/d139-gua-carve.py tests/d139-gua-carve/`. diff --git a/docs/changelog-20260731-snap-proxy-apply-ipv6.md b/docs/changelog-20260731-snap-proxy-apply-ipv6.md index b23bb27..143e6f0 100644 --- a/docs/changelog-20260731-snap-proxy-apply-ipv6.md +++ b/docs/changelog-20260731-snap-proxy-apply-ipv6.md @@ -663,3 +663,110 @@ **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. diff --git a/netbox/d139-gua-carve.py b/netbox/d139-gua-carve.py index ff82b43..700af86 100644 --- a/netbox/d139-gua-carve.py +++ b/netbox/d139-gua-carve.py @@ -24,8 +24,7 @@ UA = "curl/8.5.0" # the apex 403s other UAs and it reads as an auth failure -- platform-traps.md SANDBOX_HOSTS = {"localhost", "127.0.0.1", "10.10.1.10"} -# --- D-139 ruling B (FLAGGED for operator reconsideration at 633da1c -- its stated -# deciding reason was refuted by measurement; the table is D-139 as it stands today) -- +# --- D-139 ruling B, table verbatim --- DC_GUA = {"vr1-dc0": "2602:f3e2:f02::/48", "vr1-dc1": "2602:f3e2:f03::/48"} # (4th hextet, apex role slug, D-139 plane label, owns a /60 parent) CARVE = [ @@ -62,6 +61,8 @@ return json.loads(resp.read() or "null") except urllib.error.HTTPError as e: raise RuntimeError("%s %s -> %d %r" % (method, path, e.code, e.read()[:300])) + except OSError as e: # unreachable/timeout is UNEVALUABLE (rc 2), not a write error (rc 1) + raise RuntimeError("%s %s -> %s" % (method, path, e)) def get_all(self, path): out, nxt = [], path + "?limit=500" diff --git a/tests/d139-gua-carve/run-tests.sh b/tests/d139-gua-carve/run-tests.sh index 23c6981..7d070eb 100755 --- a/tests/d139-gua-carve/run-tests.sh +++ b/tests/d139-gua-carve/run-tests.sh @@ -73,6 +73,7 @@ # T09 IDEMPOTENCE. provider-public :10/:11 are already built; they must be reported # EXISTS and must not appear in the CREATE block. c="$(printf '%s' "$OUT" | sed -n '/^ CREATE:/,/^$/p')" +[ -n "$c" ] && ok || bad "T09 CREATE-section extraction EMPTY -- every absence check below would pass vacuously" for cidr in "2602:f3e2:f02:10::/60" "2602:f3e2:f02:10::/64" "2602:f3e2:f02:11::/64"; do printf '%s' "$c" | grep -qF "$cidr" && bad "T09 would re-create existing $cidr" || ok printf '%s' "$OUT" | sed -n '/^ EXISTS/,/^$/p' | grep -qF "$cidr" && ok || bad "T09 $cidr not reported EXISTS" @@ -82,6 +83,7 @@ # and counts what would be orphaned (13 statics in each of :220 and :221). has "RETIRE -- REPORTED ONLY, this tool never deletes" && ok || bad "T10 retire section is not marked report-only" r="$(printf '%s' "$OUT" | sed -n '/^ RETIRE/,$p')" +[ -n "$r" ] && ok || bad "T10/T11 RETIRE-section extraction EMPTY -- the T11 scope checks would pass vacuously" for cidr in "fd50:840e:74e2:220::/60" "fd50:840e:74e2:220::/64" "fd50:840e:74e2:221::/64" \ "fd50:840e:74e2:230::/60" "fd50:840e:74e2:230::/64" "fd50:840e:74e2:240::/60" \ "fd50:840e:74e2:240::/64" "fd50:840e:74e2:250::/60" "fd50:840e:74e2:250::/64"; do @@ -129,10 +131,15 @@ OUT="$(NETBOX_URL="http://127.0.0.1:$PORT" NETBOX_TOKEN=stub python3 "$TMP/mutant.py" --dc vr1-dc0 2>&1)"; RC=$? [ "$RC" -eq 2 ] && has "User-Agent trap" && ok || bad "T15 a wrong-UA build did not REFUSE with the UA hint" -# T16 an empty scoped read is NOT a virgin DC. 22 confident CREATEs off a failed -# query is the exact shape of this repo's recorded negative-misread class. -grep -q "instrument currency before negatives" "$S" && ok || bad "T16 lost the zero-scoped-read refusal" -grep -q 'if not scoped:' "$S" && ok || bad "T16 zero-scoped-read guard is gone" +# T16 an empty scoped read is NOT a virgin DC. Confident CREATEs off a failed query +# is the exact shape of this repo's recorded negative-misread class. Asserted on +# BEHAVIOUR (the stub returns rows, none scoped to dc0), not on the tool's source text. +start_stub empty-scope +run --dc vr1-dc0 +[ "$RC" -eq 2 ] && has "instrument currency before negatives" && ok \ + || bad "T16 zero scoped rows must REFUSE(2) and say why (rc=$RC)" +has "CREATE" && bad "T16 printed a plan built on a read it had already refused to trust" || ok +[ ! -s "$TMP/writes.log" ] && ok || bad "T16 wrote to the apex despite refusing" # T17 the READ-BACK is not decoration. The stub answers every POST 201 but never # adds the row -- the opnsense-plugins.sh class, where apply reported success and @@ -145,6 +152,14 @@ [ "$(wc -l < "$TMP/writes.log")" = "11" ] && ok || bad "T17 posted $(wc -l < "$TMP/writes.log") rows, want 11" [ "$(grep -c '^POST /api/ipam/prefixes/$' "$TMP/writes.log")" = "11" ] && ok || bad "T17 wrote to an endpoint other than ipam/prefixes" +# T18 an UNREACHABLE apex is UNEVALUABLE, not a write error. Port 1 refuses instantly. +# rc MUST be 2 (REFUSE); rc 1 is this tool's "write/read-back error" and a dry run that +# never reached the apex has written nothing. Measured pre-fix: raw traceback, rc=1. +OUT="$(NETBOX_URL=http://127.0.0.1:1 NETBOX_TOKEN=stub python3 "$S" --dc vr1-dc0 2>&1)"; RC=$? +[ "$RC" -eq 2 ] && ok || bad "T18 unreachable apex rc=$RC, want 2 (REFUSE)" +has "apex read failed" && ok || bad "T18 did not name the read that failed" +has "Traceback" && bad "T18 raw traceback where a REFUSE was owed" || ok + echo total=$((pass+fail)) if [ "$fail" -eq 0 ]; then echo "d139-gua-carve: $pass/$total PASS"; exit 0; fi diff --git a/tests/d139-gua-carve/stub-apex.py b/tests/d139-gua-carve/stub-apex.py index b6df9ac..c8d4715 100644 --- a/tests/d139-gua-carve/stub-apex.py +++ b/tests/d139-gua-carve/stub-apex.py @@ -3,7 +3,7 @@ (two other agents share this jumphost -- a fixed port is a flaky-test generator). Fixture rows mirror the state MEASURED live 2026-08-01 (docs/audit/d139-carve-dryrun- -20260801.txt). STUB_MODE: normal | no-role | conflict. Every non-GET is appended to +20260801.txt). STUB_MODE: normal | no-role | conflict | empty-scope. Every non-GET is appended to $STUB_WRITELOG -- the harness asserts that file stays EMPTY through a dry run. """ import json @@ -52,6 +52,10 @@ PREFIXES.append(row("172.31.0.%d/30" % (int(n) * 4), dc, "transit", "office1<->dc%s transit (D-124)" % n)) if MODE == "conflict": PREFIXES.append(row("2602:f3e2:f02:20::/64", "vr1-dc1", "storage", "mis-bound")) +if MODE == "empty-scope": + # A FILTERED/failed read: rows come back, none for the asked DC. The site still + # exists, so only the scoped-prefix count distinguishes this from a virgin DC. + PREFIXES = [p for p in PREFIXES if (p.get("scope") or {}).get("slug") != "vr1-dc0"] ADDRESSES = [{"address": "%s::%d/64" % (p, o), "description": "VIP"} for p in ("fd50:840e:74e2:220", "fd50:840e:74e2:221",