release: v4.0.0 - the home node's arbitrations, its inventory, and naming.v4 #12

Merged
administrator merged 21 commits from review/home-node-arbitration into main 2026-10-29 20:17:26 +00:00
Collaborator

100% integrated: rows 1-12 of section 6 landed by 0cf96f4, row 13 with the cut (b69e4a8), make check green.

This merges review/home-node-arbitration into main as v4.0.0: the home node's eleven arbitrations on v3.0.0, its contradictory review, the maintainer's decisions on both (decisions/2026-10-29-home-node-arbitration-review.md), then the home node's exhaustive inventory - 198 objects confronted with the tool - and the fifteen arbitrations on it (decisions/2026-10-29-home-node-inventory-arbitrations.md, I1-I15). There is no v3.x: the patches merge untagged into this release.

Rows to commits (inventory, section 6)

# Change Commit
1 Products runner (hrn) and backup (hbk) 63106dd
2 A runner's env follows D3 dd14e13
3 Under single, the separator is what substituted_controls declares (D4) e2d5f58
4 Reserved IPSet names: management, blacklist, ipfilter-net<n> 316e2d7
5 Scaleway default groups and policies are structural exceptions (I8) 9131d2b
6 A Proxmox user is also a machine identity 064c354
7 Tailscale header: the tag carries the org ab877fd
8 Review: project per-shr-ops-foundation-proj, runner at 1500904 9178d4e
9 Adoption guide: I1-I4, I6-I15, no "to decide" left c29c1e7
10 Consumer template, rules 9 and 10 (I13, I14) d96ef96
11 CHANGELOG: each entry landed with its row, then the introduction 7bfe16a
12 decisions/README.md index ba99ce2
- Template example (section 3): the proposed default 0cf96f4
13 registry/conventions.yaml to version 4; kit.tag and every pin to v4.0.0 b69e4a8

What the table does not show.

  • Row 4: ipfilter-net<n> was verified before it was added. In pve-firewall master, src/PVE/Firewall.pm, compute_ipfilter_ipset_name returns "ipfilter-$iface". The Proxmox VE Firewall documentation (current, 8.x) lists all three names as standard IP sets. Both were read on 2026-10-29; nothing was re-measured on a node.
    • The mechanism is a new type field, reserved_names (anchored regexes), read by the tool, the module and the policy, and linted.
    • It is held by six mutations, each caught: the tool, then the module, never and always reserving; the policy never reserving; an unanchored entry.
  • Rows 8 and 9 corrected the same stale examples where they had spread. The runner at 1500902 in CONVENTION.md and conventions.yaml. The PBS example shr-bkp-pbs-main-vault (a v2 shape) in CONVENTION.md and the datastore note.
  • The cut commit (row 13).
    • Every emitted tag now reads naming.v4, and the policy fixtures and tests follow.
    • tests/address.py's own comment carried ref= with a tag as an example and had been counted as a 25th pin. It is reworded, and the sweep finds the 24 real ones.

make check

The cut was validated with a provisional local v4.0.0 tag, because address requires the declared tag to exist.

fmt            ok
lint           3451 types, 97 capabilities, 0 problem(s)
cases          77 cases, 0 failure(s)
portability    19 portability cases, 0 failure(s)
audit-test     23 resources audited, 0 failure(s)
mapping-sync   ok
tftest-sync    ok
tofu-test      101 passed, 0 failed
policy-test    66 tests, 66 passed; real aws/gcp/proxmox/scaleway plans: 0 deny, 0 warn
lints          44 mutations, 0 silent
english        96 files, 0 findings
address        24 pins @ v4.0.0 (present), 0 findings
artifact-sync  1 page, 44 names, 0 findings
reservations   0 findings

All five spot checks give the expected value: per-shr-ops-runner-vm-04, 1500904, per-shr-bkp-base-vault, management OK, per-shr-ops-foundation-proj. grep "to decide" adoption/home-node-infra.md finds nothing.

Still open after v4.0.0

  • A gap in make english: it let a French note through because the note contained none of the listed function words. The tool is not fixed in this release.
  • review/home-node-inventory is fully contained in this branch. It is not deleted here.
  • The read-only deploy key on home-node-infra has no remaining use, now that its archives are on its main (ba8d35d).
  • The rename perso -> personal comes last, by the maintainer, in the interface.

🤖 Generated with Claude Code

