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): +r registered-only-join channel mode (charybdis MODE_REGONLY) — P11 slice 241

Adds the leveva-native channel flag mode +r: on a +r channel only a client
signed in to a services account may JOIN; a signed-out client is refused 477
(charybdis ERR_NEEDREGGEDNICK, which overloads numeric 477 — reuses
Numeric::ErrNochanmodes since a duplicate-discriminant variant is impossible).
The join-plane sibling of +g; a hard JOIN gate like +O/+S (an INVITE/key/
exception does NOT bypass it — account identity is a user property), so KNOCK to
a +r channel is refused too.

- mode.rs: ChanMode::RegisteredOnly, letter r, bit 0x80000000 (last free u32
bit), Flag kind; added to ALL/as_char/bit/mode_name/from_char.
- command/mode.rs: supported_flag 'r' (the second allowlist).
- channel.rs: pure regonly_blocks(set, has_account) seam + the hard-gate clause
in check_join (reads attrs.account, same source as $a extban); new
JoinReject::RegisteredOnly.
- command/join.rs + command/knock.rs: reject mapping + KNOCK refusal.
- ISUPPORT/MYINFO auto-derive (CHANMODES …SzTgr, MYINFO …SzTgr…); 10 snapshots
+ isupport/registration assertions updated (token-only).

Reclaiming the slice-77 ripped-out reop letter r triggered the
unknown-chanmode-letter hazard: removed the obsolete ripped_out_reop_is_unknown
test, updated chan_mode_bits_match_oracle, switched golden_modes's unknown-mode
probe to +>, and repurposed the modes_proptest AnonReop op to BareAdmin.

Tests: golden_regonly.rs (RED-first), channel/mode/command::mode units (gate +
inverse + hard-gate proof + truth table), regonly_proptest.rs, modes_proptest
FLAGS += RegisteredOnly.

cargo test -p leveva green; cargo clippy -p leveva --tests clean; cargo build
--workspace 0 warnings.

