Repository navigation
fix(packaging): stop broadcasting the operator username in the default private SSID - #531
Amperstrand wants to merge 5 commits into
Conversation
|
Verification run (worktree at head Content parity with #487. Same fix at 99-tollgate-setup:729 ( Gate + negative control. Behavioral harness (real lines lifted from the shipped script, stub
Composition with #524. Ran #524's Two changelog defects to fix before merge (both in
Carried-over follow-ups from the #487 review, all still applicable on this tree (re-checked: Disposition: request-changes — only for the two CHANGELOG fixes (duplicate heading + missing PR link); the fix itself, the gate, deployed-SSID preservation, and #524 composition are all verified land-quality. |
|
Wave-3 follow-up fixup pushed ( |
|
Review at head The core fix is correct and verified at this head: default private SSID is now Three changes before merge:
[INFO] No CI has reported on this branch ( The person-name removal, the preserve-existing-SSID semantics, and the guard-test idea are all right. These three are cheap fixes in the scope the PR already claims; happy to re-review. |
felixfelix-bot
left a comment
There was a problem hiding this comment.
Review at head 217b0a88d79692e87bf3cf86acc97e897c63675f — Changes requested: 3 [RISK], no [BLOCK] (fleet gate 1 GLM + gate 2.5 cross-family cold audit, both run at this commit).
The core fix is correct and verified at this head: default private SSID is now TollGate-Private-${RANDOM_SUFFIX} (packaging/files/etc/uci-defaults/99-tollgate-setup:729), generated only when unset (:724-728), so no deployed network changes; the old literal is gone from the shipped tree; tests/contract/check-identity-leak.sh passes here (exit 0), fails correctly on the pre-fix tree (exit 1, 3 FAILs — negative control re-run locally), shellcheck -S warning clean, wired into both workflow twins (.github/workflows/test.yml:97-98, .ngit/act/workflows/test.yml:88-89) and hooks/pre-commit:26-29; changelog entry sits once under [Unreleased] → Fixed with the frozen v0.6.0-alpha4 section intact.
Three changes before merge:
-
[RISK]
99-tollgate-setup:729+check-identity-leak.sh:74-79— the new default and its gate hardcodeTollGate-, ignoring the brand selector.load_brand()(:57-66) makesBRAND_HOSTNAMEthe single source of shipped names ("AP SSIDs … follow this single selector", :45-49):net4satsbuilds broadcastNet4sats-XXXXon the public APs (:336, :301) but will now broadcastTollGate-Private-XXXXon the private AP — a shipped default naming the wrong product, the exact rebrand scenario the changelog invokes. And the gate's prefix case (TollGate-*|TollGate) would fail the brand-correct value, so CI now enforces the wrong name. Use${BRAND_HOSTNAME}-Private-${RANDOM_SUFFIX}and derive the gate's expected prefix the same way (or state why the private SSID is deliberately brand-invariant). -
[RISK]
CONTRIBUTING.md:324-334— stale guidance now contradicts this PR. It still documentsc08r4d0r-<suffix>as the default private SSID prefix and instructs reviewers: "Do not report it or a guard test on it as an 'identity leak'." A reviewer following CONTRIBUTING today must reject this PR and callcheck-identity-leak.sha bug. The PR already edits docs (release-process.md,CHANGELOG.md); update this section in the same change. -
[RISK]
check-identity-leak.sh:1-2,13+CHANGELOG.md:20-25— the gate and changelog claim more than they enforce. The script asserts "nothing the package ships advertises who built it" and callspackaging/files/"the tree the .ipk/.apk payload is staged from" — but the payload also installs the compiled binaries (packaging/Makefile:159-160→/usr/bin/tollgate-wrt,/usr/bin/tollgate), and those embed three of the five forbidden identities as shipped defaults:src/config_manager/config_manager_config.go:286-294(c08r4d0r,amperstrand,origami74at 0.07 profit-share) andsrc/config_manager/config_manager_identities.go:46-56(same names plus personal Lightning addresses, e.g.c3e23eb5e3d00f18b2f4f588@coinos.io). The gate passes while the shipped binary carries exactly what its header forbids, and the changelog's "free of operator-identity literals" is false as written. Either narrow the claim to the private-SSID default andpackaging/files/, or extend the scan tosrc/— noting that the honest version of the scan fails today and forces the real decision on the profit-share defaults.
[INFO] No CI has reported on this branch (gh pr checks → none); all claims above were verified at the commit level locally instead.
The person-name removal, the preserve-existing-SSID semantics, and the guard-test idea are all right. These three are cheap fixes in the scope the PR already claims; happy to re-review.
|
Follow-up: the three Head
Reproduction at |
|
Re-review at head Finding 1 — hardcoded Finding 2 — stale CONTRIBUTING.md contradicting the PR: RESOLVED. Finding 3 — gate/changelog claiming more than they enforce: RESOLVED by narrowing honestly. Verification re-run at head (fresh clone at
Non-blocking follow-ups (none hold approval):
APPROVED. |
felixfelix-bot
left a comment
There was a problem hiding this comment.
Re-review at head 8808d7a13acf28b67501f051cf45dedac6d15f7b (was 217b0a88d796), covering the fix commit "derive the default private SSID from the brand selector". APPROVED — all three round-1 [RISK] findings are resolved and re-verified at this head.
Finding 1 — hardcoded TollGate- prefix ignoring the brand selector: RESOLVED.
99-tollgate-setup:733 now reads private_ssid="${BRAND_HOSTNAME}-Private-${RANDOM_SUFFIX}". Ordering is sound: load_brand() runs at :916, before setup_private_network at :931 in the full-setup path; the same-version path calls it at :839 and exits at :908 before the private-network step. Composed defaults verified for both selector values: net4sats build → Net4sats-Private-XXXX (no collision with the public Net4sats-XXXX), default/unknown brand → TollGate-Private-XXXX. The new gate invariant C (check-identity-leak.sh:106-110) pins the exact literal — re-introducing the hardcoded prefix fails the gate (verified by negative control, exit 1).
Finding 2 — stale CONTRIBUTING.md contradicting the PR: RESOLVED.
CONTRIBUTING.md:324-342 now draws the line cleanly: the shared nym may appear in documentation and examples, but a shipped default never carries an operator handle and always derives from the brand selector — "a handle in a documented example is not a leak, a handle in a shipped default is." A reviewer following this section today reaches the same verdict as this PR instead of being instructed to reject it.
Finding 3 — gate/changelog claiming more than they enforce: RESOLVED by narrowing honestly.
The gate header (check-identity-leak.sh:2-11) now states its scope is packaging/files/, names the unscanned compiled-binary identities (c08r4d0r, amperstrand, origami74 — the documented 0.07 profit-share examples, README.md:150 / docs/merchant.md:600) and explicitly leaves whether those stay as "its own decision". The closing message (:125) and the CHANGELOG entry now claim only packaging/files/ defaults. No false scope claims remain.
Verification re-run at head (fresh clone at 8808d7a; no CI reports on this branch — gh pr checks → none, 0 check-runs and 0 workflow runs on the head SHA, and the repo's Actions queue has every run queued 14h+ including on main, so the absence is repo-wide, not branch-specific; everything below ran locally at the exact head):
- gate exit 0 at head — 6 checks pass (A once, B for both brand literals, C×3), including the new invariant B
- negative controls all fire: hardcoded
TollGate-Private-prefix → exit 1;BRAND_HOSTNAME="c08r4d0r"→ exit 1 (invariants A + B both trip); identity literal planted underpackaging/files/→ exit 1; the round-1 head itself now fails tightened invariant C, as it should shellcheck -S warningclean on the gate;bash -nclean on both scripts; the setup script's SC3043 (×23)/SC2046 (×1) warnings are byte-identical on basea99d983, round-1 head and this head — pre-existing, none in the changed region- wiring intact:
.github/workflows/test.yml:98,.ngit/act/workflows/test.yml:89,hooks/pre-commit:28 - CHANGELOG entry sits once under
[Unreleased] → Fixed; frozenv0.6.0-alpha4section intact - preserve-existing-SSID semantics unchanged (
:724-728): default only generated when unset — no deployed network changes - no other shipped file generates a private SSID (repo-wide grep: only
99-tollgate-setup); sibling contract suites green at head (private-subnet 12/12, same-version-allowlist 43/43, band 22/22, nodogsplash-443 19/19)
Non-blocking follow-ups (none hold approval):
- [NIT]
CHANGELOG.md:32-34still has a 3-blank-line run before## [v0.6.0-alpha4](carried from round 1). - [NIT]
check-identity-leak.sh:43,55,68-79— invariant matching is case-sensitive: a capitalizedBRAND_HOSTNAME="Amperstrand"passes both A and B today (verified: gate exit 0).grep -iEon the identity patterns would close it. - [NIT]
check-identity-leak.sh:90—sed … | head -1reads "the firstprivate_ssid=literal in the file", not specifically the one insidesetup_private_network(); a decoy literal earlier in the file would mask a hardcoded real default. Scoping the extraction to the function body would make invariant C match what actually runs. - [INFO]
check-identity-leak.sh:68-71— underset -euo pipefail, a zero-matchgrepat:68aborts before the advisory:70-71can print; the check still fails closed, only the message is dead code. - [INFO] The narrowed gate leaves
src/config_manager/config_manager_config.go:286-294/config_manager_identities.go:46-58(operator identities + personal Lightning addresses compiled into the shipped binaries) uncovered by any gate — pre-existing, outside this diff, but worth a maintainer decision of its own.
APPROVED.
…t private SSID
The private network's SSID is generated by 99-tollgate-setup when neither
radio has one yet, and it was written as "c08r4d0r-${RANDOM_SUFFIX}" - a
maintainer's own handle. That string is broadcast in the AP beacon of every
router that took the default, so any WiFi scanner in range read it: flashing
a router published the operator's username.
The default is now "TollGate-Private-${RANDOM_SUFFIX}" - a product name
plus the same device-unique suffix. Routers that already have a private SSID
keep it, because the default is only generated when the option is unset, so
no deployed network changes.
tests/contract/check-identity-leak.sh encodes the invariant: no maintainer
identity literal anywhere under packaging/files/, and the generated default
private SSID stays device-unique and product-prefixed. It is wired into the
build-purity CI job (both lanes), hooks/pre-commit, and the release-process
gate list.
…not privacy The old handle named the operator of that router, on that operator's own premises — not a privacy leak. What the change actually buys: the default describes the network's purpose (whitelisted devices, no payment) and no shipped default embeds a specific operator's identity, which matters for rebranded or resold routers.
… the duplicate section heading The wave-3 rebase artifact: the entry landed inside the frozen [v0.6.0-alpha4] section (whose tag already exists) and left two blank-separated '### Changed / Internal' headings at its end. The entry moves to a fresh [Unreleased] -> Fixed; one duplicate heading goes.
…or (#540) * fix(packaging): derive the default private SSID from the brand selector The private network's default SSID was made `TollGate-Private-${RANDOM_SUFFIX}`, which named the product but ignored `load_brand()`: a net4sats build broadcast `Net4sats-XXXX` on its open APs and `TollGate-Private-XXXX` on its own private network, and the identity-leak gate enforced that wrong name by pinning a literal `TollGate-` prefix. The default is now `${BRAND_HOSTNAME}-Private-${RANDOM_SUFFIX}`, and check-identity-leak.sh derives the expectation the same way: it reads the brand -> BRAND_HOSTNAME mapping out of the setup script, rejects a mapping that names an identity literal or is not a product name, and requires the default to be exactly the brand-composed form. A hardcoded product name now fails the gate instead of passing it. The gate's own claim is narrowed to what it enforces: packaging/files/ (the config/script tree the payload stages from) and the defaults a device broadcasts. The compiled binaries built from src/ carry the documented representative operator identities (README.md, docs/merchant.md) and are out of scope here — that is a separate decision, stated in the header rather than implied. * docs(contributing): a shipped default never carries an operator handle The operator-nym section still documented `c08r4d0r-<suffix>` as the default private SSID prefix and told reviewers not to report it as an identity leak, so following CONTRIBUTING meant rejecting the change that removes it. The section now draws the line the code draws: the shared nym may appear in documentation and examples (the reader replaces them), never in a shipped default — anything a device broadcasts reads to anyone in radio range — and `tests/contract/check-identity-leak.sh` enforces that for packaging/files/. --------- Co-authored-by: felixfelix-bot <felixfelix-bot@users.noreply.github.com>
8808d7a to
324c16d
Compare
|
Card-level re-verification at head Independently re-verified first-hand in a fresh clone at
Verdict unchanged: APPROVED stands at One new fact since that review posted — non-blocking for the review, blocking for landing:
|
|
Re-review at head APPROVED — no change requests.The default private SSID ( All three round-1 [RISK] findings are resolved at this head:
Evidence re-derived first-hand at
[INFO] [INFO] The intermediate CHANGELOG commits ( [INFO] Gate 2.5 cold audit (DeepSeek, cross-family): AGREE — 8/8 claims verified first-hand at a fresh detached worktree of this head (branch state, SSID assignment + preserve-guard, (Fleet review loop: gate 1 GLM, gate 2.5 cold audit DeepSeek — cross-family vs author.) |
felixfelix-bot
left a comment
There was a problem hiding this comment.
Re-review at head 992f4f4cbba99ac616757744bf1dfc40de7eeaaf (branch rebased onto main since the last APPROVED at 8808d7a; history rewritten, so every claim below was re-derived first-hand at the new head, full diff vs main @ 2226a04c, 8 files +177/−109).
APPROVED — no change requests.
APPROVED at head 992f4f4 — no change requests. Full re-review evidence: see the head-anchored comment (issuecomment-5815205259). All three round-1 [RISK] findings resolved; identity-leak gate exit 0; gitleaks clean; shellcheck delta 0; zero Go changes. Gate 2.5 cold audit (DeepSeek): AGREE, 8/8 claims verified.
|
Technical review (evidence pass) — PR head The fix is right, and I verified it works. The one-line behavior change in 99-tollgate-setup replaces the The gate is real and catches the old world. What blocks the merge is the CHANGELOG, not the change. As pushed, the head's CHANGELOG state is a rebase casualty in two ways:
Commit 3's message claims the wave-3 rebase artifact was cleaned and commit 5 claims the resurrected duplicate was dropped, but the diff against the merge-base still carries both problems, so the cleanup was incomplete. Concrete fix: rebase onto current main (the conflict GitHub already reports is precisely this file), take main's side wholesale for everything below Smaller items, none blocking on their own:
Disposition: request-changes — for the CHANGELOG restoration, the duplicate heading, and the rebase onto current main (plus the PR-link and body touch-up while in there). The substantive change — the uci-defaults fix, the gate script, the wiring, and the CONTRIBUTING policy reversal — is verified correct and ready; once the changelog delta is regenerated as a pure addition, this should land without another review round. |
|
Re-review at head The code fix is correct and unchanged at this head. The blockers are in Verified correct at this head — no objection to the change itself:
[BLOCK]
[RISK] [NIT] [INFO]
Once the changelog delta is regenerated as a pure addition (plus the |
felixfelix-bot
left a comment
There was a problem hiding this comment.
Superseding the standing APPROVED review [#5305199472] at this head (992f4f4cbba99ac616757744bf1dfc40de7eeaaf): the branch's rebased CHANGELOG deletes five entries its own merge-base and main still carry (19 added / 101 deleted), plus a duplicate empty ### Changed / Internal heading. The uci-defaults fix itself is verified correct and unchanged. Full evidence: #531 (comment)
Resolving a CHANGELOG conflict with a section-level 'take ours' can resurrect an entry a branch commit had already moved or reworded — the same bullet then ships in two sections, release notes describe one change twice, and entry-counting fold scripts go wrong. This happened twice in one rebasing session (#524 and #531 both carried a resurrected stale copy past a --ours resolution until audited). check-changelog-duplicates.py fails when the same bold lead-in appears in two sections and at least one is [Unreleased] — the live edit zone. The same lead-in inside already-released sections (15 such pairs exist in history, e.g. 'gonuts re-pin.') is frozen history and deliberately tolerated: release notes are not rewritten. Wired into hooks/pre-commit next to the version-sync check; pinned by changelog-duplicates_test.sh across all three verdicts. PR #581 review follow-ups, folded into the same commit: - Wire the check and its verdict script into .github/workflows/test.yml beside the deps/import contract checks: pre-commit cannot gate sequencer commits (a bad --ours resolution during rebase --continue sails past the hook), so CI is the only place the check runs for everyone regardless of hooks. - Scope the docstring and the failure/success wording to the version-section keying actually enforced — the same lead-in in two version sections with [Unreleased] among them — instead of promising lead-in-wide uniqueness the frozen-history tolerance contradicts. - Append the PR link to the changelog entry; the rebase onto post-#577 main resolves the CHANGELOG conflict keep-both and drops the branch's inherited orphaned second [Unreleased] section.
|
Fix-and-rebase run against the review's blocking list — body refreshed; CHANGELOG/rebase deferred on a Per finding:
Verification run this pass: gate at head Head: |
|
Decision: closing as superseded by #605 — and the default nym stays. #605's "one device code" ADR ( Why that's the right outcome, on the merits. This PR's defect was real when it was filed: the default SSID broadcast what was effectively the operator's personal handle, with no decision behind it and no operator awareness. #605's model removes exactly that defect by a different route: What survives from this PR for whoever next touches SSID defaults: the harness and gate-script wiring (the #524 thread carries the follow-on work of updating the SSID contract to the #605 device-code model), and the CONTRIBUTING policy discussion. The uci-defaults change itself is dead by decision. |
Resolving a CHANGELOG conflict with a section-level 'take ours' can resurrect an entry a branch commit had already moved or reworded — the same bullet then ships in two sections, release notes describe one change twice, and entry-counting fold scripts go wrong. This happened twice in one rebasing session (#524 and #531 both carried a resurrected stale copy past a --ours resolution until audited). check-changelog-duplicates.py fails when the same bold lead-in appears in two sections and at least one is [Unreleased] — the live edit zone. The same lead-in inside already-released sections (15 such pairs exist in history, e.g. 'gonuts re-pin.') is frozen history and deliberately tolerated: release notes are not rewritten. Wired into hooks/pre-commit next to the version-sync check; pinned by changelog-duplicates_test.sh across all three verdicts. PR #581 review follow-ups, folded into the same commit: - Wire the check and its verdict script into .github/workflows/test.yml beside the deps/import contract checks: pre-commit cannot gate sequencer commits (a bad --ours resolution during rebase --continue sails past the hook), so CI is the only place the check runs for everyone regardless of hooks. - Scope the docstring and the failure/success wording to the version-section keying actually enforced — the same lead-in in two version sections with [Unreleased] among them — instead of promising lead-in-wide uniqueness the frozen-history tolerance contradicts. - Append the PR link to the changelog entry; the rebase onto post-#577 main resolves the CHANGELOG conflict keep-both and drops the branch's inherited orphaned second [Unreleased] section.
…ollGate#531's TollGate-Private-XXXX is the post-OpenTollGate#531 state and OpenTollGate#531 is still open; the shipped 99-tollgate-setup broadcasts c08r4d0r-${RANDOM_SUFFIX}. Say that, and point at OpenTollGate#531 for the rename (PR OpenTollGate#524 review follow-up).
* test(contract): pin the shipped AP SSID format
The open AP is named `<brand>-<4 hex characters>` (`TollGate-A1B2` for the
default brand) and both radios carry that one name. Nothing enforced it: the
4-character device suffix and the single-name property were conventions in
99-tollgate-setup, so a band suffix ("TollGate-A1B2-2.4GHz") or a hardcoded
brand could come back unnoticed. The Go side keys on the `TollGate-` prefix
(discovery_log.go `hasTollGateSSID`, vendor_element_manager.go
`calculateScore`), and the SSID rides in every beacon, so the drift is both a
client-matching and a naming-honesty bug.
tests/contract/check-ssid-format.sh asserts the contract in two layers:
- behavioural: sources the real load_brand()/setup_public_wifi() out of the
script (driver stripped) and runs them against a stub `uci` that records
what they would have written, then checks the SSID actually generated —
both radios matching ^TollGate-[0-9A-F]{4}$ for the default brand, one
shared name for both radios, a per-device 4-hex suffix over 6 generations.
- structural: checks the assignment sites a checkout cannot execute — the
same-version reinstall path recovers its SSID from live config and the
whitelabel branch reads /etc/tollgate/brand — failing on a band token or on
an SSID written from anything but a variable.
Red evidence, all reverted before this commit: appending "-2.4GHz" to the
gateway name fails 12 checks; hardcoding the brand at the `.ssid` write fails
the structural layer (which the behavioural layer alone cannot see); deleting
the write fails the anti-vacuity guard.
Wired into both CI test workflows (build-purity job), hooks/pre-commit, and
the release/contribution runbooks.
Refs: t_9e5f0b8d
* docs(readme): document SSID format conventions
Public AP is BRAND-XXXX with a 4-hex uppercase suffix from /dev/urandom,
shared across the 2.4GHz and 5GHz radios (band steering). Private
management AP is BRAND-Private-XXXX with a WPA2 word-word-word-NN
passphrase. nodogsplash gatewayname derives as '<gateway> Portal'.
The public format is pinned by tests/contract/check-ssid-format.sh.
Documents the post-fix private SSID (TollGate-Private-XXXX); the
operator-username variant (c08r4d0r-XXXX) is a security leak being
fixed on pr/private-ssid-no-operator-username.
* test(contract): follow the band-aware setup in the SSID harness
Main's setup now resolves radios by band before assigning APs
(detect_band_radios -> setup_band_ap), so the harness bootstraps a
banded first-boot wireless state, calls detect_band_radios before
setup_public_wifi, and reads the SSIDs back from the tollgate_*_open
sections the band-aware setup writes (adopting or creating them per
radio). The asserted contract is unchanged: one shared
`^TollGate-[0-9A-F]{4}$` name on both open radios, per-device suffix,
no band tokens.
* docs(changelog): move the SSID-format contract entry to [Unreleased]
The wave-3 catch: the entry sat inside the frozen [v0.6.0-alpha4]
section (the tag exists); new entries belong in [Unreleased].
* docs(readme): the private-AP bullet documents today's SSID, not #531's
TollGate-Private-XXXX is the post-#531 state and #531 is still open;
the shipped 99-tollgate-setup broadcasts c08r4d0r-${RANDOM_SUFFIX}.
Say that, and point at #531 for the rename (PR #524 review follow-up).
* test(contract): assert the acceptance domain [A-Z0-9], not hex-only codes
Codes minted by the current writers are hex, but the contract's acceptance
domain is [A-Z0-9] (adopted codes from earlier writers, e.g. the
pre-device-code installer's full-alphanumeric mints). Per #618's ADR
clarification.
* test(contract): follow the one-device-code model in the SSID harness
#605 changed the derivation this gate pins: the SSID is no longer a
per-boot RANDOM_SUFFIX but DEVICE_SSID, built by setup_device_identity
from a code minted once and stored in /etc/config/tollgate. The harness
still modelled the pre-#605 world — setup_public_wifi died on
'DEVICE_SSID: unbound variable' after load_brand's brand read, and the
checker was 13 checks red at head.
The behavioural layer now runs the driver's own order (load_brand →
setup_device_identity → detect_band_radios → setup_public_wifi) with
DEVICE_CODE_STORE redirected into the per-run state (the env seam the
device-code tests use), so a wiped state is a fresh device that really
mints, and a new keep-mode run pins the other half of the contract: a
same-store rerun adopts the stored code and never re-mints.
The stub uci now renders section types bare in show output like the real
uci (the band-test shim's rule): with quoted section types band
detection and AP adoption were silently disabled, so the harness
exercised the fallback create path while claiming to follow the banded
first-boot state. The readback follows whichever sections the APs
actually landed on (the adopted default_radioN today) instead of keying
on tollgate_*_open, so the labels cannot lie about the path that ran.
Assertion domain is the full [A-Z0-9] code alphabet (this side's mint
emits hex; the installer's emits all of [A-Z0-9]; the contract is the
shared alphabet), and B2 accepts the $DEVICE_SSID derivation in place
of the removed RANDOM_SUFFIX/hexdump forms.
Verified: green at head (18 ok / 0 fail, 5 consecutive runs); red on
fresh-archive mutations — band suffix on the derivation (exit 1),
hardcoded literal at GATEWAY_NAME (exit 1: distinctness + store/SSID
disagreement + B2), deleted public ssid write (exit 1); band suite
22/22, device-code suite 72/72.
* docs: the SSID contract wording follows the device-code alphabet
The README public-AP bullet still called the XXXX a '4-hex uppercase
device code' and the changelog entry still pinned ^TollGate-[0-9A-F]{4}$
and a 'per-device 4-hex suffix' — both stale against the #605 decision
(docs/architecture/one-device-code.md): the code is four characters of
[A-Z0-9] (this side's mint emits hex, the installer's emits the full
alphabet), minted once and reused from the store. The changelog entry
now also describes the harness as it works after the one-device-code
adaptation: the identity chain is sourced and run, the stub renders
uci show section types bare, and the code-reuse property is asserted.
---------
Co-authored-by: felixfelix-bot <felixfelix-bot@users.noreply.github.com>
Co-authored-by: Amperstrand <amperstrand@localhost>
Resolving a CHANGELOG conflict with a section-level 'take ours' can resurrect an entry a branch commit had already moved or reworded — the same bullet then ships in two sections, release notes describe one change twice, and entry-counting fold scripts go wrong. This happened twice in one rebasing session (#524 and #531 both carried a resurrected stale copy past a --ours resolution until audited). check-changelog-duplicates.py fails when the same bold lead-in appears in two sections and at least one is [Unreleased] — the live edit zone. The same lead-in inside already-released sections (15 such pairs exist in history, e.g. 'gonuts re-pin.') is frozen history and deliberately tolerated: release notes are not rewritten. Wired into hooks/pre-commit next to the version-sync check; pinned by changelog-duplicates_test.sh across all three verdicts. PR #581 review follow-ups, folded into the same commit: - Wire the check and its verdict script into .github/workflows/test.yml beside the deps/import contract checks: pre-commit cannot gate sequencer commits (a bad --ours resolution during rebase --continue sails past the hook), so CI is the only place the check runs for everyone regardless of hooks. - Scope the docstring and the failure/success wording to the version-section keying actually enforced — the same lead-in in two version sections with [Unreleased] among them — instead of promising lead-in-wide uniqueness the frozen-history tolerance contradicts. - Append the PR link to the changelog entry; the rebase onto post-#577 main resolves the CHANGELOG conflict keep-both and drops the branch's inherited orphaned second [Unreleased] section.
Resolving a CHANGELOG conflict with a section-level 'take ours' can resurrect an entry a branch commit had already moved or reworded — the same bullet then ships in two sections, release notes describe one change twice, and entry-counting fold scripts go wrong. This happened twice in one rebasing session (#524 and #531 both carried a resurrected stale copy past a --ours resolution until audited). check-changelog-duplicates.py fails when the same bold lead-in appears in two sections and at least one is [Unreleased] — the live edit zone. The same lead-in inside already-released sections (15 such pairs exist in history, e.g. 'gonuts re-pin.') is frozen history and deliberately tolerated: release notes are not rewritten. Wired into hooks/pre-commit next to the version-sync check; pinned by changelog-duplicates_test.sh across all three verdicts. PR #581 review follow-ups, folded into the same commit: - Wire the check and its verdict script into .github/workflows/test.yml beside the deps/import contract checks: pre-commit cannot gate sequencer commits (a bad --ours resolution during rebase --continue sails past the hook), so CI is the only place the check runs for everyone regardless of hooks. - Scope the docstring and the failure/success wording to the version-section keying actually enforced — the same lead-in in two version sections with [Unreleased] among them — instead of promising lead-in-wide uniqueness the frozen-history tolerance contradicts. - Append the PR link to the changelog entry; the rebase onto post-#577 main resolves the CHANGELOG conflict keep-both and drops the branch's inherited orphaned second [Unreleased] section.
Resolving a CHANGELOG conflict with a section-level 'take ours' can resurrect an entry a branch commit had already moved or reworded — the same bullet then ships in two sections, release notes describe one change twice, and entry-counting fold scripts go wrong. This happened twice in one rebasing session (#524 and #531 both carried a resurrected stale copy past a --ours resolution until audited). check-changelog-duplicates.py fails when the same bold lead-in appears in two sections and at least one is [Unreleased] — the live edit zone. The same lead-in inside already-released sections (15 such pairs exist in history, e.g. 'gonuts re-pin.') is frozen history and deliberately tolerated: release notes are not rewritten. Wired into hooks/pre-commit next to the version-sync check; pinned by changelog-duplicates_test.sh across all three verdicts. PR #581 review follow-ups, folded into the same commit: - Wire the check and its verdict script into .github/workflows/test.yml beside the deps/import contract checks: pre-commit cannot gate sequencer commits (a bad --ours resolution during rebase --continue sails past the hook), so CI is the only place the check runs for everyone regardless of hooks. - Scope the docstring and the failure/success wording to the version-section keying actually enforced — the same lead-in in two version sections with [Unreleased] among them — instead of promising lead-in-wide uniqueness the frozen-history tolerance contradicts. - Append the PR link to the changelog entry; the rebase onto post-#577 main resolves the CHANGELOG conflict keep-both and drops the branch's inherited orphaned second [Unreleased] section.
…581) Resolving a CHANGELOG conflict with a section-level 'take ours' can resurrect an entry a branch commit had already moved or reworded — the same bullet then ships in two sections, release notes describe one change twice, and entry-counting fold scripts go wrong. This happened twice in one rebasing session (#524 and #531 both carried a resurrected stale copy past a --ours resolution until audited). check-changelog-duplicates.py fails when the same bold lead-in appears in two sections and at least one is [Unreleased] — the live edit zone. The same lead-in inside already-released sections (15 such pairs exist in history, e.g. 'gonuts re-pin.') is frozen history and deliberately tolerated: release notes are not rewritten. Wired into hooks/pre-commit next to the version-sync check; pinned by changelog-duplicates_test.sh across all three verdicts. PR #581 review follow-ups, folded into the same commit: - Wire the check and its verdict script into .github/workflows/test.yml beside the deps/import contract checks: pre-commit cannot gate sequencer commits (a bad --ours resolution during rebase --continue sails past the hook), so CI is the only place the check runs for everyone regardless of hooks. - Scope the docstring and the failure/success wording to the version-section keying actually enforced — the same lead-in in two version sections with [Unreleased] among them — instead of promising lead-in-wide uniqueness the frozen-history tolerance contradicts. - Append the PR link to the changelog entry; the rebase onto post-#577 main resolves the CHANGELOG conflict keep-both and drops the branch's inherited orphaned second [Unreleased] section. Co-authored-by: Amperstrand <amperstrand@localhost>
…nflicts stop existing (#679) * feat(changelog): fold changelog.d fragments into CHANGELOG.md Two PRs that change operator-visible behaviour both append one bullet to the same anchor of CHANGELOG.md, so the second merge conflicts every time — about a dozen hand-resolved hunks on the 0.6.0 train, and two of those resolutions (#524, #531) resurrected an entry a branch had already moved past a keep-both-sides resolution. 15 open PRs carry a CHANGELOG.md hunk at once as this lands, so the merge order was also a changelog-editing order. A fragment is one file per change — changelog.d/<pr>-<slug>.<type>.md, the body being the bullet verbatim — so unique filenames cannot conflict and a merge never touches the changelog at all. `scripts/changelog-assemble.py fold` writes the accumulated entries into the release section in one deterministic step (one wave under the release heading, newest PR first, subsections in canonical order) and deletes the folded fragments; `check` validates names and bodies; `--dry-run` prints the wave and writes nothing. The contract test went red first — 21 failing cases with the tool absent — and green after: 18 cases covering where entries land, the order they land in, what is deleted, the dry run, and each verdict of check. shellcheck clean; the fold was also run against the real 3,500-line CHANGELOG.md in a copy to see the wave it produces (folded into `## [Unreleased]`, everything else untouched). Decision and the rejected alternatives: docs/architecture/changelog-fragments-decision.md * chore(changelog): validate fragments in the pre-commit hook and CI `check` runs where the other contract checks run: the pre-commit hook refuses a malformed fragment (wrong filename, unknown type, a body that is not the bullet) before it is committed, and the `contract-lint` job runs the check plus the tool's contract test on every PR. Wired into `.github/workflows/test.yml` and its ngit port, which is the lane that actually executes. * docs(changelog): record the fragment decision and the workflow The decision record states the problem (one shared anchor, a dozen hand-resolved hunks a train, two resurrections), the decision (fragments + one fold), and the alternatives it rejects — a `merge=union` gitattributes driver (hides the shared anchor and silently keeps both sides of a bullet that was rewritten) and generating the changelog from commit subjects (loses the prose this changelog is worth reading for). CONTRIBUTING sends authors to changelog.d/ instead of CHANGELOG.md, and the release process folds the accumulated fragments in its own commit before the release commit is cut. * docs(changelog): this PR's own entry is a fragment The rule this PR adds applies to it: the changelog entry lives in changelog.d/679-changelog-fragments.added.md and is not folded here, so the first fold the maintainer runs produces it. --------- Co-authored-by: Felix <felix@opentollgate.org>
Replaces #527 (same branch, fresh PR — the old one's head index wedged after force-push churn).
The private network's default SSID was
c08r4d0r-${RANDOM_SUFFIX}— a maintainer's handle broadcast in every default-configured router's AP beacon. Now${BRAND_HOSTNAME}-Private-${RANDOM_SUFFIX}(#540: derived from thebrandselector like every other shipped name, so anet4satsbuild broadcastsNet4sats-Private-XXXXon its private network rather than another brand's product name), withtests/contract/check-identity-leak.sh(CI gate in both twins + pre-commit) failing if an identity literal reappears underpackaging/files/or the default stops being device-unique and brand-derived. Deployed networks untouched (default only generates when unset).Verified at commit level on this head: fix present, gate script wired in both workflow twins, identity-leak gate green; negative control (pre-fix tree → exit 1) verified during review.