**100% integrated: rows 1-12 of section 6 landed by `0cf96f4`, row 13 with the cut (`b69e4a8`), `make check` green.** This merges `review/home-node-arbitration` into `main` as v4.0.0: the home node's eleven arbitrations on v3.0.0, its contradictory review, the maintainer's decisions on both (`decisions/2026-10-29-home-node-arbitration-review.md`), then the home node's exhaustive inventory - 198 objects confronted with the tool - and the fifteen arbitrations on it (`decisions/2026-10-29-home-node-inventory-arbitrations.md`, I1-I15). There is no v3.x: the patches merge untagged into this release. ## Rows to commits (inventory, section 6) | # | Change | Commit | |---|---|---| | 1 | Products `runner` (`hrn`) and `backup` (`hbk`) | `63106dd` | | 2 | A runner's env follows D3 | `dd14e13` | | 3 | Under `single`, the separator is what `substituted_controls` declares (D4) | `e2d5f58` | | 4 | Reserved IPSet names: `management`, `blacklist`, `ipfilter-net<n>` | `316e2d7` | | 5 | Scaleway default groups and policies are structural exceptions (I8) | `9131d2b` | | 6 | A Proxmox user is also a machine identity | `064c354` | | 7 | Tailscale header: the tag carries the org | `ab877fd` | | 8 | Review: project `per-shr-ops-foundation-proj`, runner at `1500904` | `9178d4e` | | 9 | Adoption guide: I1-I4, I6-I15, no "to decide" left | `c29c1e7` | | 10 | Consumer template, rules 9 and 10 (I13, I14) | `d96ef96` | | 11 | CHANGELOG: each entry landed with its row, then the introduction | `7bfe16a` | | 12 | `decisions/README.md` index | `ba99ce2` | | - | Template example (section 3): the proposed default | `0cf96f4` | | 13 | `registry/conventions.yaml` to version 4; `kit.tag` and every pin to `v4.0.0` | `b69e4a8` | **What the table does not show.** - **Row 4: `ipfilter-net<n>` was verified before it was added.** In pve-firewall master, `src/PVE/Firewall.pm`, `compute_ipfilter_ipset_name` returns `"ipfilter-$iface"`. The Proxmox VE Firewall documentation (current, 8.x) lists all three names as standard IP sets. Both were read on 2026-10-29; nothing was re-measured on a node. - The mechanism is a new type field, `reserved_names` (anchored regexes), read by the tool, the module and the policy, and linted. - It is held by six mutations, each caught: the tool, then the module, never and always reserving; the policy never reserving; an unanchored entry. - **Rows 8 and 9 corrected the same stale examples where they had spread.** The runner at `1500902` in `CONVENTION.md` and `conventions.yaml`. The PBS example `shr-bkp-pbs-main-vault` (a v2 shape) in `CONVENTION.md` and the datastore note. - **The cut commit (row 13).** - Every emitted tag now reads `naming.v4`, and the policy fixtures and tests follow. - `tests/address.py`'s own comment carried `ref=` with a tag as an example and had been counted as a 25th pin. It is reworded, and the sweep finds the 24 real ones. ## `make check` The cut was validated with a provisional local `v4.0.0` tag, because `address` requires the declared tag to exist. ``` fmt ok lint 3451 types, 97 capabilities, 0 problem(s) cases 77 cases, 0 failure(s) portability 19 portability cases, 0 failure(s) audit-test 23 resources audited, 0 failure(s) mapping-sync ok tftest-sync ok tofu-test 101 passed, 0 failed policy-test 66 tests, 66 passed; real aws/gcp/proxmox/scaleway plans: 0 deny, 0 warn lints 44 mutations, 0 silent english 96 files, 0 findings address 24 pins @ v4.0.0 (present), 0 findings artifact-sync 1 page, 44 names, 0 findings reservations 0 findings ``` All five spot checks give the expected value: `per-shr-ops-runner-vm-04`, `1500904`, `per-shr-bkp-base-vault`, `management` OK, `per-shr-ops-foundation-proj`. `grep "to decide" adoption/home-node-infra.md` finds nothing. ## Still open after v4.0.0 - **A gap in `make english`:** it let a French note through because the note contained none of the listed function words. The tool is not fixed in this release. - **`review/home-node-inventory`** is fully contained in this branch. It is not deleted here. - **The read-only deploy key on `home-node-infra`** has no remaining use, now that its archives are on its `main` (`ba8d35d`). - **The rename `perso` -> `personal`** comes last, by the maintainer, in the interface. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A test bench for the eleven points arbitrated on 2026-10-29, not a release: section 5 of
decisions/2026-10-29-home-node-arbitration-review.md proposes the split into six branches,
of which two are a major.