+568 -70
+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: 240 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` (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), 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: 241 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), 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 ---
+1 -1
docs/claude-memory/README.md
··· 76 76 - [leveva +r restricted enforcement](leveva-restricted-enforcement.md) — slices 193–196: the `+r` (IRCnet `FLAGS_RESTRICT`) connection is enforced (was set-but-cosmetic) — a restricted user can become neither IRC operator (OPER→484) nor channel operator (no creator-op; 194: no third-party `MODE +o`, halfop+, voice OK; 195: never auto-reopped by ANY trigger via the `enforce_reop` chokepoint) and cannot change nick (196: NICK→484, RFC 2812 §3.1.7, family complete); `mode::is_restricted` seam 77 77 - [leveva KNOCK (slices 198–200, 209)](leveva-knock.md) — leveva-native charybdis-style KNOCK to a +i/+k/+l channel (710/711); shares the JOIN check_join gate; S2S propagation done (199, `ENCAP * KNOCK`); per-channel 712 flood throttle done (200, `knock_throttle::KnockThrottle`, local-path only); per-user throttle done (209, `knock_delay` 300s, second `knock_user_throttle` keyed by UID, `cooling`/`arm` peek-then-arm split); family complete; 210 = both `knock_delay`/`knock_delay_channel` are now REHASH-able `options { knock-delay / knock-delay-channel }` knobs (0 disables) 78 78 - [leveva flood penalty (slices 201–208, COMPLETE)](leveva-flood-penalty.md) — command-rate flood protection (the IRCnet "fakelag" penalty clock), deferred since the start; `flood::FloodClock` (idle-decay, Excess Flood kill), armed by the serve loop NOT `Session::new` (heartbeat opt-in precedent — synthetic-burst tests don't trip it unless they call `enable_flood_protection()`); lit up the dead can-flood/no-penalty privileges; 202 = per-command cost table (`command_penalty`: PONG exempt, WHO/LIST/etc +4, JOIN/KNOCK/INVITE +2); 203 = pre-registration guard (charge the same clock in the `Registering` phase too, after QUIT/before CAP); 204 = `options { flood-kill-ahead N }` config knob (REHASH-reloadable via ConfStore, parser rejects 0, NO ServerContext field — flood is serve-loop-armed); 208 = the fakelag *delay/smoothing* half (`MAX_PENALTY`=10 const; serve loop `sleep_until`-parks the read arm when the clock is past the threshold, output arms stay live; `delay_secs`/`flood_delay_secs`, `charge` unchanged) — family now COMPLETE; 210 = `MAX_PENALTY` is now a REHASH-able `options { max-penalty }` knob (default 10, 0=aggressive smoothing) — no flood threshold remains a const 79 - - [leveva +C no-CTCP + flag-mode wiring](leveva-noctcp-and-flag-mode-wiring.md) — slice 220 `+C` (charybdis MODE_NOCTCP, PRIVMSG-only, distributed enforcement); THE GOTCHA: adding a channel flag mode needs BOTH `ChanMode` (mode.rs) AND `command::mode::supported_flag` (the second allowlist) or the MODE command 472s; `channel::is_blocked_ctcp` + `Channels::blocks_ctcp` seam; family extended with `+T` no-NOTICE (239) and `+g` free-invite (240, charybdis MODE_FREEINVITE — any member may INVITE; gate `channel::free_invite_allows` in `Channels::invite`; `g` is the *channel* letter, disjoint from user-mode `+g` CallerId) 79 + - [leveva +C no-CTCP + flag-mode wiring](leveva-noctcp-and-flag-mode-wiring.md) — slice 220 `+C` (charybdis MODE_NOCTCP, PRIVMSG-only, distributed enforcement); THE GOTCHA: adding a channel flag mode needs BOTH `ChanMode` (mode.rs) AND `command::mode::supported_flag` (the second allowlist) or the MODE command 472s; `channel::is_blocked_ctcp` + `Channels::blocks_ctcp` seam; family extended with `+T` no-NOTICE (239) and `+g` free-invite (240, charybdis MODE_FREEINVITE — any member may INVITE; gate `channel::free_invite_allows` in `Channels::invite`; `g` is the *channel* letter, disjoint from user-mode `+g` CallerId) and `+r` registered-only join (241, charybdis MODE_REGONLY — hard JOIN gate via `channel::regonly_blocks` in `check_join`, 477 reusing ErrNochanmodes; reclaiming the ripped-out reop letter `r` hit the unknown-chanmode-letter-is-gt hazard) 80 80 - [golden read_until timeout flake](golden-read-until-timeout-flake.md) — boot-golden tests flake under load (read_until breaks on socket read-timeout, returns partial buffer, panic point shifts between runs); stash-and-rerun to confirm pre-existing, not a slice regression (observed golden_restricted_op 2026-06-16) 81 81 - [leveva stale deferral comments](leveva-stale-deferral-comments.md) — many "out of scope"/"deferred" doc comments in leveva src are STALE (feature landed in a later slice, comment never updated); verify against code first — confirmed stale: connlimit max_global_* (done slice 150), kill inbound-S2S notice (185), wss +Z (213+227); slice 229 closed metadata SYNC (774 ruled a deliberate divergence) → the draft/metadata-2 track is now COMPLETE, no metadata follow-ons remain 82 82 - [leveva +z op-moderation](leveva-opmod-plus-z.md) — `+z` (charybdis MODE_OPMODERATE): a `+n`/`+m`/`+b`-blocked message on a `+z` channel redirects to ops not 404; slices 235 (PRIVMSG/NOTICE local), 236 (TAGMSG/REDACT local), 237 (PRIVMSG/NOTICE S2S via the `@<chan>` statusmsg op-redirect seam), 238 (TAGMSG/REDACT S2S on that same seam) — **track COMPLETE, no follow-on**
+22
docs/claude-memory/leveva-noctcp-and-flag-mode-wiring.md
··· 118 118 is CallerId — channel and user mode namespaces never collide. Distributed enforcement (gate at the 119 119 inviter's home, bit replicated) → NO S2S follow-on. golden `tests/golden_freeinvite.rs`. 120 120 121 + **Slice 241 added `+r` (registered-only join), a port of charybdis `MODE_REGONLY`** — the 122 + **join-plane** sibling of `+g` (and a JOIN gate like `+S`/`+O`, not a speech mode). On a `+r` 123 + channel only a client signed in to a services account may JOIN; a signed-out client is refused 124 + `477` (charybdis overloads 477 = `ERR_NEEDREGGEDNICK`; leveva reuses `Numeric::ErrNochanmodes` = 125 + 477 since a duplicate-discriminant variant is impossible). A plain flag mode (bit `0x80000000`, 126 + the **last free u32 bit**, next after `+g`) — NO new match arms (rides the generic 127 + `ModeChange::Flag` path). Two-allowlist gotcha applied (`mode.rs` + `supported_flag` `'r'`). Pure 128 + seam `channel::regonly_blocks(set, has_account)` = `set && !has_account`, fuzz 129 + `tests/regonly_proptest.rs`; the gate is a **hard-gate** clause in `Channels::check_join` alongside 130 + `+O`/`+S` BEFORE the invite override (INVITE/key/exception do NOT bypass — account identity is a 131 + user property, not a channel relationship; a documented divergence from charybdis, where the 132 + regonly check sits with `+i`/`+b`/`+l` after the invite shortcut). `attrs.account` was already 133 + threaded in (same registry-record field as `$a` extban / EXTENDED-JOIN). New 134 + `JoinReject::RegisteredOnly`; KNOCK to `+r` refused like `+O`/`+S`. Distributed enforcement → NO 135 + S2S follow-on. golden `tests/golden_regonly.rs`. **GOTCHA (the [[unknown-chanmode-letter-is-gt]] 136 + hazard, hit live):** the channel letter `r` was the slice-77 ripped-out reop flag; reclaiming it 137 + broke two tests that asserted `r` named no mode (`ripped_out_reop_is_unknown` removed; 138 + `chan_mode_bits_match_oracle` updated) and forced `golden_modes`'s unknown-mode probe from `+r` to 139 + `+>`, and the `modes_proptest` `AnonReop` op (which modeled `a`/`r` as ripped-out no-ops) was 140 + repurposed to `BareAdmin` (only the bare-membership `a`-never-renders invariant survives). The 141 + chm channel-flag family is now `+C`/`+c`/`+S`/`+z`/`+T` (speech) + `+g`/`+r` (join-plane). 142 + 121 143 Related: [[leveva-extbans]] (slice 214 speech-plane ban mute via `CanSend::Banned`), 122 144 [[leveva-elemental-channel-modes]], [[leveva-channel-mode-rework-slice77]], 123 145 [[leveva-restricted-enforcement]] (the `+Z` secure user mode lineage).
+84
docs/progress-log/p11.md
··· 13465 13465 **Plan:** [`docs/superpowers/plans/2026-06-17-p11-slice240-freeinvite-plus-g.md`](../superpowers/plans/2026-06-17-p11-slice240-freeinvite-plus-g.md). 13466 13466 13467 13467 **Gate:** `cargo test -p leveva` green + `cargo clippy -p leveva --tests` clean + `cargo build --workspace` 0 warnings. 13468 + 13469 + ## 2026-06-17 — P11 slice 241: `+r` registered-only-join channel mode (charybdis `MODE_REGONLY`) 13470 + 13471 + Adds the leveva-native channel flag mode **`+r`** — a port of charybdis's `MODE_REGONLY`. When 13472 + set, only a client signed in to a services account may `JOIN`; a not-identified client is 13473 + refused with `477` (charybdis `ERR_NEEDREGGEDNICK`) and never joins. Clearing `-r` restores the 13474 + open state. It is the join/management-plane sibling of the `chm_simple` channel-flag family 13475 + ([[leveva-noctcp-and-flag-mode-wiring]]) — `+C`/`+c`/`+S`/`+z`/`+T` were speech-plane flags, 13476 + `+g` (240) and `+r` are join-plane flags. leveva-native (bit past the oracle range), so no 13477 + differential. 13478 + 13479 + The channel letter `r` was the **ripped-out reop flag** (slice-77 rework dropped it; the reop 13480 + *list* is the unrelated capital `R`); slice 241 reclaims it — exactly the 13481 + [[unknown-chanmode-letter-is-gt]] hazard (a future slice claims a previously-"unused" letter), 13482 + which is why the `golden_modes` unknown-mode probe was switched from `+r` to `+>`. 13483 + 13484 + ### Mechanism (a plain flag mode + one hard-gate clause) 13485 + 13486 + - **`mode.rs`**: new `ChanMode::RegisteredOnly` — letter `'r'`, bit `0x80000000` (leveva-native, 13487 + the last free u32 bit, next after `FreeInvite`'s `0x40000000`), `mode_name` `"MODE_REGONLY"`, 13488 + `kind` `Flag`; added to `ChanMode::ALL` (now `[ChanMode; 27]`) after `FreeInvite`, and to 13489 + `as_char`/`bit`/`mode_name`/`from_char`. A Flag mode needs **no** new match arms anywhere — it 13490 + rides the generic `ModeChange::Flag` path in `apply_changes`/`apply_modes_as_server`/both 13491 + renderers/CHANTS `parse_flag_state`, and the `render(flags)` bitmask. 13492 + - **`command/mode.rs`**: `'r' => Some(ChanMode::RegisteredOnly)` in `supported_flag` — **the 13493 + second allowlist** (without it a client `MODE +r` would 472, per the 13494 + [[leveva-noctcp-and-flag-mode-wiring]] gotcha). 13495 + - **`channel.rs`**: pure decision `regonly_blocks(regonly_set, joiner_has_account)` (`regonly_set 13496 + && !has_account`) — the seam the gate and fuzz target read. `check_join` adds a hard-gate 13497 + clause (alongside `+O`/`+S`, before the invite override): `if 13498 + regonly_blocks(m.has(RegisteredOnly), attrs.account.is_some()) { return 13499 + Some(JoinReject::RegisteredOnly); }`. `attrs.account` is already threaded in (the same 13500 + registry-record field the `$a` extban and EXTENDED-JOIN read). New `JoinReject::RegisteredOnly`. 13501 + - **`command/join.rs`**: `JoinReject::RegisteredOnly` → `(Numeric::ErrNochanmodes /*477*/, 13502 + "Cannot join channel (+r) - you need to be identified to a registered nick")`. 13503 + - **`command/knock.rs`**: `+r` is a hard gate an INVITE can't bypass, so `KNOCK` to a `+r` 13504 + channel is refused (`cannot_knock`, "registered-nick-only channel") — same stance as `+O`/`+S`. 13505 + - **`numeric.rs`**: `ErrNochanmodes`'s doc records that charybdis overloads **477** as 13506 + `ERR_NEEDREGGEDNICK` (a duplicate-discriminant `Numeric` variant is impossible; 477 is the 13507 + wire-correct code). 13508 + - **ISUPPORT / MYINFO**: auto-derive from `ChanMode::ALL` — CHANMODES type-D group becomes 13509 + `psmntiOCcSzTgr`; MYINFO `004` chan-mode string becomes `psmntiOCcSzTgrovqahYbeIRklj`. 10 golden 13510 + snapshots + the `isupport`/`registration` hard-coded assertions updated (token-only). 13511 + 13512 + ### Divergences 13513 + 13514 + - **Hard gate (not invite-overridable).** Unlike charybdis (where the regonly check sits inside 13515 + the same block as `+i`/`+b`/`+l`, after the invite shortcut), leveva treats `+r` as a hard gate 13516 + alongside `+O`/`+S`: an INVITE/key/exception does **not** bypass it, because account identity is 13517 + a property of the user, not of the channel relationship. 13518 + - **477 overload.** Reuses `Numeric::ErrNochanmodes` (=477) for the wire-correct charybdis 13519 + `ERR_NEEDREGGEDNICK` code. 13520 + - **Distributed enforcement** — the gate runs at the **joiner's** local server; the `+r` bit 13521 + replicates network-wide via the standard flag-mode MODE/CHANTS machinery, so every server 13522 + evaluates the gate against the same account state → no `+r`-specific S2S follow-on (as for 13523 + `+C`/`+c`/`+T`/`+g`). The `+r` mode is complete. 13524 + 13525 + ### Tests 13526 + 13527 + - `tests/golden_regonly.rs` (TDD RED first): on a `+r` channel a signed-out client's JOIN gets 13528 + `477` and does not join; after `-r` the same client JOINs cleanly. (Boot-level sign-in needs 13529 + SASL, so the signed-**in** admit is covered by the unit + proptest, where an account is 13530 + synthesized directly.) 13531 + - `channel` unit: `check_join_enforces_registered_only` (signed-in admit + signed-out reject + 13532 + hard-gate proof that an INVITE does not bypass + the `-r` inverse round-trip), 13533 + `regonly_blocks_truth_table`. 13534 + - `command::mode` unit: `regonly_flag_set_and_clear` (the `supported_flag` allowlist — MODE 13535 + command echoes a MODE line not 472, renders in 324, `-r` clears). The obsolete 13536 + `ripped_out_reop_is_unknown` test (premised on `r` naming no mode) was removed. 13537 + - `mode` unit: `regonly_mode_is_a_plain_flag` (letter/bit/name/round-trip + disjoint bit over all 13538 + modes + `r`≠`R`); `chan_mode_bits_match_oracle` updated (`from_char('r')` now 13539 + `Some(RegisteredOnly)`, `'R'` still `ReopList`). 13540 + - Fuzz: `tests/regonly_proptest.rs` — `regonly_blocks` is total + matches an independent model 13541 + (`regonly_set && !has_account`) over arbitrary `(regonly_set, has_account)`; an account always 13542 + admits, no `+r` never blocks, a block implies `+r`-and-no-account. `tests/modes_proptest.rs` 13543 + FLAGS += `ChanMode::RegisteredOnly` (the `0x80000000` bit exercised set/clear/render in lockstep 13544 + with the model); the old `AnonReop` op was repurposed to `BareAdmin` (guards only that a 13545 + bare membership `a` never leaks into the flag string — `r` is now a real flag). 13546 + - `tests/golden_modes.rs`: the unknown-mode probe switched from `+r` to `+>` (charybdis's 13547 + non-existent example-mode letter, the [[unknown-chanmode-letter-is-gt]] rule). 13548 + 13549 + **Plan:** [`docs/superpowers/plans/2026-06-17-p11-slice241-regonly-plus-r.md`](../superpowers/plans/2026-06-17-p11-slice241-regonly-plus-r.md). 13550 + 13551 + **Gate:** `cargo test -p leveva` green + `cargo clippy -p leveva --tests` clean + `cargo build --workspace` 0 warnings.
+109
docs/superpowers/plans/2026-06-17-p11-slice241-regonly-plus-r.md
··· 1 + # P11 slice 241 — `+r` registered-only-join channel mode (charybdis `MODE_REGONLY`) 2 + 3 + ## Goal 4 + 5 + Port charybdis's `+r` (`MODE_REGONLY`): when a channel carries `+r`, only a client that is 6 + **signed in to a services account** may JOIN it; a not-signed-in client is rejected with 7 + `477` (charybdis `ERR_NEEDREGGEDNICK`) and never joins. Clearing `-r` restores the open 8 + state. `+r` is a hard join gate — like `+O` (oper-only) and `+S` (TLS-only) it is **not** 9 + overridable by an INVITE, a key, or an `+I`/`+e` exception: account identity is a property of 10 + the *user*, not the channel relationship. 11 + 12 + This is the join/management-plane sibling of the `chm_simple` channel-flag family 13 + ([[leveva-noctcp-and-flag-mode-wiring]]). It is a leveva-native bit past the oracle range, so 14 + this is a leveva-only slice (no differential). It ties straight into the existing account 15 + infrastructure: `attrs.account` (sourced from the registry record, the same field the `$a` 16 + extban and EXTENDED-JOIN already read) is already threaded into `check_join`. 17 + 18 + The letter `r` is **free** in the *channel*-mode namespace. (It names the disjoint *user* mode 19 + `Restricted` — channel and user modes never collide. Note the capital `R` is the unrelated 20 + `ReopList` channel mode.) 21 + 22 + ## Why this is a small slice 23 + 24 + `+r` is a plain **Flag** mode. Flag modes are handled generically everywhere — `kind()`'s 25 + `_ => Flag` catch-all, `render(flags)` bitmask, `apply_changes` / `apply_modes_as_server` / 26 + both renderers' `ModeChange::Flag` arms, and CHANTS `parse_flag_state`'s generic 27 + `ModeChange::Flag => flags |= mode.bit()`. So `+r` needs **zero** new match arms. The only 28 + behavioral wiring is one new `check_join` gate clause + its `JoinReject` mapping. 29 + 30 + ## Numeric note (477 overload) 31 + 32 + Charybdis reuses numeric **477** for both `ERR_NOCHANMODES` and `ERR_NEEDREGGEDNICK`. leveva's 33 + `Numeric::ErrNochanmodes = 477` is defined but **never emitted**. Since 477 is the wire-correct 34 + code for the registered-only rejection, the `+r` reject maps to `Numeric::ErrNochanmodes` 35 + (=477). Its doc comment is updated to record the charybdis overload. No new `Numeric` variant 36 + (a duplicate-`477` discriminant is impossible). 37 + 38 + ## Port checklist 39 + 40 + 1. **`mode.rs`** 41 + - New `ChanMode::RegisteredOnly` variant (chm_simple-family doc comment), placed in the flag 42 + cluster **after `FreeInvite`** in the enum and in `ALL` (so MYINFO/CHANMODES order becomes 43 + `…SzTgr`, appended last → no churn to existing positions). 44 + - `ALL: [ChanMode; 27]` (was 26) + `RegisteredOnly`. 45 + - `as_char` → `'r'`; `from_char` `'r'` → `RegisteredOnly`; `mode_name` → `"MODE_REGONLY"`. 46 + - `bit` → `0x80000000` (next free leveva-native bit after `FreeInvite`'s `0x40000000`; last 47 + u32 bit). 48 + - `kind()` — **no edit** (falls through to `Flag`). 49 + - Add bit/round-trip asserts to the existing mode unit tests. 50 + 51 + 2. **`command/mode.rs::supported_flag`** — add `'r' => Some(ChanMode::RegisteredOnly)` (the 52 + second allowlist; without it `MODE #c +r` → `472 unknown mode char`). 53 + 54 + 3. **`channel.rs`** 55 + - New `JoinReject::RegisteredOnly` variant. 56 + - Pure decision helper `channel::regonly_blocks(plus_r: bool, has_account: bool) -> bool` 57 + (`plus_r && !has_account`) — the fuzz seam. 58 + - `check_join` gate, placed with the other hard gates (after `+O`/`+S`, before the invite 59 + override): `if regonly_blocks(m.has(RegisteredOnly), attrs.account.is_some()) { return 60 + Some(JoinReject::RegisteredOnly); }`. 61 + 62 + 4. **`command/join.rs::join_reject`** — map `JoinReject::RegisteredOnly` → `(Numeric::Err 63 + Nochanmodes /*477*/, "Cannot join channel (+r) - you need to be identified to a registered 64 + nick")`. 65 + 66 + ## Tests (TDD — RED first) 67 + 68 + - **`channel.rs` unit:** 69 + - `regonly_rejects_a_signed_out_joiner` — `#c` is `+r`; a joiner with `account: None` → 70 + `Some(RegisteredOnly)`; the same joiner with `account: Some("acct")` → `None`. 71 + - `regonly_is_not_overridden_by_an_invite` (hard-gate proof) — `#c` is `+r`, the joiner holds 72 + a pending INVITE token, still `account: None` → `Some(RegisteredOnly)` (not bypassed). 73 + - `without_regonly_a_signed_out_joiner_is_admitted` (inverse) — `#c` has no `+r`; a signed-out 74 + joiner → `None`. 75 + - `regonly_blocks_truth_table` — `regonly_blocks` over the 4 combinations. 76 + - **`command/mode.rs` / `mode.rs` unit:** `regonly_mode_is_a_plain_flag` (letter/bit/name/ 77 + round-trip + disjoint bit over all modes); `supported_flag` accepts `'r'`. 78 + - **`tests/golden_regonly.rs`** (boot-level, models `golden_operonly.rs`): on a `+r` channel a 79 + signed-out client's JOIN gets `477` and does not join; after `-r` the same client JOINs 80 + cleanly. (Account sign-in at boot level needs SASL; the golden drives the signed-**out** 81 + rejection + the `-r` inverse, which need no account; the signed-in *admit* is covered by the 82 + unit + proptest where an account can be synthesized directly.) 83 + - **Fuzz:** 84 + - `tests/modes_proptest.rs` — add `ChanMode::RegisteredOnly` to `FLAGS`: the `0x80000000` bit 85 + is exercised set/clear/render in lockstep with the model over arbitrary MODE strings. 86 + - `tests/regonly_proptest.rs` — `regonly_blocks` is total and matches an independent model 87 + (`plus_r && !has_account`) over arbitrary `(plus_r, has_account)`; a denial implies the 88 + channel was `+r` and the joiner had no account; an account always admits. 89 + - **Snapshots:** regenerate the 005 CHANMODES / 004 MYINFO snapshots (token-only: type-D group 90 + `psmntiOCcSzTg` → `psmntiOCcSzTgr`, MYINFO chan string gains `r`). 91 + 92 + ## Divergences 93 + 94 + - **Hard gate (not invite-overridable).** Unlike charybdis (where the regonly check sits inside 95 + the same block as `+i`/`+b`/`+l`, after the invite shortcut), leveva treats `+r` as a hard 96 + gate alongside `+O`/`+S`: an INVITE/key/exception does **not** bypass it, because an account 97 + is a property of the user, not of the channel relationship. Documented, consistent with the 98 + existing leveva hard-gate trio. 99 + - **`477` overload.** Reuses `Numeric::ErrNochanmodes` (=477) for the wire-correct charybdis 100 + `ERR_NEEDREGGEDNICK` code (see Numeric note). 101 + - **Distributed enforcement.** The gate runs at the **joiner's** local server; the `+r` bit 102 + replicates network-wide via the standard flag-mode MODE/CHANTS machinery, so every server 103 + evaluates the gate against the same account state. No `+r`-specific S2S follow-on (like every 104 + other flag mode). 105 + 106 + ## Gate 107 + 108 + `cargo test -p leveva` green + `cargo clippy -p leveva --tests` clean + `cargo build --workspace` 109 + 0 warnings.
+86
leveva/src/channel.rs
··· 471 471 /// `519 ERR_SSLONLYCHAN`. A hard gate like `+O`: not overridable by an INVITE (a transport 472 472 /// requirement, not an access list). See [`sslonly_blocks`]. 473 473 SecureOnly, 474 + /// `+r` (registered-only) set and the joiner is not signed in to a services account → 475 + /// `477 ERR_NEEDREGGEDNICK` (charybdis overloads numeric 477). A hard gate like `+O`/`+S`: 476 + /// not overridable by an INVITE (account identity is a property of the user, not an access 477 + /// list). See [`regonly_blocks`]. 478 + RegisteredOnly, 474 479 } 475 480 476 481 /// Whether a `+S` ([`crate::mode::ChanMode::SslOnly`]) channel refuses this joiner (charybdis ··· 483 488 #[inline] 484 489 pub fn sslonly_blocks(sslonly_set: bool, joiner_secure: bool) -> bool { 485 490 sslonly_set && !joiner_secure 491 + } 492 + 493 + /// Whether a `+r` ([`crate::mode::ChanMode::RegisteredOnly`]) channel refuses this joiner 494 + /// (charybdis `MODE_REGONLY`, slice 241): the mode is set **and** the joiner is not signed in 495 + /// to a services account. A pure decision seam — the account signal is the joiner's registry 496 + /// record (`attrs.account.is_some()`), resolved at the [`Channels::check_join`] call site — 497 + /// paired here for unit + fuzz coverage (`tests/regonly_proptest.rs`). Returns `false` 498 + /// whenever the channel is not `+r` (anyone joins freely) or the joiner has an account (a 499 + /// signed-in user joins a `+r` channel). 500 + #[inline] 501 + pub fn regonly_blocks(regonly_set: bool, joiner_has_account: bool) -> bool { 502 + regonly_set && !joiner_has_account 486 503 } 487 504 488 505 /// The outcome of a channel-message permission check ([`Channels::can_send`]). ··· 955 972 // carried here in the subject's umodes. 956 973 if m.has(ChanMode::SslOnly) && sslonly_blocks(true, crate::mode::is_secure(attrs.umodes)) { 957 974 return Some(JoinReject::SecureOnly); 975 + } 976 + // +r (registered-only, charybdis MODE_REGONLY) is a hard gate too: a client not signed 977 + // in to a services account cannot join even with an INVITE or key. The account signal is 978 + // the joiner's registry record, carried here as `attrs.account` (the same field the `$a` 979 + // extban reads). 980 + if regonly_blocks(m.has(ChanMode::RegisteredOnly), attrs.account.is_some()) { 981 + return Some(JoinReject::RegisteredOnly); 958 982 } 959 983 // +k is enforced for everyone, invited or not. 960 984 if let Some(want) = &m.key { ··· 3250 3274 assert!(!sslonly_blocks(true, true), "+S + secure → admitted"); 3251 3275 assert!(!sslonly_blocks(false, false), "no +S → admitted"); 3252 3276 assert!(!sslonly_blocks(false, true), "no +S → admitted"); 3277 + } 3278 + 3279 + /// `+r` (registered-only, charybdis `MODE_REGONLY`) admits a signed-in client and rejects a 3280 + /// signed-out one with [`JoinReject::RegisteredOnly`] — a hard gate not overridden by an 3281 + /// INVITE; clearing `-r` reopens the channel to a signed-out client. 3282 + #[test] 3283 + fn check_join_enforces_registered_only() { 3284 + let ch = Channels::new(); 3285 + ch.join("#rust", &alice(), 0).unwrap(); 3286 + assert!(ch.apply_modes_as_server( 3287 + "#rust", 3288 + &[ModeChange::Flag { 3289 + add: true, 3290 + mode: ChanMode::RegisteredOnly, 3291 + }], 3292 + )); 3293 + let signed_in = crate::extban::SubjectAttrs { 3294 + account: Some("acct"), 3295 + ..crate::extban::SubjectAttrs::plain(false) 3296 + }; 3297 + let signed_out = crate::extban::SubjectAttrs::plain(false); 3298 + // A signed-in client (has an account) may join; a signed-out one is rejected. 3299 + assert_eq!( 3300 + ch.check_join("#rust", &bob(), "bob!u@host", None, &signed_in), 3301 + None, 3302 + "a signed-in client may join a +r channel" 3303 + ); 3304 + assert_eq!( 3305 + ch.check_join("#rust", &bob(), "bob!u@host", None, &signed_out), 3306 + Some(JoinReject::RegisteredOnly), 3307 + "a signed-out client is refused from a +r channel" 3308 + ); 3309 + // Hard gate: an INVITE does NOT let a signed-out client in. 3310 + ch.invite("#rust", &alice(), &bob()); 3311 + assert_eq!( 3312 + ch.check_join("#rust", &bob(), "bob!u@host", None, &signed_out), 3313 + Some(JoinReject::RegisteredOnly), 3314 + "an invite must not bypass +r" 3315 + ); 3316 + // Inverse: -r reopens the channel to a signed-out client. 3317 + assert!(ch.apply_modes_as_server( 3318 + "#rust", 3319 + &[ModeChange::Flag { 3320 + add: false, 3321 + mode: ChanMode::RegisteredOnly, 3322 + }], 3323 + )); 3324 + assert_eq!( 3325 + ch.check_join("#rust", &bob(), "bob!u@host", None, &signed_out), 3326 + None, 3327 + "-r admits a signed-out client again" 3328 + ); 3329 + } 3330 + 3331 + /// The pure [`regonly_blocks`] decision: blocks only when the mode is set and the joiner 3332 + /// has no account (the full truth table). 3333 + #[test] 3334 + fn regonly_blocks_truth_table() { 3335 + assert!(regonly_blocks(true, false), "+r + signed-out → blocked"); 3336 + assert!(!regonly_blocks(true, true), "+r + signed-in → admitted"); 3337 + assert!(!regonly_blocks(false, false), "no +r → admitted"); 3338 + assert!(!regonly_blocks(false, true), "no +r → admitted"); 3253 3339 } 3254 3340 3255 3341 /// Seed a `+R` reop mask onto an existing channel (via the trusted server path, so no
+6
leveva/src/command/join.rs
··· 16 16 Numeric::ErrSslonlychan, 17 17 "Cannot join channel (+S) - SSL/TLS required", 18 18 ), 19 + // 477: charybdis overloads ERR_NOCHANMODES (leveva's name for 477) as ERR_NEEDREGGEDNICK 20 + // for the +r (registered-only) gate. The wire code 477 is what charybdis sends. 21 + JoinReject::RegisteredOnly => ( 22 + Numeric::ErrNochanmodes, 23 + "Cannot join channel (+r) - you need to be identified to a registered nick", 24 + ), 19 25 }; 20 26 Message::builder(num) 21 27 .prefix(&ctx.name)
+9
leveva/src/command/knock.rs
··· 127 127 "SSL/TLS required", 128 128 )] 129 129 } 130 + // +r (registered-only) is a hard gate an INVITE cannot bypass either — refuse. 131 + Some(JoinReject::RegisteredOnly) => { 132 + vec![cannot_knock( 133 + ctx, 134 + &client.nick, 135 + &display, 136 + "registered-nick-only channel", 137 + )] 138 + } 130 139 // Invite-only / wrong-or-missing key / full: an INVITE would let them in — deliver. 131 140 Some(JoinReject::InviteOnly | JoinReject::BadKey | JoinReject::Full) => { 132 141 // Two flood throttles, both of which must be clear for the knock to be delivered:
+18 -14
leveva/src/command/mode.rs
··· 63 63 'z' => Some(ChanMode::OpModerate), 64 64 'T' => Some(ChanMode::NoNotice), 65 65 'g' => Some(ChanMode::FreeInvite), 66 + 'r' => Some(ChanMode::RegisteredOnly), 66 67 _ => None, 67 68 } 68 69 } ··· 1255 1256 dispatch(c, &Message::parse(line).unwrap(), ctx) 1256 1257 } 1257 1258 1258 - /// The ripped-out reop letter `r` still names no mode → `472`. (`a` is now the Admin 1259 - /// membership mode — covered in `owner_admin_halfop_modes_parse_and_echo`.) `r` carries 1260 - /// no nick, so the bare `+r`/`-r` land in the unknown-letter path. 1261 - #[test] 1262 - fn ripped_out_reop_is_unknown() { 1263 - let ctx = ctx(); 1264 - let mut alice = op_of(&ctx, "#rust"); 1265 - for line in ["MODE #rust +r", "MODE #rust -r"] { 1266 - let r = run_mode(&ctx, &mut alice, line); 1267 - assert_eq!(codes(&r), &["472"], "{line} → 472"); 1268 - } 1269 - let (modes, _) = ctx.channels.channel_mode_string("#rust", true).unwrap(); 1270 - assert!(!modes.contains('r'), "got {modes}"); 1271 - } 1259 + // (The old `ripped_out_reop_is_unknown` test asserted the ripped-out reop letter `r` named 1260 + // no mode → `472`. As of slice 241 the channel letter `r` is claimed for the registered-only 1261 + // mode `MODE_REGONLY` — covered by `regonly_flag_set_and_clear` above.) 1272 1262 1273 1263 /// `+O` is now a plain oper-only-join flag: a chanop sets and clears it like any other 1274 1264 /// flag (it takes no nick parameter — an extra word is ignored), and it renders in 324. ··· 1319 1309 run_mode(&ctx, &mut alice, "MODE #rust -g"); 1320 1310 let (modes, _) = ctx.channels.channel_mode_string("#rust", true).unwrap(); 1321 1311 assert!(!modes.contains('g'), "-g clears it: {modes}"); 1312 + } 1313 + 1314 + #[test] 1315 + fn regonly_flag_set_and_clear() { 1316 + let ctx = ctx(); 1317 + let mut alice = op_of(&ctx, "#rust"); 1318 + let r = run_mode(&ctx, &mut alice, "MODE #rust +r"); 1319 + assert_eq!(codes(&r), &["MODE"], "+r echoes a MODE line (not 472)"); 1320 + let (modes, _) = ctx.channels.channel_mode_string("#rust", true).unwrap(); 1321 + assert!(modes.contains('r'), "+r renders: {modes}"); 1322 + // Inverse: -r clears it. 1323 + run_mode(&ctx, &mut alice, "MODE #rust -r"); 1324 + let (modes, _) = ctx.channels.channel_mode_string("#rust", true).unwrap(); 1325 + assert!(!modes.contains('r'), "-r clears it: {modes}"); 1322 1326 } 1323 1327 1324 1328 /// `+z` (op-moderation, slice 235) is a plain flag a chanop sets/clears like any other flag
+8 -6
leveva/src/isupport.rs
··· 143 143 assert_eq!(user_modes_string(), "oOiwraWBxsgGZ"); 144 144 // ChanMode::ALL order: p s m n t i O C c S z T g o v q a h Y b e I R k l j (C=no-CTCP slice 145 145 // 220, c=no-color slice 232, S=TLS-only slice 233, z=op-moderation slice 235, 146 - // j=join-throttle slice 234, T=no-NOTICE slice 239, g=free-invite slice 240) 147 - assert_eq!(chan_modes_string(), "psmntiOCcSzTgovqahYbeIRklj"); 146 + // j=join-throttle slice 234, T=no-NOTICE slice 239, g=free-invite slice 240, 147 + // r=registered-only slice 241) 148 + assert_eq!(chan_modes_string(), "psmntiOCcSzTgrovqahYbeIRklj"); 148 149 } 149 150 150 151 #[test] ··· 169 170 170 171 #[test] 171 172 fn chanmodes_spec_is_grouped_by_kind() { 172 - // A=list(b,e,I,R) B=key(k) C=limit(l),join-throttle(j) D=flags(p,s,m,n,t,i,O,C,c,S,z,T,g — 173 - // C=no-CTCP slice 220, c=no-color slice 232, S=TLS-only slice 233, z=op-moderation slice 174 - // 235, j=join-throttle slice 234, T=no-NOTICE slice 239, g=free-invite slice 240) 175 - assert_eq!(chanmodes_spec(), "beIR,k,lj,psmntiOCcSzTg"); 173 + // A=list(b,e,I,R) B=key(k) C=limit(l),join-throttle(j) D=flags(p,s,m,n,t,i,O,C,c,S,z,T,g,r 174 + // — C=no-CTCP slice 220, c=no-color slice 232, S=TLS-only slice 233, z=op-moderation slice 175 + // 235, j=join-throttle slice 234, T=no-NOTICE slice 239, g=free-invite slice 240, 176 + // r=registered-only slice 241) 177 + assert_eq!(chanmodes_spec(), "beIR,k,lj,psmntiOCcSzTgr"); 176 178 } 177 179 178 180 #[test]
+58 -3
leveva/src/mode.rs
··· 412 412 /// the inviter's home server and the `+g` bit replicates via the standard flag-mode MODE/ 413 413 /// CHANTS machinery, so there is no `+g`-specific S2S follow-on (as for `+C`/`+c`/`+T`). 414 414 FreeInvite, 415 + /// `+r` — registered-only join: only a client signed in to a services account may JOIN 416 + /// (`MODE_REGONLY`). 417 + /// 418 + /// **leveva-native — a port of charybdis's `MODE_REGONLY`.** When `+r` is set, a JOIN by a 419 + /// client with no services account is refused with `477` (charybdis `ERR_NEEDREGGEDNICK`) 420 + /// and the client never joins; a signed-in client joins normally. Like 421 + /// [`OperOnly`](ChanMode::OperOnly) and [`SslOnly`](ChanMode::SslOnly) it is a **hard 422 + /// gate** — an INVITE, a key, or an `+I`/`+e` exception does **not** bypass it, because 423 + /// account identity is a property of the *user*, not of the channel relationship. A plain 424 + /// channel flag (no parameter), settable by any channel manager; the bit is a leveva-native 425 + /// value (`0x80000000`, past the oracle range). Enforced in 426 + /// [`crate::channel::Channels::check_join`] via [`crate::channel::regonly_blocks`] against 427 + /// the joiner's registry account (the same source the `$a` extban and EXTENDED-JOIN read). 428 + /// Distributed enforcement — the gate runs at the joiner's home server and the `+r` bit 429 + /// replicates via the standard flag-mode MODE/CHANTS machinery, so there is no 430 + /// `+r`-specific S2S follow-on (as for `+C`/`+c`/`+T`/`+g`). (Note: this is the channel 431 + /// letter — disjoint from the *user* mode `r` `Restricted`; the capital `R` is the 432 + /// unrelated [`ReopList`](ChanMode::ReopList) channel mode.) 433 + RegisteredOnly, 415 434 /// `+o` — channel operator privilege for a member (`MODE_CHANOP`). 416 435 Op, 417 436 /// `+v` — voice privilege for a member (`MODE_VOICE`). ··· 463 482 /// Every channel mode. Flag modes come first, in `flags[]` table order 464 483 /// (the order [`render`](ChanMode::render) emits them), followed by the 465 484 /// membership, list, and parameterised modes. 466 - pub const ALL: [ChanMode; 26] = [ 485 + pub const ALL: [ChanMode; 27] = [ 467 486 ChanMode::Private, 468 487 ChanMode::Secret, 469 488 ChanMode::Moderated, ··· 477 496 ChanMode::OpModerate, 478 497 ChanMode::NoNotice, 479 498 ChanMode::FreeInvite, 499 + ChanMode::RegisteredOnly, 480 500 ChanMode::Op, 481 501 ChanMode::Voice, 482 502 ChanMode::Owner, ··· 509 529 ChanMode::OpModerate => 'z', 510 530 ChanMode::NoNotice => 'T', 511 531 ChanMode::FreeInvite => 'g', 532 + ChanMode::RegisteredOnly => 'r', 512 533 ChanMode::Op => 'o', 513 534 ChanMode::Voice => 'v', 514 535 ChanMode::Owner => 'q', ··· 550 571 ChanMode::OpModerate => 0x10000000, // leveva-native (op-moderation, charybdis MODE_OPMODERATE) 551 572 ChanMode::NoNotice => 0x20000000, // leveva-native (no-NOTICE, charybdis MODE_NONOTICE) 552 573 ChanMode::FreeInvite => 0x40000000, // leveva-native (free-invite, charybdis MODE_FREEINVITE) 574 + ChanMode::RegisteredOnly => 0x80000000, // leveva-native (registered-only join, charybdis MODE_REGONLY) 553 575 ChanMode::Owner => 0x100000, // leveva-native (membership ~) 554 576 ChanMode::Admin => 0x200000, // leveva-native (membership &) 555 577 ChanMode::Halfop => 0x400000, // leveva-native (membership %) ··· 575 597 ChanMode::OpModerate => "MODE_OPMODERATE", 576 598 ChanMode::NoNotice => "MODE_NONOTICE", 577 599 ChanMode::FreeInvite => "MODE_FREEINVITE", 600 + ChanMode::RegisteredOnly => "MODE_REGONLY", 578 601 ChanMode::Op => "MODE_CHANOP", 579 602 ChanMode::Voice => "MODE_VOICE", 580 603 ChanMode::Owner => "MODE_OWNER", ··· 696 719 'z' => ChanMode::OpModerate, 697 720 'T' => ChanMode::NoNotice, 698 721 'g' => ChanMode::FreeInvite, 722 + 'r' => ChanMode::RegisteredOnly, 699 723 'o' => ChanMode::Op, 700 724 'v' => ChanMode::Voice, 701 725 'q' => ChanMode::Owner, ··· 905 929 assert!(ChanMode::render(ChanMode::OperOnly.bit()).contains('O')); 906 930 // `a` is now the leveva-native Admin membership mode (was the dead anonymous flag). 907 931 assert_eq!(ChanMode::from_char('a'), Some(ChanMode::Admin)); 908 - // `r` (reop flag) stays removed. 909 - assert_eq!(ChanMode::from_char('r'), None); 932 + // `r` (was the ripped-out reop flag) is now the registered-only mode (slice 241); the 933 + // reop *list* is the unrelated capital `R`. 934 + assert_eq!(ChanMode::from_char('r'), Some(ChanMode::RegisteredOnly)); 935 + assert_eq!(ChanMode::from_char('R'), Some(ChanMode::ReopList)); 910 936 } 911 937 912 938 #[test] ··· 1009 1035 // Renders among the flag letters; round-trips through ALL. 1010 1036 assert!(ChanMode::render(ChanMode::FreeInvite.bit()).contains('g')); 1011 1037 assert!(ChanMode::ALL.contains(&ChanMode::FreeInvite)); 1038 + } 1039 + 1040 + #[test] 1041 + fn regonly_mode_is_a_plain_flag() { 1042 + // `+r` (registered-only join, slice 241): leveva-native flag, its own letter/bit, no 1043 + // parameter. Disjoint from the *user* mode `r` (Restricted) and the channel `R` (ReopList). 1044 + assert_eq!(ChanMode::RegisteredOnly.as_char(), 'r'); 1045 + assert_eq!(ChanMode::RegisteredOnly.bit(), 0x80000000); 1046 + assert_eq!(ChanMode::RegisteredOnly.mode_name(), "MODE_REGONLY"); 1047 + assert_eq!(ChanMode::from_char('r'), Some(ChanMode::RegisteredOnly)); 1048 + assert_eq!(ChanMode::RegisteredOnly.kind(), ChanModeKind::Flag); 1049 + assert!(!ChanMode::RegisteredOnly.takes_parameter()); 1050 + assert_eq!(ChanMode::RegisteredOnly.rank(), 0); 1051 + assert_eq!(ChanMode::RegisteredOnly.sigil(), '\0'); 1052 + // Disjoint bit from every other channel mode. 1053 + for m in ChanMode::ALL { 1054 + if m != ChanMode::RegisteredOnly { 1055 + assert_eq!( 1056 + m.bit() & ChanMode::RegisteredOnly.bit(), 1057 + 0, 1058 + "{m:?} overlaps +r" 1059 + ); 1060 + } 1061 + } 1062 + // The channel `r` and the channel `R` are distinct modes. 1063 + assert_ne!(ChanMode::from_char('r'), ChanMode::from_char('R')); 1064 + // Renders among the flag letters; round-trips through ALL. 1065 + assert!(ChanMode::render(ChanMode::RegisteredOnly.bit()).contains('r')); 1066 + assert!(ChanMode::ALL.contains(&ChanMode::RegisteredOnly)); 1012 1067 } 1013 1068 1014 1069 #[test]
+3 -1
leveva/src/numeric.rs
··· 403 403 ErrBadchannelkey = 475, 404 404 /// `ERR_BADCHANMASK` 405 405 ErrBadchanmask = 476, 406 - /// `ERR_NOCHANMODES` 406 + /// `ERR_NOCHANMODES`. charybdis overloads numeric **477** as `ERR_NEEDREGGEDNICK` — leveva 407 + /// emits it under that meaning for the `+r` (registered-only) JOIN gate (slice 241), since 408 + /// 477 is the wire-correct code and a duplicate-discriminant variant is impossible. 407 409 ErrNochanmodes = 477, 408 410 /// `ERR_BANLISTFULL` 409 411 ErrBanlistfull = 478,
+2 -2
leveva/src/registration.rs
··· 1261 1261 // + the leveva-native elemental membership letters q/a/h (slice 158) and Y (OJOIN, 165) 1262 1262 // + C (no-CTCP, slice 220) + c (no-color, slice 232) + S (TLS-only, slice 233) 1263 1263 // + z (op-moderation, slice 235) + j (join-throttle, slice 234) + T (no-NOTICE, slice 239) 1264 - // + g (free-invite, slice 240). 1265 - assert_eq!(p[4], "psmntiOCcSzTgovqahYbeIRklj"); 1264 + // + g (free-invite, slice 240) + r (registered-only, slice 241). 1265 + assert_eq!(p[4], "psmntiOCcSzTgrovqahYbeIRklj"); 1266 1266 } 1267 1267 1268 1268 #[test]
+7 -5
leveva/tests/golden_modes.rs
··· 98 98 } 99 99 100 100 /// The remaining list/flag modes end-to-end: `+e` overrides a `+b`, `+I` bypasses `+i`, 101 - /// the `MODE #c e` exception-list query, and the ripped-out `+r` reop letter rejected `472`. 101 + /// the `MODE #c e` exception-list query, and a genuinely-unknown mode letter (`+>`) rejected `472`. 102 102 #[test] 103 103 fn exception_and_invite_masks_and_server_only_flags() { 104 104 let _srv = boot(); ··· 133 133 alice.send("MODE #rust e"); 134 134 let except_list = alice.read_until(" 349 "); 135 135 136 - // 4. The removed +r reop letter names no mode → 472. (`+a` is now the admin membership 137 - // mode, slice 158, so it is no longer a 472.) 138 - alice.send("MODE #rust +r"); 136 + // 4. A genuinely-unknown mode letter names no mode → 472. `>` is charybdis's non-existent 137 + // example-mode letter and is never a real mode (the old `+r` reop letter used here was 138 + // claimed for the registered-only mode in slice 241, exactly the "never assert on a real 139 + // unused letter" hazard). 140 + alice.send("MODE #rust +>"); 139 141 let anon_472 = alice.read_until(" 472 "); 140 142 141 143 let mut transcript = String::new(); ··· 154 156 keep(&carol_admitted, " 366 ", "carol in(+I): "); 155 157 keep(&except_list, " 348 ", "except 348: "); 156 158 keep(&except_list, " 349 ", "except 349: "); 157 - keep(&anon_472, " 472 ", "alice +r 472: "); 159 + keep(&anon_472, " 472 ", "alice +> 472: "); 158 160 159 161 insta::assert_snapshot!(transcript); 160 162 }
+67
leveva/tests/golden_regonly.rs
··· 1 + //! Boot-level golden: the **`+r` registered-only-join** channel mode (charybdis 2 + //! `MODE_REGONLY`) end-to-end through the real `leveva` binary (P11 slice 241). 3 + //! 4 + //! `alice` registers, creates `#reg` (so she is its chanop), and sets `MODE #reg +r`. Then: 5 + //! 6 + //! - `bob` (a plain, signed-out client) is rejected with `477 ERR_NEEDREGGEDNICK` and never 7 + //! joins — a not-identified client cannot enter a `+r` channel; 8 + //! - `alice` clears `-r`, after which `bob` JOINs cleanly — the inverse. 9 + //! 10 + //! The signed-**in** admit path needs a services account (SASL), so it is covered by the 11 + //! `channel` unit + `regonly_proptest` tests where an account is synthesized directly; the 12 + //! golden exercises the signed-out rejection + the `-r` reopen, which need no account. 13 + //! 14 + //! Host/SID are fixed (ident off), so the gate fires deterministically — no canonicalizer. 15 + 16 + mod harness; 17 + use harness::{boot, Client}; 18 + 19 + /// Register a plain client (`NICK`/`USER`), reading through the `422` (no-MOTD) sentinel 20 + /// that ends the welcome burst. 21 + fn register(c: &mut Client, nick: &str) { 22 + c.send(&format!("NICK {nick}")); 23 + c.send(&format!("USER {nick} 0 * :{nick}")); 24 + c.read_until(" 422 "); 25 + } 26 + 27 + #[test] 28 + fn registered_only_channel_rejects_signed_out_then_reopens() { 29 + let _srv = boot(); 30 + 31 + // alice: creator of #reg, sets +r. 32 + let mut alice = Client::connect(); 33 + register(&mut alice, "alice"); 34 + alice.send("JOIN #reg"); 35 + alice.read_until(" 366 "); 36 + alice.send("MODE #reg +r"); 37 + assert!( 38 + alice.read_until(" MODE #reg +r").contains(" MODE #reg +r"), 39 + "the +r change is echoed" 40 + ); 41 + 42 + // bob: a plain signed-out client — rejected with 477, never joins (PING barrier so a 43 + // regression that wrongly admitted bob can't hang the read). 44 + let mut bob = Client::connect(); 45 + register(&mut bob, "bob"); 46 + bob.send("JOIN #reg"); 47 + bob.send("PING barrier1"); 48 + let denied = bob.read_until("PONG"); 49 + assert!( 50 + denied.contains(" 477 "), 51 + "signed-out JOIN of +r gets 477: {denied:?}" 52 + ); 53 + assert!( 54 + !denied.contains(" JOIN #reg"), 55 + "signed-out client must NOT have joined: {denied:?}" 56 + ); 57 + 58 + // Inverse: alice clears -r (barrier on the echo), then bob joins cleanly. 59 + alice.send("MODE #reg -r"); 60 + alice.read_until(" MODE #reg -r"); 61 + bob.send("JOIN #reg"); 62 + let allowed = bob.read_until(" 366 "); 63 + assert!( 64 + allowed.contains(" JOIN #reg") && allowed.contains(" 366 "), 65 + "after -r a signed-out client joins: {allowed:?}" 66 + ); 67 + }
+16 -22
leveva/tests/modes_proptest.rs
··· 80 80 ChanMode::OperOnly, 81 81 ChanMode::NoNotice, 82 82 ChanMode::FreeInvite, 83 + ChanMode::RegisteredOnly, 83 84 ]; 84 85 85 86 /// The mask pool, shared by every list mode (`b`/`e`/`I`/`R`). ··· 122 123 mi: usize, 123 124 add: bool, 124 125 }, 125 - /// A client `±a`/`±r` op — `ar` false = `a`, true = `r`. These letters were **removed** 126 - /// (the slice-77 rework): they now name no mode, so any add or clear is rejected `472` 127 - /// and the rendered flags must never contain them. 128 - AnonReop { 126 + /// A client `±a` op (no nick parameter). `a` is the Admin **membership** mode (slice 158), 127 + /// not a flag: a bare `MODE #x +a`/`-a` with no target nick names no flag, so it must never 128 + /// render in the channel **flag** string. (Since slice 241 the sibling letter `r` is the 129 + /// real `RegisteredOnly` flag, exercised via `FLAGS`; only the bare-membership `a` case is 130 + /// guarded here.) 131 + BareAdmin { 129 132 who: bool, 130 - ar: bool, 131 133 add: bool, 132 134 }, 133 135 OpBob { ··· 165 167 any::<bool>() 166 168 ) 167 169 .prop_map(|(who, li, mi, add)| Mop::ListMask { who, li, mi, add }), 168 - (any::<bool>(), any::<bool>(), any::<bool>()).prop_map(|(who, ar, add)| Mop::AnonReop { 169 - who, 170 - ar, 171 - add 172 - }), 170 + (any::<bool>(), any::<bool>()).prop_map(|(who, add)| Mop::BareAdmin { who, add }), 173 171 (any::<bool>(), any::<bool>()).prop_map(|(who, add)| Mop::OpBob { who, add }), 174 172 (any::<bool>(), any::<bool>()).prop_map(|(who, add)| Mop::VoiceBob { who, add }), 175 173 ] ··· 239 237 BANS[*mi] 240 238 ), 241 239 ), 242 - Mop::AnonReop { who, ar, add } => ( 240 + Mop::BareAdmin { who, add } => ( 243 241 *who, 244 - format!( 245 - "MODE #x {}{}\r\n", 246 - if *add { '+' } else { '-' }, 247 - if *ar { 'r' } else { 'a' } 248 - ), 242 + format!("MODE #x {}a\r\n", if *add { '+' } else { '-' }), 249 243 ), 250 244 Mop::OpBob { who, add } => ( 251 245 *who, ··· 296 290 list.retain(|m| *m != mask); 297 291 } 298 292 } 299 - // Removed letters: `a`/`r` name no mode, so the model tracks nothing — the 300 - // post-script flag assertion proves they never render. 301 - Mop::AnonReop { .. } => {} 293 + // Bare `a` (no nick) is a membership op with no target → tracks no flag; the 294 + // post-script flag assertion proves it never renders in the flag string. 295 + Mop::BareAdmin { .. } => {} 302 296 Mop::OpBob { add, .. } => model.bob_op = *add, 303 297 Mop::VoiceBob { .. } => {} // voice tracked by the server; not asserted here 304 298 } ··· 320 314 prop_assert_eq!(&modes, &want_modes, "mode string drifted from model"); 321 315 prop_assert_eq!(&params, &want_params.join(" "), "mode params drifted from model"); 322 316 323 - // --- the removed a/r letters never render (any add or clear is 472) --- 317 + // --- a bare membership `a` (no nick) never leaks into the flag string --- 324 318 prop_assert_eq!( 325 - modes.contains('a') || modes.contains('r'), 319 + modes.contains('a'), 326 320 false, 327 - "the ripped-out +a/+r letters must never appear: {}", &modes 321 + "the membership letter `a` must never appear in the flag string: {}", &modes 328 322 ); 329 323 330 324 // --- ban list == model (order preserved) ---
+56
leveva/tests/regonly_proptest.rs
··· 1 + //! Property-based fuzzing of the `+r` (registered-only join) decision 2 + //! ([`leveva::channel::regonly_blocks`], P11 slice 241) — the pure predicate 3 + //! [`leveva::channel::Channels::check_join`] reads to decide whether a `+r` channel refuses a 4 + //! joiner. 5 + //! 6 + //! The function must be **total** and agree with an independent specification model: a join is 7 + //! blocked iff the channel is `+r` (`regonly_set`) **and** the joiner has no services account 8 + //! (`!has_account`). An account always admits; a channel without `+r` admits anyone. 9 + 10 + use leveva::channel::regonly_blocks; 11 + use proptest::prelude::*; 12 + 13 + /// Reference specification, independently coded. 14 + fn model(regonly_set: bool, has_account: bool) -> bool { 15 + regonly_set && !has_account 16 + } 17 + 18 + #[test] 19 + fn fixed_cases_match_the_model() { 20 + for &(r, a) in &[(true, true), (true, false), (false, true), (false, false)] { 21 + assert_eq!( 22 + regonly_blocks(r, a), 23 + model(r, a), 24 + "mismatch on (+r={r}, has_account={a})" 25 + ); 26 + } 27 + } 28 + 29 + proptest! { 30 + /// Total + agrees with the model on every combination. 31 + #[test] 32 + fn matches_model(regonly_set in any::<bool>(), has_account in any::<bool>()) { 33 + prop_assert_eq!(regonly_blocks(regonly_set, has_account), model(regonly_set, has_account)); 34 + } 35 + 36 + /// A signed-in client (has an account) is never blocked, with or without `+r`. 37 + #[test] 38 + fn an_account_always_admits(regonly_set in any::<bool>()) { 39 + prop_assert!(!regonly_blocks(regonly_set, true)); 40 + } 41 + 42 + /// A channel without `+r` never blocks, signed in or out. 43 + #[test] 44 + fn no_regonly_never_blocks(has_account in any::<bool>()) { 45 + prop_assert!(!regonly_blocks(false, has_account)); 46 + } 47 + 48 + /// The only block is a signed-out joiner on a `+r` channel. 49 + #[test] 50 + fn block_implies_regonly_and_no_account(regonly_set in any::<bool>(), has_account in any::<bool>()) { 51 + if regonly_blocks(regonly_set, has_account) { 52 + prop_assert!(regonly_set); 53 + prop_assert!(!has_account); 54 + } 55 + } 56 + }
+2 -2
leveva/tests/snapshots/golden_cap__cap_302_negotiation_suspends_then_resumes.snap
··· 7 7 :leveva.test 001 dan :Welcome to the Internet Relay Network dan!d@127.0.0.1 8 8 :leveva.test 002 dan :Your host is leveva.test, running version leveva-<VER> 9 9 :leveva.test 003 dan :This server was created <MASKED> 10 - :leveva.test 004 dan leveva.test leveva-<VER> oOiwraWBxsgGZ psmntiOCcSzTgovqahYbeIRklj 11 - :leveva.test 005 dan CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTg PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 10 + :leveva.test 004 dan leveva.test leveva-<VER> oOiwraWBxsgGZ psmntiOCcSzTgrovqahYbeIRklj 11 + :leveva.test 005 dan CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTgr PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 12 12 :leveva.test 005 dan BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 13 13 :leveva.test 251 dan :There are 1 users and 0 services on 1 servers 14 14 :leveva.test 253 dan 1 :unknown connection(s)
+2 -2
leveva/tests/snapshots/golden_cap__cap_ls_without_version_then_end.snap
··· 6 6 :leveva.test 001 jo :Welcome to the Internet Relay Network jo!j@127.0.0.1 7 7 :leveva.test 002 jo :Your host is leveva.test, running version leveva-<VER> 8 8 :leveva.test 003 jo :This server was created <MASKED> 9 - :leveva.test 004 jo leveva.test leveva-<VER> oOiwraWBxsgGZ psmntiOCcSzTgovqahYbeIRklj 10 - :leveva.test 005 jo CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTg PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 9 + :leveva.test 004 jo leveva.test leveva-<VER> oOiwraWBxsgGZ psmntiOCcSzTgrovqahYbeIRklj 10 + :leveva.test 005 jo CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTgr PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 11 11 :leveva.test 005 jo BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 12 12 :leveva.test 251 jo :There are 1 users and 0 services on 1 servers 13 13 :leveva.test 253 jo 1 :unknown connection(s)
+1 -1
leveva/tests/snapshots/golden_extban__extban_join_gate_end_to_end.snap
··· 2 2 source: leveva/tests/golden_extban.rs 3 3 expression: transcript 4 4 --- 5 - 005 EXTBAN: :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTg PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 5 + 005 EXTBAN: :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTgr PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 6 6 set $r ban: :alice!alice@127.0.0.1 MODE #real +b $r:*EVIL* 7 7 carol 474: :leveva.test 474 carol #real :Cannot join channel (+b) 8 8 dave 366: :leveva.test 366 dave #real :End of /NAMES list
+1 -1
leveva/tests/snapshots/golden_modes__exception_and_invite_masks_and_server_only_flags.snap
··· 8 8 carol in(+I): :leveva.test 366 carol #rust :End of /NAMES list 9 9 except 348: :leveva.test 348 alice #rust bob!*@* 10 10 except 349: :leveva.test 349 alice #rust :End of channel exception list 11 - alice +r 472: :leveva.test 472 alice r :is unknown mode char to me for #rust 11 + alice +> 472: :leveva.test 472 alice > :is unknown mode char to me for #rust
+1 -1
leveva/tests/snapshots/golden_ojoin__ojoin_official_join_end_to_end.snap
··· 2 2 source: leveva/tests/golden_ojoin.rs 3 3 expression: transcript 4 4 --- 5 - 005 PREFIX: :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTg PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 5 + 005 PREFIX: :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTgr PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 6 6 ojoin +Yo: :leveva.test MODE #help +Yo alice alice 7 7 ojoin notice: :leveva.test NOTICE #help :alice joined #help on official network business 8 8 bob sees MODE: :leveva.test MODE #help +Yo alice alice
+2 -2
leveva/tests/snapshots/golden_pass__correct_password_welcomes.snap
··· 5 5 :leveva.test 001 alice :Welcome to the Internet Relay Network alice!alice@127.0.0.1 6 6 :leveva.test 002 alice :Your host is leveva.test, running version leveva-<VER> 7 7 :leveva.test 003 alice :This server was created <MASKED> 8 - :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZ psmntiOCcSzTgovqahYbeIRklj 9 - :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTg PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 8 + :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZ psmntiOCcSzTgrovqahYbeIRklj 9 + :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTgr PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 10 10 :leveva.test 005 alice BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 11 11 :leveva.test 251 alice :There are 1 users and 0 services on 1 servers 12 12 :leveva.test 253 alice 1 :unknown connection(s)
+2 -2
leveva/tests/snapshots/golden_registration__registration_welcome_burst.snap
··· 5 5 :leveva.test 001 alice :Welcome to the Internet Relay Network alice!alice@127.0.0.1 6 6 :leveva.test 002 alice :Your host is leveva.test, running version leveva-<VER> 7 7 :leveva.test 003 alice :This server was created <MASKED> 8 - :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZ psmntiOCcSzTgovqahYbeIRklj 9 - :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTg PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 8 + :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZ psmntiOCcSzTgrovqahYbeIRklj 9 + :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTgr PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 10 10 :leveva.test 005 alice BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 11 11 :leveva.test 251 alice :There are 1 users and 0 services on 1 servers 12 12 :leveva.test 253 alice 1 :unknown connection(s)
+1 -1
leveva/tests/snapshots/golden_server_query__server_query_target_resolution.snap
··· 3 3 expression: transcript 4 4 --- 5 5 VER: :leveva.test 351 alice leveva-<VER> leveva.test :https://tangled.org/xeiaso.net/ircd.rs 6 - VER: :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTg PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 6 + VER: :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTgr PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 7 7 VER: :leveva.test 005 alice BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 8 8 VER-LOCAL: :leveva.test 351 alice leveva-<VER> leveva.test :https://tangled.org/xeiaso.net/ircd.rs 9 9 VER-402: :leveva.test 402 alice nope.invalid :No such server
+1 -1
leveva/tests/snapshots/golden_setname__setname_changes_realname_and_notifies_only_cap_clients.snap
··· 5 5 # CAP LS advertises setname 6 6 :leveva.test CAP * LS :cap-notify multi-prefix message-tags away-notify account-notify invite-notify echo-message server-time userhost-in-names draft/oper-tag draft/message-redaction standard-replies chghost no-implicit-names extended-monitor draft/pre-away setname account-tag extended-join labeled-response batch draft/metadata-2=max-subs=30,max-keys=30,max-value-bytes=300 7 7 # registration 005 advertises NAMELEN 8 - :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTg PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 8 + :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTgr PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 9 9 # alice self-echo (she negotiated setname) 10 10 :alice!alice@127.0.0.1 SETNAME :Bruce Wayne 11 11 # dan (setname) receives the SETNAME push
+2 -2
leveva/tests/snapshots/golden_user_spaceless__spaceless_user_realname_welcome_burst.snap
··· 5 5 :leveva.test 001 alice :Welcome to the Internet Relay Network alice!alice@127.0.0.1 6 6 :leveva.test 002 alice :Your host is leveva.test, running version leveva-<VER> 7 7 :leveva.test 003 alice :This server was created <MASKED> 8 - :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZ psmntiOCcSzTgovqahYbeIRklj 9 - :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTg PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 8 + :leveva.test 004 alice leveva.test leveva-<VER> oOiwraWBxsgGZ psmntiOCcSzTgrovqahYbeIRklj 9 + :leveva.test 005 alice CASEMAPPING=rfc1459 CHANTYPES=# CHANMODES=beIR,k,lj,psmntiOCcSzTgr PREFIX=(Yqaohv)!~&@%+ NICKLEN=15 CHANNELLEN=50 TOPICLEN=255 NAMELEN=50 LINELEN=2048 MODES=8 EXCEPTS=e INVEX=I EXTBAN=$,acjmorsxz :are supported by this server 10 10 :leveva.test 005 alice BOT=B WHOX MONITOR=100 UTF8ONLY CALLERID=g ELIST=CMNTU SAFELIST NETWORK=TestNet :are supported by this server 11 11 :leveva.test 251 alice :There are 1 users and 0 services on 1 servers 12 12 :leveva.test 253 alice 1 :unknown connection(s)