Proof of concept mechanical port of ircnet/ircd to Rust as part of a bit about C being insecure for network services
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(leveva): options { default-snomask } REHASH knob — configurable bare-+s default — P11 slice 266

Promote the bare-`+s` default snomask from a hard-coded SnoMask::ALL to a
REHASH-able `options { default-snomask }` config knob (charybdis
`default_snomask`). A bare `MODE <nick> +s` (no following mask token) now seeds
the configured categories instead of every category.

Closes the deferral named verbatim in slice 257's divergence list ("No config
knob for the default mask (a future slice could add one, the MAX_PENALTY 208->210
trajectory)") via the same ConfStore live-or-default seam MAX_PENALTY (208->210),
max-bans (245->246) and service-string (256) took.

- snomask::parse_config(s) -> Result<u32, char>: strict config parser (rejects a
typo'd letter) vs the lenient runtime apply_delta; pure+total+panic-free fuzz seam.
- Options.default_snomask + parser + ConfBlocks/ConfStore/live_default_snomask()
+ boot seed; command::mode bare-+s reads live_default_snomask().unwrap_or(ALL).
- examples/ + dist/ ircd.kdl documented line.

Tests: snomask + config-parse + control round-trip/re-seed-inverse units;
golden_default_snomask_config.rs (subprocess, isolates the global singleton:
oper bare +s -> 008 +ck, not the all-categories default); fuzz
default_snomask_proptest.rs (never-panics + subset-of-ALL round-trip +
strict-reject/lenient-ignore contrast).

Gate: cargo test -p leveva green; cargo clippy -p leveva --tests clean;
cargo build --workspace 0 warnings; no 004/221/welcome-burst snapshot churn.

+444 -3
+1 -1
PLAN.md
··· 90 90 | **P8 — Delete the C + retire the oracle harness ✅ DONE** (per-sub-phase: [`PLAN-P8-progress.md`](PLAN-P8-progress.md) + [`docs/progress-log/p8.md`](docs/progress-log/p8.md)) | remaining C (the ~25 variadic sender trampolines + residual data globals) + `ircd-sys` C build + `ircd-golden` (L2) + `ircd-testkit`/`cref_*` archives (L1) + the frozen reference‑C tree | With all C _logic_ already Rust (P7 exit), delete the last C: replace the variadic sender trampolines with a non‑variadic Rust sender API at every call site (faithful `wire!`/`reply_one!`/`format!` first; leveva typed messages adopted alongside the IRC senders), drop the residual data globals via the data‑symbol seam (bindgen `extern` decls resolve to `#[no_mangle]` Rust defs at link), then drop **all** C compilation from `ircd-sys` (no more `cc::Build`), retire the differential harness (`cref_*`/L1 `ircd-testkit`/`ircd-golden` L2 can no longer be built once the oracle is gone), and **migrate the load‑bearing tests into `ircd-common`** as self‑contained Rust tests (own fixtures, no `cref_` oracle). **Progress (P8a–P8r — full entries in the progress logs):** **every variadic sender trampoline is now Rust** (P8a–P8m, incl. the keystones `sendto_one`/`sendto_flag`/`esendto_*`), and the five data‑global `.o`s are dropped outright (`s_auth.o` iauth globals P8n, `send.o` P8o, `s_bsd.o` `local[]`/`highest_fd`/… P8p, `ircd.o` `me`/`client`/timers/… P8q). **P8r dropped the last C‑logic TU `support.o`** — `dgets`/`make_isupport` ported to Rust + the `ipv6string`/`minus_one` data globals defined as `#[no_mangle]` statics (`snprintf_append` is dead in Rust — WHOX is field‑by‑field — so it died with the `.o`; `irc_sprintf` is deleted not ported, in P11). **Every ircd C TU is now Rust:** the link set holds only the generated `version.o` + the L1 harness's `ctruth.o`. **P8s migrated all 115 L1 `cref_` differentials into self‑contained `ircd-common` `insta` snapshot tests** (drive only the Rust port via `link_anchor()`, no oracle; soundness via the capture chain) — the suite is green + 0 warnings. **P8t ported the last *generated* C TU `version.c` to Rust** (`ircd-common/src/version.rs`): the data globals `generation`/`creation`/`pass_version`/`infotext`/`isupport` are now `#[no_mangle]` statics via the data‑symbol seam — `generation`/`creation` sourced from crate metadata at build time (`CARGO_PKG_VERSION` + an `ircd-common/build.rs` UTC build stamp), `pass_version`/`infotext` copied verbatim. `version.c.SH` generation + the `version.o` compile/archive are gone from `ircd-sys/build.rs`; `libircd_c.a` now holds **only** the L1 harness `ctruth.o`. (`creation` is now genuinely build‑time volatile, so the golden canonicalizer masks RPL_CREATED 003, matching the existing 371 Birth‑Date rule.) **P8u (FINAL) retired the oracle.** Ran the differential suite a last time green — L1 `ircd-testkit` all pass; L2 `ircd-golden` 86 pass + only the documented reference‑C‑garbage `s_serv_stats` flake (where Rust is the *correct* side) — then **dropped all C compilation** from `ircd-sys/build.rs`: the `make` object build, the `cref_*`/`ctruth.o`/`res.o` recompiles, the `libircd_c.a` archive, and the whole‑archive link + `-lz`/`-lm`/`-lcrypt` (none needed by Rust — `pow` resolves from glibc 2.29+). `build.rs` now only runs `configure` + bindgen over the C *headers* (the struct view `ircd-common` still uses until P9–P12) and expands the install‑path Makefile vars. `ircd-testkit` (L1) + `ircd-golden` (L2) are `exclude`d from the workspace (mothballed, git‑recoverable); the `ctruth.c`/`layout.rs` drift‑net is deleted. **The workspace is now 100% Rust — zero C TUs compiled.** **P8v (literal end of C) deleted the C *source tree* itself + retired `ircd-sys`:** froze the generated `bindings.rs` into a committed `ircd-common/src/bindings.rs` (a self‑alias `extern crate self as ircd_sys;` keeps every `ircd_sys::bindings::*` path resolving, zero src churn) + the install‑path consts as literals, then deleted `ircd-sys`, the mothballed `ircd-testkit`/`ircd-golden` oracle crates, the `iauth-rs` C‑iauth differential, and **all** C source (`common/ ircd/ iauth/ support/ contrib/ cbuild/ configure .clang-format* clangformat.yml`). `ircd-rs` calls `ircd_common::ircd::c_ircd_main` directly. **Not one line of C remains in the repo.** | **MET. Final differential run green; oracle mothballed; `cargo build --workspace` 0 warnings links pure Rust; `cargo test --workspace` 695 pass / 0 fail (P8v; was 698 before deleting the C‑oracle tests); the `ircd-common` snapshot tests carry the reference‑C correctness forward. C source tree deleted — repo is 100% Rust at the file level.** | 91 91 | **P9 — std‑library cleanup ❌ RETIRED (2026‑06‑08)** | — (was: whole Rust tree) | **Skipped by decision — superseded by the `leveva` greenfield.** P9 assumed an *in‑place* idiomatic transformation of the mechanical port (`MyMalloc`/`MyFree`→`Box`/`Vec`, `dbuf` freelist→owned, raw `libc`→`std`/`nix`, `[c_char;N]`→`[u8;N]`). Because the idiomatic end‑state is now a separate greenfield crate (`leveva`), `ircd-common` is throwaway code (the oracle, deleted at parity), so polishing it earns nothing. Nothing was implemented; the row is kept for provenance. | n/a — retired. | 92 92 | **P10 — `leveva` greenfield foundations 🔶 IN PROGRESS** ([`docs/progress-log/p10.md`](docs/progress-log/p10.md)) | new `leveva` crate (lib) + `leveva-integration` differential tests | The idiomatic‑Rust product, built from scratch (not transformed from the port). **Done so far:** RFC 1459 case‑folding (`casemap`); typed identifier strings (`IrcStr`/`IrcString` + `Nick`/`ChanName`/`Uid`/`Sid`/`Cid`/`ServerName`/`UserName`/`HostName` validating newtypes via `ident_newtype!`); `Message`/`MessageBuilder`; the full `Numeric` reply/error enum (184 codes, discriminant = wire code); a generic safe `patricia` trie; glob `matching`; `mode`; KDL `config` (model/parse/password/privilege); `rustls`/`tokio` `tls` endpoint with hot reload; `ident`. The async boot path (`main.rs`) binds listeners + watches certs but currently drops accepted connections — no protocol layer yet. Idiomatic Rust allowed everywhere; pulls real crates (tokio/rustls/kdl/nom/clap). | `cargo test -p leveva` green + clippy clean per module; `leveva-integration` pins each greenfield reimplementation against `ircd-common` (the oracle) within their shared domain (documented divergences asserted explicitly). | 93 - | **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 265 slices.** The slices cover the full client command surface (registration, channels + channel/user modes, WHO/WHOIS/WHOX, OPER/STATS), the UID-based S2S link/burst/netsplit protocol (keepalive, live introduction, NJOIN chunking, TS merge arbitration), `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident), config live-rehash across every block, a channel-mode rework, CertFP, the **IRCv3 track** (message-tags/server-time/msgid/account/bot/oper/echo/labeled-response/CAP, `draft/metadata-2`, caller-id `+g`/`+G`, the **STS/TLS** sub-track), elemental channel modes + extbans, KNOCK, command-rate flood protection (fakelag), the leveva-native channel modes `+C`/`+c`/`+S`/`+j`/`+z`/`+T`/`+g`/`+r` (the `+z` op-moderation track 235–238 — local + network-wide S2S op-redirect for PRIVMSG/NOTICE/TAGMSG/REDACT via the statusmsg `@<chan>` seam — now complete; `+T` no-NOTICE, charybdis `MODE_NONOTICE`, slice 239; `+g` free-invite, charybdis `MODE_FREEINVITE`, slice 240; `+r` registered-only join, charybdis `MODE_REGONLY`, slice 241; `+f` channel forwarding, charybdis `MODE_FORWARD`, slice 242 — a forwardable JOIN reject redirects to the `+f` target with `470`; `+Q`/`+F` forward-target control, charybdis `MODE_DISFORWARD`/`MODE_FREETARGET`, slice 243 — `+F` lifts the op-on-target requirement for setting `+f`, `+Q` makes a channel refuse incoming forwards; `+P` permanent channel, charybdis `MODE_PERMANENT`/`chm_staff`, slice 244 — an oper-only flag that stops an empty channel being destroyed, so its topic/modes persist with zero members; `+L` large ban list, charybdis `MODE_EXLIMIT`/`chm_staff`, slice 245 — an oper-only flag that raises the per-list size cap on `+b`/`+e`/`+I` from 100 to 500, `478 ERR_BANLISTFULL` when a normal list is full — **the last missing standard charybdis channel mode**; the `max-bans`/`max-bans-large` caps `+L` toggles between are promoted to REHASH-able `options { max-bans / max-bans-large }` knobs, slice 246 — the const→knob trajectory `MAX_PENALTY` took); the user mode `+D` deaf, charybdis `UMODE_DEAF`, slice 247 — a client-settable, purely-local umode whose holder receives no channel-addressed messages (the channel fan-out filter runs on the deaf user's home server, so it is not in `SEND_UMODES`); the user mode `+R` registered-only messages, charybdis `UMODE_REGONLYMSG`, slice 248 — a client-settable, purely-local umode whose holder refuses a **private** `PRIVMSG`/`NOTICE` from an unidentified sender (`486 ERR_NONONREG`), bypassed by an identified/operator/accepted sender; the local-sender plane (slice 248); the `+R` remote-sender S2S plane, slice 249 — the inbound `relay::inbound_message` re-runs the same `callerid::regonly_msg_blocks` gate on the target's home server for a *remote* sender (neither `+R` nor `+g`/`+G` is in `SEND_UMODES`), bouncing one `486 ERR_NONONREG` to the sender's uplink for a blocked PRIVMSG (no notify pair, unlike caller-id) and silently dropping a blocked NOTICE — the caller-id 216→217 split repeated, closing the `+R` family; the user mode `+Q` no-forward, charybdis `UMODE_NOFORWARD`, slice 250 — the *user*-side counterpart of the channel `+f` forwarding feature: a client-settable, purely-local umode whose holder is never redirected by a `+f` channel (a forwardable JOIN reject falls through to the source's original numeric instead of `470` + redirect; the gate runs on the joiner's home server, so not in `SEND_UMODES`; the disjoint channel `+Q` `MODE_DISFORWARD` reuses the same letter), closing the `+f` family; the user mode `+G` soft caller-id MODE echo fix, slice 251 — `command::mode::umode_diff` omitted `SoftCallerId`, so a bare `±G` set the bit but echoed an empty trailing (local-echo repair to the existing slice-218 mode; not in `SEND_UMODES`, no snapshot/004 change); the user mode `+z` operwall + the `OPERWALL` command, charybdis `UMODE_OPERWALL`, slice 252 — the oper-only sibling of `+w`/WALLOPS: an operator broadcasts to every `+z` operator network-wide (`OPERWALL` command propagates, each server fans to its own local `+z`), `+z` is oper-gated to set like `+s` and purely-local (not in `SEND_UMODES`), a near-exact mirror of the slice-178 WALLOPS machinery (`command/operwall.rs` + `s2s/operwall.rs`); the user mode `+S` service, charybdis `UMODE_SERVICE`, slice 253 — a **server-set-only** umode (settable only on an S2S burst, a client/oper `MODE +S` silently ignored like `+Z`) whose holder is **immune to an operator's `KILL`** (`481` "Cannot kill a network service"; an inbound *server* KILL is still honoured, matching charybdis's `MyClient`-gated `no_kill_services` hook); unlike the recent local-only umodes it **propagates** (`SEND_UMODES`) since a service is a service network-wide; the user mode `+l` locops + the `LOCOPS` command, charybdis `UMODE_LOCOPS`, slice 254 — the local-server sibling of `+z`/OPERWALL (an oper broadcasts to `+l` users on the originating server only; the command never crosses a link, the point of "LOCal OPerator Wall"), the last named standard charybdis umode candidate, shipped alongside a product decision that **leveva has no local-only user modes**: `SEND_UMODES` became *every* umode (so `+O`/`+x`/`+s`/`+g`/`+G`/`+D`/`+R`/`+Q`/`+z`/`+l` all propagate network-wide and a remote `WHOIS` shows them), while behavioural enforcement (caller-id/`+R` accept-list gating) stays on the target's home server; the WHOIS service display, slice 255 — a `+S` target reads `313` "is a Network Service" (precedence over the operator string) and its channels are hidden (no `319`), closing the `+S` track; the `options { service-string }` REHASH knob, slice 256 — promotes the slice-255 hard-coded WHOIS `313` service text to a configurable charybdis-`servicestring` knob via the `ConfStore` live-or-default seam (the const→knob trajectory `MAX_PENALTY` 208→210 and `max-bans` 245→246 took), closing the last slice-255 follow-on; the `+s` server-notice **snomask**, charybdis `snomask`, slice 257 — promotes the slice-184 all-or-nothing `+s` umode to a charybdis category mask: `+s` becomes the one parameterized umode (a following token is its snomask delta, a bare `+s` seeds every category for slice-184 back-compat), the per-record mask routes each notice by category (`c` connect/exit, `k` kill, `s` general/rehash/SA*), and `008 RPL_SNOMASK` reports the mask (an empty mask drops `+s`); the `n` NCHANGE snomask producer, charybdis `SNO_NCHANGE`, slice 258 — the first producer wired for a slice-257 reserved-but-empty category: a local nick change (via the shared `apply_local_nick_change` chokepoint, so both `NICK` and `SANICK`) fans a `Nick change: From <old> to <new> [<user>@<host>]` spy notice to local `+s +n` watchers, local-only like every snomask producer; the `x` EXTERNAL snomask producer, charybdis `SNO_EXTERNAL`, slice 259 — the second producer wired for a slice-257 reserved-but-empty category: a remote server introduced behind a linked peer (the `s2s::server::handle_server` success path) fans a `Server <name> being introduced by <peer>` spy notice to local `+s +x` watchers (`by` = the direct peer, charybdis `client_p->name`), local-only like every snomask producer; the `y` SPY snomask producer, charybdis `SNO_SPY`, slice 260 — the third producer wired for a slice-257 reserved-but-empty category: a client requesting `STATS` from this server (the `command::stats::stats` chokepoint) fans a `STATS <c> requested by <nick> (<user>@<host>) [<server>]` spy notice to local `+s +y` watchers, fired for any selector char (even unrecognized), skipped for a bare `STATS` or an unresolvable caller, local-only like every snomask producer; the `f` FULL snomask producer, charybdis `SNO_FULL`, slice 261 — the fourth producer wired for a slice-257 reserved-but-empty category: a registering client refused for a full connection *cap* (the two `session.rs` finalize denials — local class `try_admit` and global per-host `check_global_admission`) fans a `Too many connections for <nick>!<user>@<host> (<reason>)` spy notice to local `+s +f` watchers (charybdis varies the verb per cap, leveva carries the exact `CapDenial::reason()`), local-only like every snomask producer; I-line/config-ban (`465`) refusals are deliberately *not* FULL (those are the future REJ/UNAUTH categories); the `r` REJ snomask producer, charybdis `SNO_REJ`, slice 262 — the fifth producer wired for a slice-257 reserved-but-empty category: a client connection *actively rejected by a ban or blocklist* at registration (the three `session.rs` finalize active-ban refusals — dnsbl auth-deny, runtime K-line, static config-`ban`) fans a `Rejecting client <nick>!<user>@<host> (<reason>)` spy notice to local `+s +r` watchers, local-only like every snomask producer; the dnsbl-deny site reports the raw `USER` param (it runs before username resolution), the K-line/config-ban sites the resolved user; the authorization-*failure* refusals (no-I-line `465` / bad-password `464` / ident-required) are deliberately *not* REJ (the future UNAUTH `u` category); the `u` UNAUTH snomask producer, charybdis `SNO_UNAUTH`, slice 263 — the sixth producer wired for a slice-257 reserved-but-empty category: a registering client refused because *no credential authorises it* (the three `session.rs` authorisation-*failure* refusals — bad connection password `464`, no matching I-line `465`, missing required ident) fans an `Unauthorised connection from <nick>!<user>@<host> (<reason>)` spy notice to local `+s +u` watchers, local-only like every snomask producer; distinct from REJ (an active ban/blocklist) — these are "no credentials" failures, reported with the resolved user since all three run after username resolution; closes the slice-262 follow-on, leaving only `C`/`d`/`b` without a producer; the `C` CCONNEXT snomask producer, charybdis `SNO_CCONNEXT`, slice 264 — the seventh producer wired for a slice-257 reserved-but-empty category: the machine-parseable sibling of the `c` CCONN connect report, a successful registration fans a positional `CLICONN <nick> <user> <host> <ip> <class> :<realname>` extended line to local `+s +C` watchers at the **same** registration chokepoint that fires the human-readable `Client connecting:` (`snotice::cliconn_notice`, `class` = matched I-line class name or `"0"`), local-only like every snomask producer, leaving only `d` DEBUG and `b` BOTS without a producer (genuine future/feature work); the `b` BOTS snomask producer, charybdis `SNO_BOTS`, slice 265 — the eighth producer wired for a slice-257 reserved-but-empty category: a **registered** client disconnected for **Excess Flood** (the command-rate fakelag kill, slices 201–208) fans a `Flooder <nick>!<user>@<host> (Excess Flood)` spy notice to local `+s +b` watchers at the registered flood-kill chokepoint (`snotice::flood_notice`), local-only like every snomask producer; the pre-registration flood guard (slice 203) deliberately does **not** fire it (no nick/identity, no charybdis analogue), and charybdis's BOTS is mainly message-target flood while leveva fans on the live command-rate kill it has — faithful intent, not a byte copy; leaving only `d` DEBUG without a producer (genuine future/feature work); a `HELP SNOMASK` reference page cataloguing all snomask categories (with `HELP USER_MODES` now alluding to it); and the native cross-daemon command-skeleton differential harness. **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** | Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. | 93 + | **P11 — `leveva` protocol layer → feature parity 🔶 IN PROGRESS** ([`docs/progress-log/p11.md`](docs/progress-log/p11.md)) | `leveva` (client/server state machine + handlers) | The bulk of the remaining work: the connection + registration state machine, the post‑registration command handlers (JOIN/PRIVMSG/MODE/KICK/TOPIC/WHO/WHOIS/…), channels + channel/user modes, the S2S link/burst protocol (UID‑based: UNICK/NJOIN/SAVE/EOB), and `leveva-iauth` (native async auth — dnsbl/socks/webproxy/pipe/ident). Each slice is built idiomatically (owned per‑connection state, typed identifiers end‑to‑end, `Result`/`thiserror`, `format!`/typed builders — **no** `sprintf`/global `buf`), unit‑tested + boot‑golden + proptest‑fuzzed, and **differentially pinned against `ircd-common`** (or `iauth-rs` for auth) wherever a pure oracle entry point exists — structural skeleton + documented divergences, since leveva emits a clean modern burst rather than a byte copy. **Done: 266 slices.** The slices cover the full client command surface (registration, channels + channel/user modes, WHO/WHOIS/WHOX, OPER/STATS), the UID-based S2S link/burst/netsplit protocol (keepalive, live introduction, NJOIN chunking, TS merge arbitration), `leveva-iauth` (dnsbl/socks/webproxy/pipe/ident), config live-rehash across every block, a channel-mode rework, CertFP, the **IRCv3 track** (message-tags/server-time/msgid/account/bot/oper/echo/labeled-response/CAP, `draft/metadata-2`, caller-id `+g`/`+G`, the **STS/TLS** sub-track), elemental channel modes + extbans, KNOCK, command-rate flood protection (fakelag), the leveva-native channel modes `+C`/`+c`/`+S`/`+j`/`+z`/`+T`/`+g`/`+r` (the `+z` op-moderation track 235–238 — local + network-wide S2S op-redirect for PRIVMSG/NOTICE/TAGMSG/REDACT via the statusmsg `@<chan>` seam — now complete; `+T` no-NOTICE, charybdis `MODE_NONOTICE`, slice 239; `+g` free-invite, charybdis `MODE_FREEINVITE`, slice 240; `+r` registered-only join, charybdis `MODE_REGONLY`, slice 241; `+f` channel forwarding, charybdis `MODE_FORWARD`, slice 242 — a forwardable JOIN reject redirects to the `+f` target with `470`; `+Q`/`+F` forward-target control, charybdis `MODE_DISFORWARD`/`MODE_FREETARGET`, slice 243 — `+F` lifts the op-on-target requirement for setting `+f`, `+Q` makes a channel refuse incoming forwards; `+P` permanent channel, charybdis `MODE_PERMANENT`/`chm_staff`, slice 244 — an oper-only flag that stops an empty channel being destroyed, so its topic/modes persist with zero members; `+L` large ban list, charybdis `MODE_EXLIMIT`/`chm_staff`, slice 245 — an oper-only flag that raises the per-list size cap on `+b`/`+e`/`+I` from 100 to 500, `478 ERR_BANLISTFULL` when a normal list is full — **the last missing standard charybdis channel mode**; the `max-bans`/`max-bans-large` caps `+L` toggles between are promoted to REHASH-able `options { max-bans / max-bans-large }` knobs, slice 246 — the const→knob trajectory `MAX_PENALTY` took); the user mode `+D` deaf, charybdis `UMODE_DEAF`, slice 247 — a client-settable, purely-local umode whose holder receives no channel-addressed messages (the channel fan-out filter runs on the deaf user's home server, so it is not in `SEND_UMODES`); the user mode `+R` registered-only messages, charybdis `UMODE_REGONLYMSG`, slice 248 — a client-settable, purely-local umode whose holder refuses a **private** `PRIVMSG`/`NOTICE` from an unidentified sender (`486 ERR_NONONREG`), bypassed by an identified/operator/accepted sender; the local-sender plane (slice 248); the `+R` remote-sender S2S plane, slice 249 — the inbound `relay::inbound_message` re-runs the same `callerid::regonly_msg_blocks` gate on the target's home server for a *remote* sender (neither `+R` nor `+g`/`+G` is in `SEND_UMODES`), bouncing one `486 ERR_NONONREG` to the sender's uplink for a blocked PRIVMSG (no notify pair, unlike caller-id) and silently dropping a blocked NOTICE — the caller-id 216→217 split repeated, closing the `+R` family; the user mode `+Q` no-forward, charybdis `UMODE_NOFORWARD`, slice 250 — the *user*-side counterpart of the channel `+f` forwarding feature: a client-settable, purely-local umode whose holder is never redirected by a `+f` channel (a forwardable JOIN reject falls through to the source's original numeric instead of `470` + redirect; the gate runs on the joiner's home server, so not in `SEND_UMODES`; the disjoint channel `+Q` `MODE_DISFORWARD` reuses the same letter), closing the `+f` family; the user mode `+G` soft caller-id MODE echo fix, slice 251 — `command::mode::umode_diff` omitted `SoftCallerId`, so a bare `±G` set the bit but echoed an empty trailing (local-echo repair to the existing slice-218 mode; not in `SEND_UMODES`, no snapshot/004 change); the user mode `+z` operwall + the `OPERWALL` command, charybdis `UMODE_OPERWALL`, slice 252 — the oper-only sibling of `+w`/WALLOPS: an operator broadcasts to every `+z` operator network-wide (`OPERWALL` command propagates, each server fans to its own local `+z`), `+z` is oper-gated to set like `+s` and purely-local (not in `SEND_UMODES`), a near-exact mirror of the slice-178 WALLOPS machinery (`command/operwall.rs` + `s2s/operwall.rs`); the user mode `+S` service, charybdis `UMODE_SERVICE`, slice 253 — a **server-set-only** umode (settable only on an S2S burst, a client/oper `MODE +S` silently ignored like `+Z`) whose holder is **immune to an operator's `KILL`** (`481` "Cannot kill a network service"; an inbound *server* KILL is still honoured, matching charybdis's `MyClient`-gated `no_kill_services` hook); unlike the recent local-only umodes it **propagates** (`SEND_UMODES`) since a service is a service network-wide; the user mode `+l` locops + the `LOCOPS` command, charybdis `UMODE_LOCOPS`, slice 254 — the local-server sibling of `+z`/OPERWALL (an oper broadcasts to `+l` users on the originating server only; the command never crosses a link, the point of "LOCal OPerator Wall"), the last named standard charybdis umode candidate, shipped alongside a product decision that **leveva has no local-only user modes**: `SEND_UMODES` became *every* umode (so `+O`/`+x`/`+s`/`+g`/`+G`/`+D`/`+R`/`+Q`/`+z`/`+l` all propagate network-wide and a remote `WHOIS` shows them), while behavioural enforcement (caller-id/`+R` accept-list gating) stays on the target's home server; the WHOIS service display, slice 255 — a `+S` target reads `313` "is a Network Service" (precedence over the operator string) and its channels are hidden (no `319`), closing the `+S` track; the `options { service-string }` REHASH knob, slice 256 — promotes the slice-255 hard-coded WHOIS `313` service text to a configurable charybdis-`servicestring` knob via the `ConfStore` live-or-default seam (the const→knob trajectory `MAX_PENALTY` 208→210 and `max-bans` 245→246 took), closing the last slice-255 follow-on; the `+s` server-notice **snomask**, charybdis `snomask`, slice 257 — promotes the slice-184 all-or-nothing `+s` umode to a charybdis category mask: `+s` becomes the one parameterized umode (a following token is its snomask delta, a bare `+s` seeds every category for slice-184 back-compat), the per-record mask routes each notice by category (`c` connect/exit, `k` kill, `s` general/rehash/SA*), and `008 RPL_SNOMASK` reports the mask (an empty mask drops `+s`); the `n` NCHANGE snomask producer, charybdis `SNO_NCHANGE`, slice 258 — the first producer wired for a slice-257 reserved-but-empty category: a local nick change (via the shared `apply_local_nick_change` chokepoint, so both `NICK` and `SANICK`) fans a `Nick change: From <old> to <new> [<user>@<host>]` spy notice to local `+s +n` watchers, local-only like every snomask producer; the `x` EXTERNAL snomask producer, charybdis `SNO_EXTERNAL`, slice 259 — the second producer wired for a slice-257 reserved-but-empty category: a remote server introduced behind a linked peer (the `s2s::server::handle_server` success path) fans a `Server <name> being introduced by <peer>` spy notice to local `+s +x` watchers (`by` = the direct peer, charybdis `client_p->name`), local-only like every snomask producer; the `y` SPY snomask producer, charybdis `SNO_SPY`, slice 260 — the third producer wired for a slice-257 reserved-but-empty category: a client requesting `STATS` from this server (the `command::stats::stats` chokepoint) fans a `STATS <c> requested by <nick> (<user>@<host>) [<server>]` spy notice to local `+s +y` watchers, fired for any selector char (even unrecognized), skipped for a bare `STATS` or an unresolvable caller, local-only like every snomask producer; the `f` FULL snomask producer, charybdis `SNO_FULL`, slice 261 — the fourth producer wired for a slice-257 reserved-but-empty category: a registering client refused for a full connection *cap* (the two `session.rs` finalize denials — local class `try_admit` and global per-host `check_global_admission`) fans a `Too many connections for <nick>!<user>@<host> (<reason>)` spy notice to local `+s +f` watchers (charybdis varies the verb per cap, leveva carries the exact `CapDenial::reason()`), local-only like every snomask producer; I-line/config-ban (`465`) refusals are deliberately *not* FULL (those are the future REJ/UNAUTH categories); the `r` REJ snomask producer, charybdis `SNO_REJ`, slice 262 — the fifth producer wired for a slice-257 reserved-but-empty category: a client connection *actively rejected by a ban or blocklist* at registration (the three `session.rs` finalize active-ban refusals — dnsbl auth-deny, runtime K-line, static config-`ban`) fans a `Rejecting client <nick>!<user>@<host> (<reason>)` spy notice to local `+s +r` watchers, local-only like every snomask producer; the dnsbl-deny site reports the raw `USER` param (it runs before username resolution), the K-line/config-ban sites the resolved user; the authorization-*failure* refusals (no-I-line `465` / bad-password `464` / ident-required) are deliberately *not* REJ (the future UNAUTH `u` category); the `u` UNAUTH snomask producer, charybdis `SNO_UNAUTH`, slice 263 — the sixth producer wired for a slice-257 reserved-but-empty category: a registering client refused because *no credential authorises it* (the three `session.rs` authorisation-*failure* refusals — bad connection password `464`, no matching I-line `465`, missing required ident) fans an `Unauthorised connection from <nick>!<user>@<host> (<reason>)` spy notice to local `+s +u` watchers, local-only like every snomask producer; distinct from REJ (an active ban/blocklist) — these are "no credentials" failures, reported with the resolved user since all three run after username resolution; closes the slice-262 follow-on, leaving only `C`/`d`/`b` without a producer; the `C` CCONNEXT snomask producer, charybdis `SNO_CCONNEXT`, slice 264 — the seventh producer wired for a slice-257 reserved-but-empty category: the machine-parseable sibling of the `c` CCONN connect report, a successful registration fans a positional `CLICONN <nick> <user> <host> <ip> <class> :<realname>` extended line to local `+s +C` watchers at the **same** registration chokepoint that fires the human-readable `Client connecting:` (`snotice::cliconn_notice`, `class` = matched I-line class name or `"0"`), local-only like every snomask producer, leaving only `d` DEBUG and `b` BOTS without a producer (genuine future/feature work); the `b` BOTS snomask producer, charybdis `SNO_BOTS`, slice 265 — the eighth producer wired for a slice-257 reserved-but-empty category: a **registered** client disconnected for **Excess Flood** (the command-rate fakelag kill, slices 201–208) fans a `Flooder <nick>!<user>@<host> (Excess Flood)` spy notice to local `+s +b` watchers at the registered flood-kill chokepoint (`snotice::flood_notice`), local-only like every snomask producer; the pre-registration flood guard (slice 203) deliberately does **not** fire it (no nick/identity, no charybdis analogue), and charybdis's BOTS is mainly message-target flood while leveva fans on the live command-rate kill it has — faithful intent, not a byte copy; leaving only `d` DEBUG without a producer (genuine future/feature work); a `HELP SNOMASK` reference page cataloguing all snomask categories (with `HELP USER_MODES` now alluding to it); the `options { default-snomask }` REHASH knob, slice 266 — promotes the hard-coded bare-`+s` default snomask (`SnoMask::ALL`) to a configurable charybdis-`default_snomask` knob via the `ConfStore` live-or-default seam (the const→knob trajectory `MAX_PENALTY` 208→210, `max-bans` 245→246 and `service-string` 256 took; strict config parse `snomask::parse_config` rejects a typo, unlike the lenient runtime MODE path), closing the deferral named verbatim by slice 257; and the native cross-daemon command-skeleton differential harness. **Full per-slice detail (scope, mechanism, divergences, gate) for every slice lives in [`docs/progress-log/p11.md`](docs/progress-log/p11.md); the standing testing plan is [`docs/superpowers/specs/2026-06-10-leveva-testing-plan.md`](docs/superpowers/specs/2026-06-10-leveva-testing-plan.md). This row stays thin.** | Per‑slice: leveva boots and serves the slice; leveva golden snapshot + proptest green; the `leveva-integration` differential agrees with `ircd-common` on the shared skeleton with divergences asserted. Across the eventual matrix: registration, channels, WHO/WHOIS/WHOX, OPER/STATS, server burst, netsplit, rehash, restart; iauth end‑to‑end. | 94 94 | **P12 — Retire the mechanical port; `leveva` is the product ✅ DONE (2026‑06‑13)** ([`docs/progress-log/p12.md`](docs/progress-log/p12.md)) | deleted `ircd-common`/`ircd-rs` + C‑era `iauth-rs` + `leveva-integration` + the mechanical port's deployment rigging; workspace = `leveva` + `leveva-iauth` | Ran the differential suite a **final time green** (`cargo test -p leveva-integration`, skeleton‑identical to the oracle), then **deleted the mechanical port** (`ircd-common`, `ircd-rs`), its C‑era auth scaffolding (`iauth-rs`, long since subsumed by the native `leveva-iauth`), and the oracle differential crate (`leveva-integration`). The oracle tests retired with their oracle — the load‑bearing behavior was already carried by leveva's self‑contained 212‑file golden + proptest suite (verified: leveva/leveva-iauth have zero Cargo/`use` dep on the deleted crates). Also stripped the mechanical port's **deployment rigging** (`docker/Dockerfile.ircd`, the `ircd` bake target, the entire `docs/k8s/` example network) and updated the README to describe the finished strangler. This is the literal end of the strangler — same lifecycle the C tree had at P8. | **MET. Final differential run green; `ircd-common`/`ircd-rs`/`iauth-rs`/`leveva-integration` deleted; `cargo build --workspace` 0 warnings; `cargo clippy --workspace --tests` clean; `cargo test -p leveva -p leveva-iauth` green — the behavioral guarantees carry forward. The workspace is now `leveva` + `leveva-iauth`.** | 95 95 96 96 ---
+59
docs/progress-log/p11.md
··· 14690 14690 clean; `cargo build --workspace` 0 warnings; no welcome-burst / `004` / `221` / STATS-reply 14691 14691 snapshot churn (the fan is a side-effect on watcher mailboxes on the flood-kill path; touches no 14692 14692 numerics). 14693 + 14694 + ## 2026-06-18 — P11 slice 266: `options { default-snomask }` REHASH knob 14695 + 14696 + **What:** promoted the **bare-`+s` default snomask** from a hard-coded `SnoMask::ALL` to a 14697 + REHASH-able `options { default-snomask }` config knob (charybdis `default_snomask`). When an 14698 + operator sets a bare `MODE <nick> +s` (no following snomask token), the seeded categories now come 14699 + from config instead of "every category". This closes the deferral named **verbatim** in slice 14700 + 257's divergence list — *"No config knob for the default mask (a future slice could add one, the 14701 + `MAX_PENALTY` 208→210 trajectory)"* — and is the same const→knob trajectory `MAX_PENALTY` 14702 + (208→210), `max-bans`/`max-bans-large` (245→246) and `service-string` (256) took: a new `options` 14703 + field, the `ConfStore` live-or-default seam, and a boot + REHASH re-seed. 14704 + 14705 + **Mechanism:** 14706 + - `snomask.rs`: new strict parser `parse_config(s) -> Result<u32, char>` — walks like `apply_delta` 14707 + (start mask `0`, default sign `+`, `+`/`-` toggle) but `Err(letter)` on an unknown non-sign 14708 + char. Strict (a config typo is surfaced) unlike the lenient runtime `apply_delta` (charybdis 14709 + ignores unknown snomask chars at the live MODE path). Pure + total + panic-free — the fuzz seam. 14710 + - `config/model.rs`: `Options.default_snomask: u32`; `Default` = `SnoMask::ALL`. 14711 + - `config/parse.rs`: `default_snomask(block)` — absent → `SnoMask::ALL`; present → `parse_config` 14712 + mapped to a `ConfigError` naming the bad letter. (`opt_string` treats `""` as absent → `""` 14713 + falls back to ALL; a sign-only `"+"` is a *legal empty* mask, distinct from absent.) 14714 + - `control.rs`: `ConfBlocks.default_snomask: u32` + `config_conf` mapping; `ConfStore` 14715 + `default_snomask: ArcSwapOption<u32>` (the exact analogue of `default_user_modes`) + seed + 14716 + accessor; free fn `live_default_snomask() -> Option<u32>`. 14717 + - `main.rs`: `default_snomask: config.options.default_snomask` in the boot `ConfBlocks`. 14718 + - `command/mode.rs`: the bare-`+s` seed reads `control::live_default_snomask().unwrap_or(ALL)` — 14719 + unseeded (unit tests) falls back to ALL, the slice-184/257 behaviour. 14720 + - `examples/ircd.kdl` + `dist/ircd.kdl`: a documented `default-snomask "+cCksnruxyfb"` line (every 14721 + category with a producer — `d` DEBUG omitted, no producer yet). 14722 + 14723 + ### Divergences (documented) 14724 + - Out-of-box default is `ALL` (slice-184/257 back-compat); charybdis's compiled default is a 14725 + smaller set. A defaults-only divergence — the routing/parse is faithful. 14726 + - Strict config parse vs lenient runtime `apply_delta` (deliberate). 14727 + - Local-only like every snomask facet (the mask is never propagated; see `snomask.rs` locality). 14728 + 14729 + ### Tests (RED first) 14730 + - `snomask.rs` units: `parse_config` happy (`"+ck"`/`"ck"`/`"cks"`), default-sign, empty/sign-only 14731 + → 0, the `-` inverse undoes a `+`, `Err(letter)` on `"cZk"`/`"9"`, and the strict-vs-lenient 14732 + contrast (`apply_delta` ignores `Z`, `parse_config` rejects it). 14733 + - `config/tests.rs` `default_snomask_default_override_empty_and_invalid`: absent → ALL; field 14734 + absent → ALL; `"+ck"` → c|k; `""` → ALL (opt_string-absent); `"+"` → 0 (legal empty); a typo'd 14735 + letter → a config error. 14736 + - `control.rs` units: `config_conf` extracts `+ck`; the fresh store seeds + reads it back; 14737 + unseeded → `None`; the re-seed inverse (the stale `+ck` of SAMPLE_CONF2 clears to `0`). 14738 + - Golden `golden_default_snomask_config.rs` (+ fixture `default_snomask.kdl`, 14739 + `default-snomask "+ck"`): an oper sets bare `MODE root +s` → `008 root +ck` (the configured 14740 + mask, **not** the all-categories `+cCksnruxydfb`) — end-to-end config → live → enforcement, in 14741 + a subprocess so the global `LIVE_CONF` singleton isn't raced by the unseeded→ALL unit test. 14742 + - Fuzz `tests/default_snomask_proptest.rs`: over arbitrary strings `parse_config` never panics; 14743 + over sign/known-letter strings it parses `Ok` with no bit outside `ALL` and round-trips through 14744 + `render`; an unknown char anywhere → `Err(that char)`, with `apply_delta` ignoring it (the 14745 + leniency contrast). 14746 + 14747 + **Plan:** [`docs/superpowers/plans/2026-06-18-p11-slice266-default-snomask-config.md`](../superpowers/plans/2026-06-18-p11-slice266-default-snomask-config.md). 14748 + 14749 + **Gate:** `cargo test -p leveva` green (lib + the new golden + proptest); `cargo clippy -p leveva 14750 + --tests` clean; `cargo build --workspace` 0 warnings; no `004`/`221`/welcome-burst snapshot churn 14751 + (the knob only changes the *seed* of a bare `+s`, surfaced via `008`, not the umode-is reply).
+85
docs/superpowers/plans/2026-06-18-p11-slice266-default-snomask-config.md
··· 1 + # P11 slice 266 — `options { default-snomask }` REHASH knob 2 + 3 + ## Goal 4 + 5 + Promote the **bare-`+s` default snomask** from a hard-coded `SnoMask::ALL` to a 6 + REHASH-able `options { default-snomask }` config knob (charybdis `general::default_operstring` 7 + sibling — its `default_snomask`/`snomask_modes` default). When an operator sets a bare 8 + `MODE <nick> +s` (no following mask token), the seeded categories come from config instead of 9 + "every category". 10 + 11 + This closes the deferral named **verbatim** in slice 257's divergence list: 12 + 13 + > The default (bare-`+s`) snomask is `ALL` … charybdis's default is server-configured. … No config 14 + > knob for the default mask (a future slice could add one, the `MAX_PENALTY` 208→210 trajectory). 15 + 16 + This is the same const→knob trajectory `MAX_PENALTY` (208→210), `max-bans`/`max-bans-large` 17 + (245→246) and `service-string` (256) took: a new `options` field, the `ConfStore` live-or-default 18 + seam, and a boot + REHASH re-seed. 19 + 20 + ## Locked decisions 21 + 22 + - **Out-of-box default stays `SnoMask::ALL`** — absent `default-snomask` preserves slice-184/257 23 + behaviour (a bare `+s` subscribes to every category). The live store unseeded (unit tests) also 24 + falls back to `ALL`, so no existing snomask test churns. 25 + - **Config parse is strict** (errors on an unknown letter), matching the sibling 26 + `default-user-modes`/`default-channel-modes` knobs which reject typos — *not* the runtime 27 + `apply_delta` leniency (charybdis ignores unknown snomask chars at the live MODE path, but a 28 + config typo should be surfaced at boot, not silently dropped). New strict parser 29 + `snomask::parse_config(s) -> Result<u32, char>` (the `Err` carries the offending letter). 30 + - **An empty mask is legal** (`default-snomask ""` or `"+"`) → a bare `+s` then seeds an empty 31 + mask, which (per the existing slice-257 rule) immediately drops the `+s` bit again. The admin's 32 + footgun, parsed without error, exactly like `max-bans 0`. 33 + - **Storage type `u32`**, stored live as `ArcSwapOption<u32>` — the exact analogue of 34 + `default_user_modes`/`default_channel_modes`. 35 + 36 + ## Design 37 + 38 + The plumbing chain (mirror `default_user_modes`, `service_string`): 39 + 40 + 1. **`snomask.rs`** — `parse_config(s) -> Result<u32, char>`: walks like `apply_delta` (start mask 41 + `0`, default sign `+`, `+`/`-` toggle) but `Err(letter)` on an unknown non-sign char. Pure, 42 + total, panic-free — the fuzz seam. Unit tests + the inverse (strip via `-`). 43 + 2. **`config/model.rs`** — `Options.default_snomask: u32`; `Default` = `SnoMask::ALL`. 44 + 3. **`config/parse.rs`** — `default_snomask(block)`: absent → `SnoMask::ALL`; present → `parse_config` 45 + mapped to a `ConfigError` naming the bad letter. Wire into the `Options` build. 46 + 4. **`control.rs`** — `ConfBlocks.default_snomask: u32`; `config_conf` maps it; `ConfStore` 47 + `default_snomask: ArcSwapOption<u32>` (`const_empty` init, `seed` stores, accessor); free fn 48 + `live_default_snomask() -> Option<u32>`. 49 + 5. **`main.rs`** — add `default_snomask: config.options.default_snomask` to the boot `ConfBlocks`. 50 + 6. **`command/mode.rs`** — line ~779: `snomask = control::live_default_snomask().unwrap_or(SnoMask::ALL)`. 51 + 7. **Example/dist KDL** — add a commented `default-snomask` line to `examples/ircd.kdl` + 52 + `examples/simple.kdl`'s `options {}` (dist mirrors); the big inline `options` line in the 53 + control.rs rehash test gains `default-snomask "+ck"`. 54 + 55 + ## Tests (RED first) 56 + 57 + - **`snomask.rs` units**: `parse_config` happy (`"+ck"`/`"ck"`/`"cks"` → the right bits), 58 + default-sign, empty → 0, the `-` inverse undoes a `+`, `Err(letter)` on an unknown letter 59 + (`"cZk"` → `Err('Z')`), and the strict-vs-lenient contrast (`apply_delta` ignores `Z`, 60 + `parse_config` rejects it). 61 + - **`config/tests.rs`**: absent → `ALL`; `default-snomask "+ck"` → `cconn()|skill()`; 62 + `default-snomask ""` → 0; an invalid letter → a parse error. 63 + - **`control.rs` units**: `config_conf` extracts it; `seed` stores it; `live_default_snomask` 64 + reads it; unseeded → `None`. 65 + - **`command/mode.rs` unit**: bare `+s` with the live store seeded to `+ck` seeds `cconn|skill` 66 + (not `ALL`); the existing bare-`+s`→ALL test still holds when **unseeded** (fallback). 67 + - **Golden `golden_default_snomask_config.rs`** (+ fixture `default_snomask.kdl`, 68 + `default-snomask "+ck"`): an oper sets bare `MODE oper +s` → `008 oper +ck` (the configured 69 + mask, **not** `+cCksnruxydfb`); end-to-end config → live → enforcement. 70 + - **Fuzz `tests/default_snomask_proptest.rs`**: over arbitrary strings `parse_config` never panics 71 + and returns `Ok`/`Err`; over letter-only strings the result renders back to a subset; a known 72 + letter always lands. 73 + 74 + ## Divergences 75 + 76 + - Out-of-box default is `ALL` (slice-184 back-compat); charybdis's compiled default is a smaller 77 + set. A defaults-only divergence — the routing/parse is faithful. 78 + - Strict config parse vs lenient runtime `apply_delta` (deliberate, above). 79 + - Local-only like every snomask facet (the mask is never propagated; see `snomask.rs` locality). 80 + 81 + ## Gate 82 + 83 + `cargo test -p leveva` green (lib + the new golden + proptest); `cargo clippy -p leveva --tests` 84 + clean; `cargo build --workspace` 0 warnings; no `004`/`221`/welcome-burst snapshot churn (the knob 85 + only changes the *seed* of a bare `+s`, surfaced via `008`, not the umode-is reply).
+4
leveva/dist/ircd.kdl
··· 90 90 // The WHOIS 313 line shown for a network service (a +S user). Absent → "is a 91 91 // Network Service". charybdis servicestring. 92 92 service-string "is a Network Service" 93 + // The category mask a bare `MODE <nick> +s` seeds (the snomask letters, e.g. 94 + // "+ckf"). Absent → every category (the historic bare-+s behaviour). An empty 95 + // mask ("+") subscribes to nothing; a typo'd letter is a config error. 96 + default-snomask "+cCksnruxyfb" 93 97 // Path to the MOTD file (sent as 372/375/376 on registration). The CLI 94 98 // `--motd` flag, when given, overrides this. 95 99 motd "leveva.motd"
+4
leveva/examples/ircd.kdl
··· 90 90 // The WHOIS 313 line shown for a network service (a +S user). Absent → "is a 91 91 // Network Service". charybdis servicestring. 92 92 service-string "is a Network Service" 93 + // The category mask a bare `MODE <nick> +s` seeds (the snomask letters, e.g. 94 + // "+ckf"). Absent → every category (the historic bare-+s behaviour). An empty 95 + // mask ("+") subscribes to nothing; a typo'd letter is a config error. 96 + default-snomask "+cCksnruxyfb" 93 97 // Path to the MOTD file (sent as 372/375/376 on registration). The CLI 94 98 // `--motd` flag, when given, overrides this. 95 99 motd "leveva.motd"
+5 -1
leveva/src/command/mode.rs
··· 776 776 if i + 1 < changes.len() { 777 777 pending = SnoPending::Apply; // next token = snomask delta 778 778 } else { 779 - snomask = crate::snomask::SnoMask::ALL; // bare +s = default 779 + // Bare `+s` seeds the configured default snomask (slice 266, 780 + // `options { default-snomask }`); unseeded (unit tests) falls back 781 + // to ALL — every category, the slice-184/257 behaviour. 782 + snomask = crate::control::live_default_snomask() 783 + .unwrap_or(crate::snomask::SnoMask::ALL); 780 784 snomask_changed = true; 781 785 } 782 786 } else if i + 1 < changes.len() {
+10
leveva/src/config/model.rs
··· 294 294 /// Network Service"), matching charybdis ignoring an empty `servicestring`. Hot-reloaded by 295 295 /// `REHASH`. 296 296 pub service_string: String, 297 + /// The category mask a bare `MODE <nick> +s` (no following snomask token) seeds — charybdis's 298 + /// configured `default_snomask` (P11 slice 266). The config field `default-snomask` is a 299 + /// snomask-letter string (`"+ck"`, `"ck"`); **absent → [`crate::snomask::SnoMask::ALL`]** 300 + /// (every category, preserving the slice-184/257 bare-`+s` behaviour). An unknown letter is a 301 + /// parse error (strict, unlike the lenient runtime MODE path); an empty mask is legal (a bare 302 + /// `+s` then drops the `+s` bit per the slice-257 empty-mask rule). Hot-reloaded by `REHASH`. 303 + pub default_snomask: u32, 297 304 } 298 305 299 306 impl Default for Options { ··· 335 342 // The out-of-box WHOIS service string, matching the `service-string`-absent case 336 343 // (slice 256). 337 344 service_string: crate::command::whois::DEFAULT_SERVICE_STRING.to_string(), 345 + // The out-of-box bare-`+s` default snomask — every category, matching the 346 + // `default-snomask`-absent case in the parser and the slice-184/257 behaviour. 347 + default_snomask: crate::snomask::SnoMask::ALL, 338 348 } 339 349 } 340 350 }
+21
leveva/src/config/parse.rs
··· 263 263 // an empty `servicestring`. 264 264 service_string: opt_string(block, "service-string") 265 265 .unwrap_or_else(|| crate::command::whois::DEFAULT_SERVICE_STRING.to_string()), 266 + // slice 266 — the bare-`+s` default snomask (charybdis `default_snomask`). Absent → ALL 267 + // (every category, the slice-184/257 behaviour). A typo'd letter is rejected (strict, 268 + // unlike the lenient runtime MODE path); an empty mask is legal. 269 + default_snomask: default_snomask(block)?, 270 + }) 271 + } 272 + 273 + /// Parse `default-snomask` (a snomask-letter string like `"+ck"`) into a category mask. Absent → 274 + /// [`crate::snomask::SnoMask::ALL`] (leveva's out-of-box bare-`+s` default, the slice-184/257 275 + /// behaviour). Strict — an unknown letter is a config error (matching the sibling 276 + /// `default-user-modes`/`default-channel-modes` knobs), via [`crate::snomask::parse_config`]. 277 + fn default_snomask(block: &KdlNode) -> Result<u32, ConfigError> { 278 + let Some(s) = opt_string(block, "default-snomask") else { 279 + return Ok(crate::snomask::SnoMask::ALL); 280 + }; 281 + crate::snomask::parse_config(&s).map_err(|letter| { 282 + bad( 283 + block, 284 + "default-snomask", 285 + &format!("unknown snomask category letter {letter:?}"), 286 + ) 266 287 }) 267 288 } 268 289
+39
leveva/src/config/tests.rs
··· 657 657 } 658 658 659 659 #[test] 660 + fn default_snomask_default_override_empty_and_invalid() { 661 + use crate::snomask::SnoMask; 662 + 663 + // No options block → the out-of-box default ALL (slice 266, the slice-184/257 bare-`+s` 664 + // behaviour: every category). 665 + let o = Config::from_kdl(MINIMAL).unwrap().options; 666 + assert_eq!(o.default_snomask, SnoMask::ALL); 667 + 668 + // An options block without the field → still ALL. 669 + let src = format!("{MINIMAL}\noptions {{ connection-accept \"on\" }}"); 670 + let o = Config::from_kdl(&src).unwrap().options; 671 + assert_eq!(o.default_snomask, SnoMask::ALL); 672 + 673 + // An explicit `"+ck"` → the c|k mask (the configured narrow default). 674 + let src = format!("{MINIMAL}\noptions {{ default-snomask \"+ck\" }}"); 675 + let o = Config::from_kdl(&src).unwrap().options; 676 + assert_eq!(o.default_snomask, SnoMask::cconn() | SnoMask::skill()); 677 + 678 + // An empty string `""` falls back to ALL (the shared `opt_string` treats "" as absent) — the 679 + // inverse of an explicit value. 680 + let src = format!("{MINIMAL}\noptions {{ default-snomask \"\" }}"); 681 + let o = Config::from_kdl(&src).unwrap().options; 682 + assert_eq!(o.default_snomask, SnoMask::ALL); 683 + 684 + // A sign-only `"+"` is a *legal empty* mask (a bare `+s` then drops the bit per slice 257) — 685 + // distinct from `""`, which is absent. 686 + let src = format!("{MINIMAL}\noptions {{ default-snomask \"+\" }}"); 687 + let o = Config::from_kdl(&src).unwrap().options; 688 + assert_eq!(o.default_snomask, 0); 689 + 690 + // An unknown letter is a config error (strict, unlike the lenient runtime MODE path). 691 + let src = format!("{MINIMAL}\noptions {{ default-snomask \"cZk\" }}"); 692 + assert!( 693 + Config::from_kdl(&src).is_err(), 694 + "a typo'd snomask letter must be a config error" 695 + ); 696 + } 697 + 698 + #[test] 660 699 fn throttle_knobs_default_override_and_accept_zero() { 661 700 use crate::flood::MAX_PENALTY; 662 701 use crate::knock_throttle::{KNOCK_DELAY_CHANNEL_SECS, KNOCK_DELAY_USER_SECS};
+47 -1
leveva/src/control.rs
··· 253 253 /// The `options { service-string }` WHOIS `313` text for a network service (slice 256, 254 254 /// charybdis `servicestring`). Held live so `REHASH` re-tunes it with no restart. 255 255 pub service_string: String, 256 + /// The `options { default-snomask }` bare-`+s` default category mask (slice 266, charybdis 257 + /// `default_snomask`). Held live so `REHASH` re-tunes which categories a fresh `+s` subscribes 258 + /// to with no restart. 259 + pub default_snomask: u32, 256 260 } 257 261 258 262 /// Parse `src` and extract just the hot-reloadable [`ConfBlocks`] (the pure core ··· 286 290 max_bans: cfg.options.max_bans, 287 291 max_bans_large: cfg.options.max_bans_large, 288 292 service_string: cfg.options.service_string, 293 + default_snomask: cfg.options.default_snomask, 289 294 }) 290 295 } 291 296 ··· 344 349 max_bans_large: ArcSwapOption<usize>, 345 350 /// The live `options { service-string }` WHOIS `313` service text (slice 256). 346 351 service_string: ArcSwapOption<String>, 352 + /// The live `options { default-snomask }` bare-`+s` default category mask (slice 266). 353 + default_snomask: ArcSwapOption<u32>, 347 354 initialized: AtomicBool, 348 355 } 349 356 ··· 377 384 max_bans: ArcSwapOption::const_empty(), 378 385 max_bans_large: ArcSwapOption::const_empty(), 379 386 service_string: ArcSwapOption::const_empty(), 387 + default_snomask: ArcSwapOption::const_empty(), 380 388 initialized: AtomicBool::new(false), 381 389 } 382 390 } ··· 426 434 .store(Some(Arc::new(b.max_bans_large))); 427 435 self.service_string 428 436 .store(Some(Arc::new(b.service_string))); 437 + self.default_snomask 438 + .store(Some(Arc::new(b.default_snomask))); 429 439 self.initialized.store(true, Ordering::SeqCst); 430 440 } 431 441 ··· 587 597 pub fn service_string(&self) -> Option<Arc<String>> { 588 598 self.service_string.load_full() 589 599 } 600 + 601 + /// The live `options { default-snomask }` bare-`+s` default category mask (`None` ⇒ never 602 + /// seeded ⇒ the MODE handler falls back to [`crate::snomask::SnoMask::ALL`]). Slice 266. 603 + pub fn default_snomask(&self) -> Option<u32> { 604 + self.default_snomask.load_full().map(|a| *a) 605 + } 590 606 } 591 607 592 608 /// The process-global live config. ··· 696 712 LIVE_CONF.service_string() 697 713 } 698 714 715 + /// The live `options { default-snomask }` bare-`+s` default category mask, or `None` when the 716 + /// store was never seeded (unit tests). The MODE handler ([`crate::command::mode`]) prefers this 717 + /// when `Some` and falls back to [`crate::snomask::SnoMask::ALL`] otherwise (slice 266). `main` 718 + /// seeds it at boot and a `REHASH` re-seeds it, so an operator can re-tune which categories a 719 + /// fresh `+s` subscribes to live. 720 + pub fn live_default_snomask() -> Option<u32> { 721 + LIVE_CONF.default_snomask() 722 + } 723 + 699 724 /// The live `options { knock-delay }` per-user KNOCK cooldown in seconds, or `None` when the 700 725 /// store was never seeded (unit tests). [`crate::command::knock`] prefers this when `Some` and 701 726 /// falls back to [`crate::knock_throttle::KNOCK_DELAY_USER_SECS`] otherwise (slice 210). A ··· 1040 1065 allow { host "*@10.0.0.*"; class "c" } 1041 1066 connect "peer.test" { host "192.0.2.1"; port 6667; class "c" } 1042 1067 operator "root" { mask "*@*"; password "pw"; class "c" } 1043 - options { default-user-modes "+i"; default-channel-modes "+m"; flood-kill-ahead 30; max-penalty 12; knock-delay 250; knock-delay-channel 45; metadata-rate-cost 5; metadata-rate-burst 7; max-bans 7; max-bans-large 9; auto-connect #true; connection-accept "split"; split-min-servers 3; split-min-users 50; sts-duration 86400; sts-preload; oper-keys "bot-likeliness-score" "secret.*"; service-string "is a Cool Bot" } 1068 + options { default-user-modes "+i"; default-channel-modes "+m"; flood-kill-ahead 30; max-penalty 12; knock-delay 250; knock-delay-channel 45; metadata-rate-cost 5; metadata-rate-burst 7; max-bans 7; max-bans-large 9; auto-connect #true; connection-accept "split"; split-min-servers 3; split-min-users 50; sts-duration 86400; sts-preload; oper-keys "bot-likeliness-score" "secret.*"; service-string "is a Cool Bot"; default-snomask "+ck" } 1044 1069 iauth { ident; timeout 25 } 1045 1070 "#; 1046 1071 ··· 1082 1107 assert!(s.max_bans_large().is_none()); 1083 1108 // slice 256 — likewise unseeded → WHOIS falls back to DEFAULT_SERVICE_STRING. 1084 1109 assert!(s.service_string().is_none()); 1110 + // slice 266 — likewise unseeded → MODE falls back to SnoMask::ALL. 1111 + assert!(s.default_snomask().is_none()); 1085 1112 } 1086 1113 1087 1114 #[test] ··· 1155 1182 assert_eq!(b.max_bans_large, 9); 1156 1183 // slice 256 — `service-string "is a Cool Bot"` → that value, not the default literal. 1157 1184 assert_eq!(b.service_string, "is a Cool Bot"); 1185 + // slice 266 — `default-snomask "+ck"` → the c|k mask, not the ALL default. 1186 + assert_eq!( 1187 + b.default_snomask, 1188 + crate::snomask::SnoMask::cconn() | crate::snomask::SnoMask::skill() 1189 + ); 1158 1190 // slice 205 — `auto-connect #true` → on, not the off default. 1159 1191 assert!(b.auto_connect); 1160 1192 // slice 206 — `connection-accept "split"` + thresholds, not the On/0/0 default. ··· 1208 1240 assert_eq!(s.max_bans_large().unwrap(), 9); 1209 1241 // slice 256 — the service string reads back from the fresh store. 1210 1242 assert_eq!(s.service_string().unwrap().as_str(), "is a Cool Bot"); 1243 + // slice 266 — the default snomask reads back from the fresh store. 1244 + assert_eq!( 1245 + s.default_snomask().unwrap(), 1246 + crate::snomask::SnoMask::cconn() | crate::snomask::SnoMask::skill() 1247 + ); 1211 1248 // slice 205 — `auto-connect #true` reads back from the fresh store. 1212 1249 assert!(s.auto_connect().unwrap()); 1213 1250 // slice 206 — connection-accept + thresholds read back from the fresh store. ··· 1369 1406 // slice 256 — re-seed an empty service string (the stale "is a Cool Bot" of 1370 1407 // SAMPLE_CONF2 must be gone); the empty value exercises the round-trip. 1371 1408 service_string: String::new(), 1409 + // slice 266 — re-seed the default snomask (the stale "+ck" of SAMPLE_CONF2 must be 1410 + // gone); `0` (empty mask) exercises the round-trip. 1411 + default_snomask: 0, 1372 1412 }); 1373 1413 assert!(s.initialized()); 1374 1414 assert!( ··· 1409 1449 s.service_string().as_deref().map(|a| a.as_str()), 1410 1450 Some(""), 1411 1451 "the stale \"is a Cool Bot\" must be gone" 1452 + ); 1453 + // slice 266 — the default snomask re-seeds too (reads `Some(0)`, not the stale "+ck"). 1454 + assert_eq!( 1455 + s.default_snomask(), 1456 + Some(0), 1457 + "the stale \"+ck\" default snomask must be gone" 1412 1458 ); 1413 1459 // slice 205 — auto-connect re-seeds too (reads `Some(false)`, not the stale `true`). 1414 1460 assert_eq!(
+1
leveva/src/main.rs
··· 178 178 max_bans: config.options.max_bans, 179 179 max_bans_large: config.options.max_bans_large, 180 180 service_string: config.options.service_string.clone(), 181 + default_snomask: config.options.default_snomask, 181 182 }); 182 183 let ctx = ServerContext::from_config(&config, created, motd); 183 184
+62
leveva/src/snomask.rs
··· 191 191 mask 192 192 } 193 193 194 + /// Parse a config `default-snomask` string (`"+ck"`, `"ck"`, `"+c-k"`) into a category mask, 195 + /// starting from an **empty** mask with default sign `+`. Like [`apply_delta`] but **strict**: an 196 + /// unknown letter is a config error (returned as `Err(letter)`), matching the sibling 197 + /// `default-user-modes`/`default-channel-modes` knobs which reject typos at boot — whereas the 198 + /// runtime MODE path ([`apply_delta`]) silently ignores unknown letters (charybdis runtime 199 + /// leniency). An empty / all-sign string yields `Ok(0)` (a legal empty default — a bare `+s` then 200 + /// drops the `+s` bit per the slice-257 empty-mask rule). Pure + total + panic-free — the 201 + /// `default-snomask` fuzz seam (P11 slice 266). 202 + pub fn parse_config(s: &str) -> Result<u32, char> { 203 + let mut mask = 0u32; 204 + let mut adding = true; 205 + for ch in s.chars() { 206 + match ch { 207 + '+' => adding = true, 208 + '-' => adding = false, 209 + other => { 210 + let bit = bit_of(other).ok_or(other)?; 211 + if adding { 212 + mask |= bit; 213 + } else { 214 + mask &= !bit; 215 + } 216 + } 217 + } 218 + } 219 + Ok(mask) 220 + } 221 + 194 222 /// Render `mask` as its set letters in canonical order, no leading sign (the caller — the 195 223 /// `008` reply — prepends `+`). Empty string when no category is set. 196 224 pub fn render(mask: u32) -> String { ··· 232 260 fn apply_delta_is_idempotent_for_a_repeated_add() { 233 261 let once = apply_delta(0, "+c"); 234 262 assert_eq!(apply_delta(once, "+c"), once); 263 + } 264 + 265 + #[test] 266 + fn parse_config_reads_a_config_default_snomask_string() { 267 + // Happy path: default sign `+`, explicit `+`, multi-letter. 268 + assert_eq!(parse_config("ck"), Ok(SnoMask::cconn() | SnoMask::skill())); 269 + assert_eq!(parse_config("+ck"), Ok(SnoMask::cconn() | SnoMask::skill())); 270 + assert_eq!( 271 + parse_config("cks"), 272 + Ok(SnoMask::cconn() | SnoMask::skill() | SnoMask::general()) 273 + ); 274 + } 275 + 276 + #[test] 277 + fn parse_config_empty_and_sign_only_yield_an_empty_mask() { 278 + // A legal empty default (a bare `+s` then drops the bit per the slice-257 rule). 279 + assert_eq!(parse_config(""), Ok(0)); 280 + assert_eq!(parse_config("+"), Ok(0)); 281 + assert_eq!(parse_config("+-+"), Ok(0)); 282 + } 283 + 284 + #[test] 285 + fn parse_config_minus_is_the_inverse_of_plus() { 286 + // `+ck-k` adds c,k then drops k → just c. 287 + assert_eq!(parse_config("+ck-k"), Ok(SnoMask::cconn())); 288 + } 289 + 290 + #[test] 291 + fn parse_config_rejects_an_unknown_letter_strictly() { 292 + // Strict, unlike the lenient runtime `apply_delta`: the bad letter is surfaced. 293 + assert_eq!(parse_config("cZk"), Err('Z')); 294 + assert_eq!(parse_config("9"), Err('9')); 295 + // Contrast: the runtime MODE path ignores the same unknown letter (charybdis leniency). 296 + assert_eq!(apply_delta(0, "cZk"), SnoMask::cconn() | SnoMask::skill()); 235 297 } 236 298 237 299 #[test]
+51
leveva/tests/default_snomask_proptest.rs
··· 1 + //! Property-based fuzzing of the `default-snomask` config parser 2 + //! ([`leveva::snomask::parse_config`], P11 slice 266, charybdis `default_snomask`). 3 + //! 4 + //! `parse_config` is the strict config seam behind `options { default-snomask }`: it turns a 5 + //! snomask-letter string into the category mask a bare `MODE <nick> +s` seeds. Unlike the lenient 6 + //! runtime MODE path ([`leveva::snomask::apply_delta`]), it rejects an unknown letter (a config 7 + //! typo) — but it must do so **without panicking** for *any* input, and a known-letter-only string 8 + //! must always parse. It is fed a hand-edited config string, so the parser is the one place this 9 + //! can't wobble. 10 + 11 + use leveva::snomask::{apply_delta, parse_config, render, SnoMask}; 12 + use proptest::prelude::*; 13 + 14 + /// The full snomask letter alphabet (every category leveva knows), the legal `parse_config` domain. 15 + const LETTERS: &str = "cCksnruxydfb"; 16 + 17 + proptest! { 18 + /// For ANY input the parser never panics — it returns `Ok(mask)` or `Err(bad_letter)`. 19 + #[test] 20 + fn parse_config_never_panics(s in ".{0,64}") { 21 + let _ = parse_config(&s); 22 + } 23 + 24 + /// A string built only from known letters + signs always parses `Ok`, and every result bit is 25 + /// a real category bit (a subset of `ALL`) — no spurious bits, no truncation. 26 + #[test] 27 + fn letter_only_strings_parse_to_a_subset_of_all( 28 + s in prop::collection::vec(prop::sample::select("cCksnruxydfb+-".chars().collect::<Vec<_>>()), 0..24) 29 + ) { 30 + let s: String = s.into_iter().collect(); 31 + let mask = parse_config(&s).expect("a sign/known-letter string must parse"); 32 + prop_assert_eq!(mask & !SnoMask::ALL, 0, "no bit outside ALL may be set"); 33 + // render → re-parse is a fixed point (the mask round-trips through its canonical letters). 34 + prop_assert_eq!(parse_config(&render(mask)), Ok(mask)); 35 + } 36 + 37 + /// An unrecognised character anywhere makes the parse fail with *that* character (strict), 38 + /// whereas the runtime `apply_delta` silently ignores it (the documented leniency contrast). 39 + #[test] 40 + fn an_unknown_letter_is_rejected_strictly( 41 + prefix in prop::collection::vec(prop::sample::select(LETTERS.chars().collect::<Vec<_>>()), 0..8), 42 + bad in prop::char::range('!', '~').prop_filter("not a snomask letter or sign", |c| 43 + !LETTERS.contains(*c) && *c != '+' && *c != '-'), 44 + ) { 45 + let prefix: String = prefix.into_iter().collect(); 46 + let s = format!("{prefix}{bad}"); 47 + prop_assert_eq!(parse_config(&s), Err(bad)); 48 + // apply_delta ignores the unknown letter → the same prefix-only mask either way. 49 + prop_assert_eq!(apply_delta(0, &s), apply_delta(0, &prefix)); 50 + } 51 + }
+11
leveva/tests/fixtures/default_snomask.kdl
··· 1 + server { name "leveva.test"; description "Leveva Test Server"; sid "0ABC" } 2 + admin { name "Admin"; email "admin@test"; location "Test Lab"; network "TestNet" } 3 + class "c" { ping-freq 90; max-links 10; sendq 512000 } 4 + listen { port 16700 } 5 + allow { host "*@*"; class "c" } 6 + operator "root" { mask "*@*"; password "hunter2"; class "c"; privileges "die" "restart" "rehash" "trace" "tkline" } 7 + connect "peer.test" { host "192.0.2.1"; port 6667; class "c" } 8 + 9 + // slice 266 — a bare `MODE <nick> +s` seeds only the connect (`c`) + kill (`k`) categories, 10 + // NOT every category. The golden proves config → live store → `008` enforcement. 11 + options { default-channel-modes "+"; default-snomask "+ck" }
+44
leveva/tests/golden_default_snomask_config.rs
··· 1 + //! Boot-level golden: the **configurable bare-`+s` default snomask** (P11 slice 266, 2 + //! `options { default-snomask }`, charybdis `default_snomask`), end-to-end through the real 3 + //! `leveva` binary. 4 + //! 5 + //! Slice 257 hard-coded a bare `MODE <nick> +s` (no following mask token) to seed **every** 6 + //! category. Slice 266 makes that seed the configured value. This proves the config → live store 7 + //! → MODE enforcement path: booting with `default-snomask "+ck"` makes a bare `+s` report 8 + //! `008 <nick> +ck` (connect + kill only), *not* the all-categories `+cCksnruxydfb`. 9 + //! 10 + //! `default_snomask.kdl` carries `default-snomask "+ck"` and the `operator root`/`hunter2` block. 11 + //! Every token is deterministic (fixed nick `root`); the `008` reply is canonicalization-free. 12 + 13 + mod harness; 14 + use harness::{boot_fixture, Client}; 15 + 16 + #[test] 17 + fn configured_default_snomask_seeds_a_bare_plus_s() { 18 + let _srv = boot_fixture("default_snomask.kdl", harness::PORT); 19 + 20 + let mut root = Client::connect(); 21 + root.send("NICK root"); 22 + root.send("USER root 0 * :Root Tester"); 23 + root.read_until(" 422 "); 24 + root.send("OPER root hunter2"); 25 + root.read_until(" 381 "); 26 + 27 + // A bare `+s` (no following mask token) seeds the *configured* default, surfaced via `008`. 28 + root.send("MODE root +s"); 29 + let line_008 = root.read_until(" 008 "); 30 + let snomask = line_008 31 + .lines() 32 + .find(|l| l.contains(" 008 ")) 33 + .expect("a 008 RPL_SNOMASK line is present"); 34 + 35 + // The configured `+ck`, NOT the all-categories default a slice-257 bare `+s` would seed. 36 + assert!( 37 + snomask.contains("+ck"), 38 + "008 must report the configured default snomask +ck: {snomask:?}" 39 + ); 40 + assert!( 41 + !snomask.contains("+cCksnruxydfb"), 42 + "008 must NOT report the all-categories default once the knob is set: {snomask:?}" 43 + ); 44 + }