Accepted and implemented here:

  A1  the allocated identifier - a kind letter plus the VMID's own seven digits, across
      naming.py, the module and the policy. One coordinate, two uses, confronted rather
      than copied. The reservation joins vmids.yaml, and HOW it is confronted is declared
      per type: by_name where the plan carries the readable name, by_presence where the
      platform offers none - a blind spot written down instead of discovered.
  A8  a product code is global. 'pfm' was declared by both repositories and the global
      lookup answered with one of them, so decode() labelled a seeds name's target
      home-node-infra. Measured, linted, mutated, and home-node platform becomes hpf.
      The other half of the question was already enforced: a target code reading as a
      domain has been refused since v3, and 'plt' is now a mutation in the suite.
  A2c the substrate declared per repository, in registry/repositories.yaml - with A7's
      apply rule beside it. Three decisions said "declared per repository" and all three
      were written once, globally, in prose: grep found no reader outside conventions.yaml.
      The module now refuses a repo that declares nothing, which fires before a plan.

Corrected by measurement, against svalabs/forgejo 1.6.1 and 1.7.0, read from source:

  forgejo_organization renames by REPLACEMENT, not in place, and implements no ImportState
  at either tag. The entry said in_place with confidence: likely. A namespace rename is a
  decision about the forge, never a diff - which is the answer to A4.
  forgejo_repository.name renames in place, but .owner replaces: moving a repository
  between namespaces destroys it.
  The Tailscale device tag note had no org in it - the v2 shape, which makes two orgs on
  one tailnet indistinguishable.
  A Proxmox security group has 18 characters, so a direction belongs in the component:
  per-shr-net-in-sg fits, per-shr-net-base-in-sg comes back as a hash. A3 is measured,
  not a matter of taste.

85 module tests, 65 policy tests, 62 shared cases, 36 lint mutations with 0 silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three new arbitrations the review raised were put to the maintainer on 2026-10-29 and each
took the recommendation. N4 is deferred on purpose, N5 is answered by a temporary read-only
deploy key.

N1 - THE SHORTENED "compact" STYLE IS REMOVED FROM THE KIT. The six remaining SDN types
(two controllers, four fabrics) join the twelve already allocated, with the kind letters c and
f. What decided it was a measurement rather than the argument:

    component spine   ->  psnetspi
    component spineB  ->  psnetspi

Eight characters: p + s + net spends six, leaving THREE for the component. One plan holding both
was caught by the duplicate rule; two stacks, two plans, were caught by nothing - and an SDN
identifier is unique at cluster level, so the second apply fails or clobbers. There is no width
at which four codes and a word fit into eight characters.

70 occurrences over 19 files. The normative ones were rewritten; the closed decision record and
the closed changelog entries keep their words. And one guard disappeared rather than moving:
"a shortened name that cannot hold the run" has no subject left, because an allocated identifier
never carries one.

N2 - THE CROSSING DEPENDS ON THE SUBSTRATE. cloud: public_path_only, measured 2026-10-13;
machine: no_principal_on_the_substrate, the rented Proxmox cluster. The flat value had been
measured on a cloud and written as a universal, which forbade a case the home node had already
decided. The two are not interchangeable - allowing the machine's crossing on a cloud reopens the
argument the amendment of 2026-10-10 closed after two wrong turns in two days - and a mutation
says so. A2a is written down beside it as org_segment: owner_not_substrate.

N3 - EVERY NAMESPACE DECLARES A VISIBILITY, not only the reserved ones.
namespaces.visibility_default: private, by asymmetry: a namespace holding only private
repositories loses nothing by being private, and a public one publishes whatever it ever comes
to hold. That rule had been a comment about namespaces the block did not cover, so nothing would
ever have reported that two org namespaces were public.

AND ONE GUARD NO ARBITRATION ASKED FOR. tests/reservations.py confronts every vmids.yaml git
tracks with the width vmid() actually produces. It found four live drifts on its first run:
three five-digit reservations in examples/proxmox and the pre-v3 formula still documented in
templates/consumer/vmids.yaml, three days after the width changed. The policy looks a
reservation up by the key the plan computes, so one of the wrong width is never matched and
never reported - the registry stops being consulted rather than failing.

63 shared cases, 85 module tests, 62 policy tests (three of the shortened style went, two
allocated ones replaced them), 42 lint mutations with 0 silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by running home-node-infra's REAL inventory through the registry, with a temporary
read-only deploy key, on 2026-10-29. Before this commit:

    per-shr-ops-foundation-bkt-hn1    accepted by generate(), by check(), and by the policy

A Scaleway bucket whose name claims to sit on a Proxmox node, on a type whose rename is a
replacement. The registry has declared a provider on every location and on every type since v3.
Nobody had asked the question.

THE HOME NODE IS THE CASE THAT MAKES IT BITE, and the only one: it is the single repository
mixing two substrates - Scaleway objects in fr-par, Proxmox objects on hn1 - with ONE location
in the context. A single naming context would have named every Scaleway object for the node,
permanently.

The guard sits in all three implementations, as the kit's rule requires: naming.py refuses at
generation and at check, the module has a precondition naming both providers, the policy has a
deny that reads the location token out of the name. "any" passes (the global pseudo-location),
and a provider for which no location is declared - a forge, a tailnet - has nothing to be
confronted with.

AND IT FOUND THREE MIXED-PLATFORM FIXTURES IN THIS REPOSITORY'S OWN SUITE, all green until now:

  v3_grammar.tftest.hcl   a Proxmox VM at location par, in two separate runs
  naming_test.rego        an AWS bucket named see-prd-data-lake-raw-bkt-par

The first two are split into one call per platform, which is what the convention always meant by
"one call of the module per component"; the third was an AWS bucket wearing a Scaleway region
since the day it was written. Two refusal tests were added so the case that was passing now
fails.

Also recorded in the review document: the object-by-object pass itself. 26 types used, 26 in the
registry, 0 absent. Twenty-one named objects, twenty named with no truncation and no collision,
one refused - a vnet serving a product, which has no VMID digit allocated. That refusal is N6,
and it has the same "cheap exactly once" shape as A1 and A8.

And the finding that changes what this review can claim: the home node's own convention
documents are NOT on the forge. main is at 47781e0, dated 2026-10-23, one branch, and 453e50b
is not a valid object there. H1-H16, the full pass and D1-D5 live in a working tree and have
never been pushed.

87 module tests, 64 policy tests, 63 shared cases, 42 lint mutations with 0 silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The question came from home-node-infra's real inventory: its per-instance boundary objects - a
pool, a vnet, two storages - exist to serve another product, which is exactly what the target
slot is for, and the vnet was refused for want of a VMID digit.

DECIDED BY THE ARITHMETIC OF ONE DIGIT, not by taste. Zero means "no target", so nine values
exist for every entity and every product that ever appears as a Proxmox target - and eight
products are already declared, so the space would be full the day somebody declared what already
exists. Widening it is measured impossible: a kind letter plus eight digits is nine characters,
and a Proxmox SDN identifier holds eight. A digit is never reused, so allocating one is
permanent.

So a boundary object carries the bounded product as its COMPONENT:

    per-shr-plt-atelier-proj            the pool
    v1500801                            the vnet, domain plt, allocation 01
    per-shr-plt-atelier-vm-zfs          the guest storage
    per-shr-plt-atelier-snippets-dir    the snippets storage

It reads as "the platform's atelier block" and costs nothing. A product in the target slot still
has no digit, and asking for one is still REFUSED rather than read as zero - the guard that
raised the question stays exactly as it was.

The registry now records this as a DECISION WITH ITS DATE rather than as a wait: the note used
to say "the day one does, the product entries get digits", which a reader would take for a
pending task. It was asked on 2026-10-29 and sent back, and the next reader needs to know that.

AND ONE MEASUREMENT THAT WAS EXPECTED TO HURT AND DOES NOT. The home node lays an ACL per VMID
range, derived from the block. Under v4 the range moves and widens rather than scattering,
because only the last two digits vary within one product instance:

    today   block 11          110 .. 119        10 values
    v4      per/shr/ops   1500901 .. 1500999    99 values
            its templates 1900901 .. 1900999

Still contiguous, so the rights stack keeps its shape - vmid_first and vmid_last are computed
from the coordinate instead of from the block.

66 shared cases (three of them the boundary objects, so the choice is exercised rather than
remembered), 90 module tests, 64 policy tests, 42 lint mutations with 0 silent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
N6 is replaced by N6': a boundary object carries the pair of the product it
bounds, whoever creates it (per-shr-ops-atelier-proj, v1500901), as the
Scaleway project see-shr-ops-forge-proj already did. The host's own objects
are plt/platform (z1500801, per-shr-plt-in-sg), the state bucket is
per-shr-ops-foundation-state-bkt-par, and an ACL is laid per reserved VMID,
never on the domain's range - the instance counts the domain, so a range
would have handed the atelier the runner.

The org segment is now defined as the org of the boundary, the tenant's on a
rented substrate; the machine crossing gains its third clause, the tenant
trusting the operator. The home node declares agent_after_maintainer_ok.
The Forgejo note records that a renamed owner redirects over HTTP only
(serv.go resolves an SSH owner by GetUserByName, Forgejo v15.0.8).

Two guards a mutation had found silent are now held: the target digit has an
output of its own, and a shared case can express a refusal, matched on the
error's text. The policy's three new guards were mutated here; each is caught.

Refusing what the previous version accepted is a major unless its text
already forbade it, so the location guard is a patch. There is no v3.x.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
198 objects the home node creates or uses, in a state, outside OpenTofu and
planned, confronted with tools/naming.py at 24057d0: 114 named, 10 refused,
58 with no type, 15 to decide, 1 unread. Every refusal, missing type and open
question was decided by the maintainer on 2026-10-29 (I1-I15): the Scaleway
project follows N6', the runner and PBS become products, Tailscale in code
lives in identity, the firewall IPSet's reserved names are accepted, and the
objects the registry has no type for follow the consumer's own rule.

Section 6 is the integration checklist for review/home-node-arbitration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 1 of the inventory arbitrations' integration checklist. The runner's env is shr by
D3; PBS is not an object of platform, whose credential rules the host. The seeds-infra
page's target vocabulary carries the two new codes, since it must equal the registry's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 2 of the inventory arbitrations' integration checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 3 of the inventory arbitrations' integration checklist. CONVENTION.md required the env
segment to be present and distinct across state stores; on the home node everything is shr,
and repositories.yaml already declares the controls that take over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 4 of the inventory arbitrations' integration checklist. A new type field,
reserved_names (anchored regexes), read by naming.py, the module and the policy alike:
management, blacklist and ipfilter-net<n> on proxmox_virtual_environment_firewall_ipset.
ipfilter-net<n> verified in pve-firewall's Firewall.pm (compute_ipfilter_ipset_name) on
2026-10-29 before being added.

Held by mutation: the tool never or always reserving, the module never or always
reserving, the policy never reserving, and an unanchored entry - each caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 5 of the inventory arbitrations' integration checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 6 of the inventory arbitrations' integration checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 7 of the inventory arbitrations' integration checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 8 of the inventory arbitrations' integration checklist: section 6.2 gives the project
per-shr-ops-foundation-proj and management as a reserved name; section 6.5 puts the runner
at 1500904, after the atelier's three reserved VMIDs. The same stale example is corrected in
CONVENTION.md and registry/conventions.yaml, which said 1500902.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 9 of the inventory arbitrations' integration checklist. Every new name in the step 2
table was generated and then confirmed by naming.py check; nothing is left 'to decide'.
CONVENTION.md's PBS example (shr-bkp-pbs-main-vault, a v2 shape) becomes per-shr-bkp-base-vault.
The prompt's pins stay at v3.0.0, as tests/address.py requires until the cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 10 of the inventory arbitrations' integration checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 11 of the inventory arbitrations' integration checklist; each entry landed with the
commit of its own row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Row 12 of the inventory arbitrations' integration checklist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one point of the inventory left unarbitrated (section 3, last item): the proposed
default is applied - keep per-shr-plt-debian13-tpl-vm-01 and say in a note why the
template is called with the pair rather than through the product.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
release: v4.0.0 - naming.v4, and every pin follows kit.tag
Some checks failed
ci / check (pull_request) Has been cancelled
b69e4a84b2
Row 13 of the inventory arbitrations' integration checklist: registry/conventions.yaml
goes to version 4, so every emitted tag reads naming.v4; kit.tag and every copyable pin
move to v4.0.0 in the same commit, as tests/address.py requires. The policy fixtures and
tests, and the seeds-infra page's tag rows, emit naming.v4. CHANGELOG's unreleased section
becomes v4.0.0 - 2026-10-29.

tests/address.py's own comment carried 'ref=v3.0.0' as an example and was counted as the
25th pin: it is reworded, and the sweep now finds the 24 real ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
administrator deleted branch review/home-node-arbitration 2026-10-29 20:17:26 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
shared/infra-conventions!12
No description provided.