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): OPME — oper self-op of an opless channel (P11 slice 292)

+699 -4
+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: 291 slices** (291 = FINDFORWARDS: charybdis-style reverse `+f` lookup (`extensions/m_findforwards.c`) — `FINDFORWARDS <channel>` lists every channel whose `+f` forward target (slice 242) is `<channel>`, the companion to the `+f` forwarding family (where `+f` points a *source* at a *target*, FINDFORWARDS asks the target "who points at me?"); new pure read seam `Channels::forwards_to(target)` (folds + scans the channel table, returns matching display names folded-sorted — the fuzz seam), `command/findforwards.rs` with the trace/ban-family gate ordering (`461` no-arg first → `403` nonexistent channel → `442` non-member-non-oper, the oper-bit bypass replacing charybdis's `IsOperSpy` à la CHANTRACE), output as charybdis-faithful `NOTICE` lines (chunked space-separated forwarder list + `End of forwards for <chan>`, or `No channels forward to <chan>` when empty) **not** numerics, `FINDFORWARDS.md` help + COMMANDS allowlist + index.md; leveva-native (no oracle — 2.11 has no FINDFORWARDS), local-only (a read of this server's channel table, mirroring charybdis's per-server `+f` state); `forwards_to` unit + 8 command units + `findforwards_proptest` (2 props: `forwards_to_is_exactly_the_pointing_channels` model set-equality + `arbitrary_args_never_panic`) + `golden_findforwards`; 290 = REMOVE: charybdis-style chanop force-part (`extensions/m_remove.c`) — a channel operator forcibly removes a member who, on the wire, appears to **PART** rather than be **KICK**ed (so client auto-rejoin-on-kick scripts don't fire); `command/remove.rs` reuses `Channels::kick_gate`/`kick` verbatim so it **inherits every KICK rule** (`403`/`442`/`482` chanop gate, `401`/`441`, elemental rank protection, `+Y` official-join immunity, cross-product, `461`), but emits `:victim!user@host PART <chan> :requested by <remover> [(<reason>)]` (prefix = the **victim's** own mask via `Registry::record_of`, works local+remote) echoed to the remover + delivered to all local members incl. the victim, propagated S2S as the victim's PART (`s2s::relay::local_part`) and recorded to chat-history as a PART; a normal chanop command (no oper privilege), the sibling of KICK/the SA*-force family; leveva-native (the C oracle has no REMOVE) → no differential, the "requested by" wording is leveva's; `REMOVE.md` help + COMMANDS allowlist + index.md; `golden_remove` + `remove_proptest` (2 props); 289 = XLINE/UNXLINE: charybdis realname (gecos) operator ban — the realname sibling of K-line/D-line, completing the runtime ban quartet **K/D/X/RESV**; bolt-for-bolt the DLINE plane (slice 284) with a single-token gecos matcher (`matching::matches`) instead of CIDR — `leveva::xline::{Xline,XlineStore}` (SQLite write-through + boot reload), `command/xline.rs` (461→`OperPrivilege::Xline` 481, reap local matching realnames with 465+ERROR), `s2s/xline.rs` `ENCAP * XLINE/UNXLINE` propagation + `xline_burst` re-assertion, registration X-line gate in `finalize_registration` after the K-line/config-`ban` gates, `STATS x`→`247 RPL_STATSXLINE`; **lights up the dead `xline_exempt` flag** end-to-end (`Admission`/`ClientRecord.xline_exempt` + `set_xline_exempt`, the dedicated exemption distinct from `kline-exempt`); divergences = single-token-glob mask (vs charybdis spaced gecos) + dedicated xline-exempt + leveva-native no-differential; `golden_xline` + `xline_proptest` (6 props); 288 = TESTMASK: `TESTMASK <[nick!]user@host> [<gecos>]` — a charybdis-style read-only operator diagnostic that **counts** how many connected clients match a hostmask (and optional realname glob), split into local vs remote; the population sibling of TESTLINE (285, which reports *bans*), together completing charybdis's `test*` diagnostic family. `461` param gate before the plain oper-bit gate (`481`); a malformed mask (no `@`, or empty user/host) → `NOTICE :Invalid parameters` (faithful, not a numeric); a registry sweep glob-matches `nick`/`user`/`host`(+`orighost`)/`gecos` (nick & gecos default `*`) and tallies `is_local_uid` → one `727 RPL_TESTMASKGECOS` `<lcount> <gcount> <nick>!<user>@<host> <gecos> :Local/remote clients match`; new numeric 727 (charybdis's 724 is dead — no format/caller), pure `parse_mask` fuzz seam, `TESTMASK.md` help; leveva-native, local-only (no S2S, mirroring charybdis); confirmed against cloned charybdis `m_testmask.c`/`messages.h`; 287 = CHANTRACE: `CHANTRACE <#channel>` — the channel-scoped sibling of TRACE (26)/ETRACE/MASKTRACE (286), completing the trace quartet: reports every member of a named channel in ETRACE's extended `708 RPL_ETRACEFULL` column format, folded-nick order, closing `262 RPL_TRACEEND`; gate is `461` (missing channel) → `403` (no such channel, existence before membership) → `442` unless the requester is a member **or** holds `OperPrivilege::Trace` (the operspy-equivalent bypass; leveva has no operspy `!` prefix); reuses `etrace::etrace_line`, no new numeric; `CHANTRACE.md` help; leveva-native (2.11 has no CHANTRACE), reports the **full roster incl. remote members** (unlike local-only ETRACE/MASKTRACE) but no S2S propagation, no IP-hiding (host==host, no separate IP column); 286 = MASKTRACE: `MASKTRACE <nick!user@host mask> [<gecos mask>]` — a charybdis-style mask-filtered extended trace, the sibling of TRACE (26) and ETRACE: reports every local client whose `nick!user@host` glob-matches (and optionally whose realname matches a 2nd gecos glob) in ETRACE's extended column format; `461` param gate before the `OperPrivilege::Trace` oper gate (`481`), one reused `708 RPL_ETRACEFULL` line per match in folded-nick order via `matching::HostMask` + the promoted `etrace::etrace_line`, closing with `262 RPL_TRACEEND` (no new numeric); `MASKTRACE.md` help; leveva-native (2.11 has no MASKTRACE), local-only (no S2S); 285 = TESTLINE: `TESTLINE <[nick!]user@host | ip/cidr | nick | #channel>` — a charybdis-style **read-only** operator diagnostic, the capstone of the ban family (RESV 272 / TKLINE 281 / DLINE 284): it reports which **active** ban would match a mask, laying/lifting nothing. Param gate (`461`) then a plain oper-bit gate (any oper, no per-ban privilege → `481`); a pure `probe()` classifier routes by mask shape (`user@host`→D-line-then-K-line, `#channel`→channel RESV, bare token→D-line-as-IP-then-nick-RESV) and reports the first hit in priority **D > K > R** as `725 RPL_TESTLINE` (`<type K/D/R> <minutes-remaining, 0 for permanent RESV> <ban-mask> :<reason>`), else `726 RPL_NOTESTLINE`; expiry honoured via `find_active`; new numerics 725/726, `TESTLINE.md` help; leveva-native (no oracle — 2.11 has no TESTLINE), single-best-match, no I-line arm, **local-only** (no S2S, mirroring charybdis); 284 = D-lines: `DLINE <duration> <ip/cidr> [:reason]` / `UNDLINE <ip/cidr>` — charybdis-style IP-level operator bans, the IP/CIDR sibling of the temporary K-line family (281–283); new `DlineStore` (mirrors `KlineStore`, CIDR-aware `host_component_matches`, same `database {}` SQLite write-through + boot reload), `OperPrivilege::Dline` (bit `0x400000`), registration gate **before** the K-line gate matching the pre-cloak connect IP (`465`+`ERROR`+REJ snomask, exemption reuses `kline-exempt`), local reap by `orighost`, S2S `ENCAP * DLINE`/`UNDLINE` propagation + burst re-assertion (slice-281 KLINE shape), `STATS d` → `250 RPL_STATSDLINE`, `DLINE.md`/`UNDLINE.md` help; leveva-native, no oracle differential; 283 = SQLite persistence for permanent (`+P`) channels — a `+P` channel (slice 244, survives empty) now also survives a **restart**, persisting to the same `database {}` SQLite file that backs chat-history (274) and K-lines/reservations (282): its identity + `created_at` TS + full `ChannelModes` + topic write through to a `channel` table (JSON `modes`/`topic` columns, the serde codec chat-history uses) on every durable mutation, via the single `Channels::reconcile_persist` chokepoint (upsert when `+P`, **delete on `-P`**) hooked into all eight durable-state mutators — `apply_modes`/`apply_modes_oper`/`apply_modes_as_server` (MODE incl. the `+P`/`-P` toggle, key, limit, all `+b/+e/+I/+R` lists), `reset_flag_state`/`wipe_list_masks` (CHANTS-merge), `set_topic`/`set_topic_as_server` (TOPIC), and `create_permanent_from_burst` (an empty `+P` channel learned from a peer burst persists so it outlives *this* server's restart too, mirroring 282's inbound-S2S persist); recreated **empty** at boot (members/INVITE-tokens/join-throttle counters never persist — an empty `+P` channel is exactly what 244/269 make valid), with `created_at` carried so TS6 merge arbitration is stable post-reboot; a `persisted: HashSet` of folded keys makes a `reconcile_persist` on a never-persisted (non-`+P`) channel a pure in-memory miss — **no disk write per MODE on an ordinary channel** — and the two leaf locks (`db`/`persisted`) are never held simultaneously so there is no lock-order hazard with the held `by_name` guard; a `new()`/no-`database{}` table stays in-memory only (every persistence path a no-op); `ChannelModes`/`Topic` gained `#[derive(Serialize, Deserialize)]`; `from_config` opens the table against `cfg.database.path` (log + fall back to in-memory on error, never fatal), exactly the 282 klines/resvs shape; 282 = SQLite persistence for K-lines + reservations — operator-laid `TKLINE` bans and `RESV` reservations (lost on restart before) now persist to the **same `database {}` SQLite file** that backs chat-history (slice 274), so they survive a reboot: each `KlineStore`/`ResvStore` gains an optional write-through connection (`open(path)` creates a `kline`/`resv` table + loads existing rows; `add`/`remove`/`prune` mirror to it; a `new()`/no-`database{}` store stays in-memory only, every persistence path a no-op), the in-memory `Vec` staying the hot-path authority; K-lines drop `expires <= now` rows on load + prune; only **runtime** (`conf == false`) reservations persist (config `resv {}` re-derives from `ircd.kdl`, so `seed_config` bypasses persistence and a `REHASH` never drops a learned reservation); `COLLATE NOCASE` primary keys make a case-differing re-add upsert not duplicate; open/write errors log and fall back to in-memory (never fatal); inbound S2S `apply_encap_kline`/`apply_encap_resv` persist what a server learns from the network too (all via `add`); 281 = S2S `KLINE`/`UNKLINE` propagation — the K-line analogue of slice 280: a `TKLINE`/`UNTKLINE` (leveva's temporary K-line family, there is no separate permanent `KLINE`) now propagates network-wide as `ENCAP * KLINE <duration> <user> <host> :<reason>`/`ENCAP * UNKLINE <user> <host>` (server-prefixed, leveva-native, the **remaining** seconds on the wire so each server re-clocks `expires = now + duration` against its own clock — no absolute-timestamp skew), wired into `command/tkline.rs` after the param/privilege/format gates; every server applies the ban to its own `KlineStore` (gating future registrations) **and reaps its own local matching clients** via the now-`is_local_uid`-guarded `reap_matching` helper (which also fixed a latent over-reap of remote users in the local `TKLINE`); `kline-exempt` clients are spared; `UNKLINE` lifts the gate only (never un-kills) and propagates unconditionally; re-asserted to a freshly-linked peer by `s2s/burst.rs::kline_burst` (active bans only, with remaining time; expired bursts nothing); new `s2s/kline.rs` modelled on `s2s/resv.rs`, ENCAP dispatch in `s2s/forward.rs`; 280 = S2S `RESV`/`UNRESV` propagation — an operator-laid nick/channel reservation (and its lift) now propagates network-wide as `ENCAP * RESV`/`UNRESV` (server-prefixed, kind re-derived from the mask prefix on inbound apply), wired into `command/resv.rs` after the param/privilege gates (a `461`/`481` reject propagates nothing; `UNRESV` propagates unconditionally), applied + split-horizon-relayed by `s2s/forward.rs`, and re-asserted to a freshly-linked peer via `s2s/burst.rs::resv_burst` (runtime `conf == false` reservations only — config `resv {}` blocks stay per-server local policy); closes the slice-272/273 S2S-propagation follow-on, mirroring the METADATA/KNOCK ENCAP carriers; 279 = remote-originated JOIN/PART/QUIT membership events are now recorded into chat-history — the membership-event parallel to slice 276 (which did PRIVMSG/NOTICE): a **live** `NJOIN` (`s2s/njoin.rs`, never a burst — bursts are bulk state-sync), a bare `JOIN`/`PART` (`s2s/relay.rs`), and a **discrete** remote `QUIT`/`KILL` (`s2s/squit.rs::quit_one_remote_user`, *not* a netsplit mass teardown) each record an event keyed by a locally-minted msgid + clock, gated on `has_local_member` (only a local member can `CHATHISTORY` it; a pure hub stores nothing); an unmirrored `NJOIN`-only ghost has no mask so its QUIT is skipped; replay/query side unchanged (slice 274 already gates these behind `draft/event-playback`); 278 = `+R` auto-reop is now enforced for **local** members only — the `enforce_reop` chokepoint gained an explicit `is_local_uid` filter, so a `+R` mask matching a `UNICK`-introduced remote user's `nick!user@host` no longer wrongly ops it locally and relays a `+o` we have no authority to emit (the locality guarantee previously rested on the false premise that remote members lack a registry record); 277 = netsplit batches are no longer duplicated to `batch`-capable clients under concurrent teardown — when a dying peer is detected on two paths at once (abrupt socket drop + a sibling's inbound `SQUIT`, in separate tasks), the `NJOIN`-only ghost-member sweep raced (`co_members` then `remove_everywhere`, no single-shot gate) and double-relayed a casualty's `QUIT`; new atomic `Channels::detach_member` fuses recipient-gather + removal in one critical section so only the first teardown to reach a member relays it (the loser gets `None`), mirroring the already-atomic `remove_behind_sid` mirror drain; new `s2s_netsplit_concurrency_proptest` fuzz; 276 = remote-originated channel messages are now recorded into chat-history — the S2S relay's inbound channel fan (`s2s::relay::inbound_message`) records the delivered line on this server too, keyed by the carried network msgid (slice 130) + `@time` (slice 134), gated on this server having a local member of the channel (only a local member can `CHATHISTORY` it; no whole-network accumulation on a hub); the `+z` op-redirect/non-channel branches never record, mirroring the local plane; closes the slice-274 gap where `CHATHISTORY` held only locally-sent lines; 275 = IRCv3 `message-redaction` now deletes from chat-history — an accepted channel `REDACT <chan> <msgid>` deletes the slice-274 stored line (`History::delete_by_msgid`, keyed `(target, msgid)`) so `CHATHISTORY` can never replay it; wired in both the local `CanSend::Ok` path and inbound `ENCAP * REDACT`, per-server; mirrors recording — a `+z` op-moderated/"shown to ops" attempt is never recorded so never deleted; authorization stays a stateless relay; 274 = IRCv3 `draft/chathistory` — SQLite-backed (`rusqlite`, bundled) channel message history behind an optional `ircd.kdl` `database {}` block; all channels auto-enrolled (PRIVMSG/NOTICE + JOIN/PART/QUIT recorded at the delivery plane), joining clients auto-replayed `replay-lines` recent lines unless they negotiated `draft/chathistory` (then exempt, per spec), full `CHATHISTORY LATEST/BEFORE/AFTER/BETWEEN/AROUND/TARGETS` (timestamp=/msgid= selectors) framed as a `chathistory` batch, `draft/chathistory`+`draft/event-playback` dynamic caps + `CHATHISTORY`/`MSGREFTYPES` ISUPPORT, age-based `retention-days` prune ticker; `Message` gained serde; 273 = registration-time nick `RESV` enforcement — a connection may no longer *register* directly as a reserved nick, only the post-registration `NICK`/JOIN were gated before; refuse `432` + reopen registration, mirroring the `433` nick-in-use path; 272 = charybdis `RESV`/`UNRESV` nick/channel reservation — JOIN→`437`/NICK→`432` gates, opers exempt, new `OperPrivilege::Resv`; defaults declared in `resv {}` config blocks, boot-seeded + REHASH-re-seeded keeping runtime `RESV`s). The slices span the full client command surface (registration; channels + the complete channel-mode surface; WHO/WHOIS/WHOX; OPER/STATS; and the query/util commands incl. OPERWALL/LOCOPS/USERIP); 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; CertFP; the **IRCv3 cap 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 **complete charybdis channel-mode set** (`+C/+c/+S/+j/+z/+T/+g/+r/+f/+Q/+F/+P/+L`, with `+L` the last standard one) and **user-mode set** (`+D/+R/+Q/+G/+z/+S/+l`, plus the product decision that leveva has no local-only umodes — `SEND_UMODES` is every umode); and the `+s` server-notice **snomask** (a charybdis category mask with a producer wired for every category — the matrix is complete). Most const limits (`max-penalty`/`max-bans`/`service-string`/`default-snomask`/…) have been promoted to REHASH-able `options { … }` knobs. **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: 292 slices** (292 = OPME: charybdis-style operator self-op of an *opless* channel (`extensions/m_opme.c`) — `OPME <channel>` grants the requesting operator `+o` **only** when the channel currently has no operators, the recovery tool for a channel whose last op left and the constrained, safety-gated sibling of `SAMODE +o`; completes the charybdis oper channel family (`SAJOIN`/`SAPART`/`SANICK`/`SAMODE`/`OJOIN`) with the recovery verb; new atomic seam `Channels::opme(name, uid) -> OpmeOutcome` (single-lock channel-missing→`NoSuchChannel` / non-member→`NotOnChannel` / has-an-op→`NotOpless` / else grant `+o`→`Opped`, the fuzz seam, no persistence — member status never persists), `command/opme.rs` gate ladder (non-oper `481` → no-arg `461` → `403`/`442`/not-opless `NOTICE`/success), on success broadcasts `:<server> MODE <chan> +o <nick>` to local members + relays server-sourced via `s2s::relay::server_channel_mode` + posts a `+s` audit snomask `<oper> used OPME on <chan>`; divergences = leveva-native (no oracle), membership-checked-before-opless (vs charybdis opless-first, so a non-member never learns op state), `+s` audit snomask not `+w` wallops (SA*/OJOIN family precedent), local-only; `OPME.md` help + COMMANDS allowlist + index.md operator line; `channel::opme` unit + 4 command units + `opme_proptest` (2 props) + `golden_opme`; 291 = FINDFORWARDS: charybdis-style reverse `+f` lookup (`extensions/m_findforwards.c`) — `FINDFORWARDS <channel>` lists every channel whose `+f` forward target (slice 242) is `<channel>`, the companion to the `+f` forwarding family (where `+f` points a *source* at a *target*, FINDFORWARDS asks the target "who points at me?"); new pure read seam `Channels::forwards_to(target)` (folds + scans the channel table, returns matching display names folded-sorted — the fuzz seam), `command/findforwards.rs` with the trace/ban-family gate ordering (`461` no-arg first → `403` nonexistent channel → `442` non-member-non-oper, the oper-bit bypass replacing charybdis's `IsOperSpy` à la CHANTRACE), output as charybdis-faithful `NOTICE` lines (chunked space-separated forwarder list + `End of forwards for <chan>`, or `No channels forward to <chan>` when empty) **not** numerics, `FINDFORWARDS.md` help + COMMANDS allowlist + index.md; leveva-native (no oracle — 2.11 has no FINDFORWARDS), local-only (a read of this server's channel table, mirroring charybdis's per-server `+f` state); `forwards_to` unit + 8 command units + `findforwards_proptest` (2 props: `forwards_to_is_exactly_the_pointing_channels` model set-equality + `arbitrary_args_never_panic`) + `golden_findforwards`; 290 = REMOVE: charybdis-style chanop force-part (`extensions/m_remove.c`) — a channel operator forcibly removes a member who, on the wire, appears to **PART** rather than be **KICK**ed (so client auto-rejoin-on-kick scripts don't fire); `command/remove.rs` reuses `Channels::kick_gate`/`kick` verbatim so it **inherits every KICK rule** (`403`/`442`/`482` chanop gate, `401`/`441`, elemental rank protection, `+Y` official-join immunity, cross-product, `461`), but emits `:victim!user@host PART <chan> :requested by <remover> [(<reason>)]` (prefix = the **victim's** own mask via `Registry::record_of`, works local+remote) echoed to the remover + delivered to all local members incl. the victim, propagated S2S as the victim's PART (`s2s::relay::local_part`) and recorded to chat-history as a PART; a normal chanop command (no oper privilege), the sibling of KICK/the SA*-force family; leveva-native (the C oracle has no REMOVE) → no differential, the "requested by" wording is leveva's; `REMOVE.md` help + COMMANDS allowlist + index.md; `golden_remove` + `remove_proptest` (2 props); 289 = XLINE/UNXLINE: charybdis realname (gecos) operator ban — the realname sibling of K-line/D-line, completing the runtime ban quartet **K/D/X/RESV**; bolt-for-bolt the DLINE plane (slice 284) with a single-token gecos matcher (`matching::matches`) instead of CIDR — `leveva::xline::{Xline,XlineStore}` (SQLite write-through + boot reload), `command/xline.rs` (461→`OperPrivilege::Xline` 481, reap local matching realnames with 465+ERROR), `s2s/xline.rs` `ENCAP * XLINE/UNXLINE` propagation + `xline_burst` re-assertion, registration X-line gate in `finalize_registration` after the K-line/config-`ban` gates, `STATS x`→`247 RPL_STATSXLINE`; **lights up the dead `xline_exempt` flag** end-to-end (`Admission`/`ClientRecord.xline_exempt` + `set_xline_exempt`, the dedicated exemption distinct from `kline-exempt`); divergences = single-token-glob mask (vs charybdis spaced gecos) + dedicated xline-exempt + leveva-native no-differential; `golden_xline` + `xline_proptest` (6 props); 288 = TESTMASK: `TESTMASK <[nick!]user@host> [<gecos>]` — a charybdis-style read-only operator diagnostic that **counts** how many connected clients match a hostmask (and optional realname glob), split into local vs remote; the population sibling of TESTLINE (285, which reports *bans*), together completing charybdis's `test*` diagnostic family. `461` param gate before the plain oper-bit gate (`481`); a malformed mask (no `@`, or empty user/host) → `NOTICE :Invalid parameters` (faithful, not a numeric); a registry sweep glob-matches `nick`/`user`/`host`(+`orighost`)/`gecos` (nick & gecos default `*`) and tallies `is_local_uid` → one `727 RPL_TESTMASKGECOS` `<lcount> <gcount> <nick>!<user>@<host> <gecos> :Local/remote clients match`; new numeric 727 (charybdis's 724 is dead — no format/caller), pure `parse_mask` fuzz seam, `TESTMASK.md` help; leveva-native, local-only (no S2S, mirroring charybdis); confirmed against cloned charybdis `m_testmask.c`/`messages.h`; 287 = CHANTRACE: `CHANTRACE <#channel>` — the channel-scoped sibling of TRACE (26)/ETRACE/MASKTRACE (286), completing the trace quartet: reports every member of a named channel in ETRACE's extended `708 RPL_ETRACEFULL` column format, folded-nick order, closing `262 RPL_TRACEEND`; gate is `461` (missing channel) → `403` (no such channel, existence before membership) → `442` unless the requester is a member **or** holds `OperPrivilege::Trace` (the operspy-equivalent bypass; leveva has no operspy `!` prefix); reuses `etrace::etrace_line`, no new numeric; `CHANTRACE.md` help; leveva-native (2.11 has no CHANTRACE), reports the **full roster incl. remote members** (unlike local-only ETRACE/MASKTRACE) but no S2S propagation, no IP-hiding (host==host, no separate IP column); 286 = MASKTRACE: `MASKTRACE <nick!user@host mask> [<gecos mask>]` — a charybdis-style mask-filtered extended trace, the sibling of TRACE (26) and ETRACE: reports every local client whose `nick!user@host` glob-matches (and optionally whose realname matches a 2nd gecos glob) in ETRACE's extended column format; `461` param gate before the `OperPrivilege::Trace` oper gate (`481`), one reused `708 RPL_ETRACEFULL` line per match in folded-nick order via `matching::HostMask` + the promoted `etrace::etrace_line`, closing with `262 RPL_TRACEEND` (no new numeric); `MASKTRACE.md` help; leveva-native (2.11 has no MASKTRACE), local-only (no S2S); 285 = TESTLINE: `TESTLINE <[nick!]user@host | ip/cidr | nick | #channel>` — a charybdis-style **read-only** operator diagnostic, the capstone of the ban family (RESV 272 / TKLINE 281 / DLINE 284): it reports which **active** ban would match a mask, laying/lifting nothing. Param gate (`461`) then a plain oper-bit gate (any oper, no per-ban privilege → `481`); a pure `probe()` classifier routes by mask shape (`user@host`→D-line-then-K-line, `#channel`→channel RESV, bare token→D-line-as-IP-then-nick-RESV) and reports the first hit in priority **D > K > R** as `725 RPL_TESTLINE` (`<type K/D/R> <minutes-remaining, 0 for permanent RESV> <ban-mask> :<reason>`), else `726 RPL_NOTESTLINE`; expiry honoured via `find_active`; new numerics 725/726, `TESTLINE.md` help; leveva-native (no oracle — 2.11 has no TESTLINE), single-best-match, no I-line arm, **local-only** (no S2S, mirroring charybdis); 284 = D-lines: `DLINE <duration> <ip/cidr> [:reason]` / `UNDLINE <ip/cidr>` — charybdis-style IP-level operator bans, the IP/CIDR sibling of the temporary K-line family (281–283); new `DlineStore` (mirrors `KlineStore`, CIDR-aware `host_component_matches`, same `database {}` SQLite write-through + boot reload), `OperPrivilege::Dline` (bit `0x400000`), registration gate **before** the K-line gate matching the pre-cloak connect IP (`465`+`ERROR`+REJ snomask, exemption reuses `kline-exempt`), local reap by `orighost`, S2S `ENCAP * DLINE`/`UNDLINE` propagation + burst re-assertion (slice-281 KLINE shape), `STATS d` → `250 RPL_STATSDLINE`, `DLINE.md`/`UNDLINE.md` help; leveva-native, no oracle differential; 283 = SQLite persistence for permanent (`+P`) channels — a `+P` channel (slice 244, survives empty) now also survives a **restart**, persisting to the same `database {}` SQLite file that backs chat-history (274) and K-lines/reservations (282): its identity + `created_at` TS + full `ChannelModes` + topic write through to a `channel` table (JSON `modes`/`topic` columns, the serde codec chat-history uses) on every durable mutation, via the single `Channels::reconcile_persist` chokepoint (upsert when `+P`, **delete on `-P`**) hooked into all eight durable-state mutators — `apply_modes`/`apply_modes_oper`/`apply_modes_as_server` (MODE incl. the `+P`/`-P` toggle, key, limit, all `+b/+e/+I/+R` lists), `reset_flag_state`/`wipe_list_masks` (CHANTS-merge), `set_topic`/`set_topic_as_server` (TOPIC), and `create_permanent_from_burst` (an empty `+P` channel learned from a peer burst persists so it outlives *this* server's restart too, mirroring 282's inbound-S2S persist); recreated **empty** at boot (members/INVITE-tokens/join-throttle counters never persist — an empty `+P` channel is exactly what 244/269 make valid), with `created_at` carried so TS6 merge arbitration is stable post-reboot; a `persisted: HashSet` of folded keys makes a `reconcile_persist` on a never-persisted (non-`+P`) channel a pure in-memory miss — **no disk write per MODE on an ordinary channel** — and the two leaf locks (`db`/`persisted`) are never held simultaneously so there is no lock-order hazard with the held `by_name` guard; a `new()`/no-`database{}` table stays in-memory only (every persistence path a no-op); `ChannelModes`/`Topic` gained `#[derive(Serialize, Deserialize)]`; `from_config` opens the table against `cfg.database.path` (log + fall back to in-memory on error, never fatal), exactly the 282 klines/resvs shape; 282 = SQLite persistence for K-lines + reservations — operator-laid `TKLINE` bans and `RESV` reservations (lost on restart before) now persist to the **same `database {}` SQLite file** that backs chat-history (slice 274), so they survive a reboot: each `KlineStore`/`ResvStore` gains an optional write-through connection (`open(path)` creates a `kline`/`resv` table + loads existing rows; `add`/`remove`/`prune` mirror to it; a `new()`/no-`database{}` store stays in-memory only, every persistence path a no-op), the in-memory `Vec` staying the hot-path authority; K-lines drop `expires <= now` rows on load + prune; only **runtime** (`conf == false`) reservations persist (config `resv {}` re-derives from `ircd.kdl`, so `seed_config` bypasses persistence and a `REHASH` never drops a learned reservation); `COLLATE NOCASE` primary keys make a case-differing re-add upsert not duplicate; open/write errors log and fall back to in-memory (never fatal); inbound S2S `apply_encap_kline`/`apply_encap_resv` persist what a server learns from the network too (all via `add`); 281 = S2S `KLINE`/`UNKLINE` propagation — the K-line analogue of slice 280: a `TKLINE`/`UNTKLINE` (leveva's temporary K-line family, there is no separate permanent `KLINE`) now propagates network-wide as `ENCAP * KLINE <duration> <user> <host> :<reason>`/`ENCAP * UNKLINE <user> <host>` (server-prefixed, leveva-native, the **remaining** seconds on the wire so each server re-clocks `expires = now + duration` against its own clock — no absolute-timestamp skew), wired into `command/tkline.rs` after the param/privilege/format gates; every server applies the ban to its own `KlineStore` (gating future registrations) **and reaps its own local matching clients** via the now-`is_local_uid`-guarded `reap_matching` helper (which also fixed a latent over-reap of remote users in the local `TKLINE`); `kline-exempt` clients are spared; `UNKLINE` lifts the gate only (never un-kills) and propagates unconditionally; re-asserted to a freshly-linked peer by `s2s/burst.rs::kline_burst` (active bans only, with remaining time; expired bursts nothing); new `s2s/kline.rs` modelled on `s2s/resv.rs`, ENCAP dispatch in `s2s/forward.rs`; 280 = S2S `RESV`/`UNRESV` propagation — an operator-laid nick/channel reservation (and its lift) now propagates network-wide as `ENCAP * RESV`/`UNRESV` (server-prefixed, kind re-derived from the mask prefix on inbound apply), wired into `command/resv.rs` after the param/privilege gates (a `461`/`481` reject propagates nothing; `UNRESV` propagates unconditionally), applied + split-horizon-relayed by `s2s/forward.rs`, and re-asserted to a freshly-linked peer via `s2s/burst.rs::resv_burst` (runtime `conf == false` reservations only — config `resv {}` blocks stay per-server local policy); closes the slice-272/273 S2S-propagation follow-on, mirroring the METADATA/KNOCK ENCAP carriers; 279 = remote-originated JOIN/PART/QUIT membership events are now recorded into chat-history — the membership-event parallel to slice 276 (which did PRIVMSG/NOTICE): a **live** `NJOIN` (`s2s/njoin.rs`, never a burst — bursts are bulk state-sync), a bare `JOIN`/`PART` (`s2s/relay.rs`), and a **discrete** remote `QUIT`/`KILL` (`s2s/squit.rs::quit_one_remote_user`, *not* a netsplit mass teardown) each record an event keyed by a locally-minted msgid + clock, gated on `has_local_member` (only a local member can `CHATHISTORY` it; a pure hub stores nothing); an unmirrored `NJOIN`-only ghost has no mask so its QUIT is skipped; replay/query side unchanged (slice 274 already gates these behind `draft/event-playback`); 278 = `+R` auto-reop is now enforced for **local** members only — the `enforce_reop` chokepoint gained an explicit `is_local_uid` filter, so a `+R` mask matching a `UNICK`-introduced remote user's `nick!user@host` no longer wrongly ops it locally and relays a `+o` we have no authority to emit (the locality guarantee previously rested on the false premise that remote members lack a registry record); 277 = netsplit batches are no longer duplicated to `batch`-capable clients under concurrent teardown — when a dying peer is detected on two paths at once (abrupt socket drop + a sibling's inbound `SQUIT`, in separate tasks), the `NJOIN`-only ghost-member sweep raced (`co_members` then `remove_everywhere`, no single-shot gate) and double-relayed a casualty's `QUIT`; new atomic `Channels::detach_member` fuses recipient-gather + removal in one critical section so only the first teardown to reach a member relays it (the loser gets `None`), mirroring the already-atomic `remove_behind_sid` mirror drain; new `s2s_netsplit_concurrency_proptest` fuzz; 276 = remote-originated channel messages are now recorded into chat-history — the S2S relay's inbound channel fan (`s2s::relay::inbound_message`) records the delivered line on this server too, keyed by the carried network msgid (slice 130) + `@time` (slice 134), gated on this server having a local member of the channel (only a local member can `CHATHISTORY` it; no whole-network accumulation on a hub); the `+z` op-redirect/non-channel branches never record, mirroring the local plane; closes the slice-274 gap where `CHATHISTORY` held only locally-sent lines; 275 = IRCv3 `message-redaction` now deletes from chat-history — an accepted channel `REDACT <chan> <msgid>` deletes the slice-274 stored line (`History::delete_by_msgid`, keyed `(target, msgid)`) so `CHATHISTORY` can never replay it; wired in both the local `CanSend::Ok` path and inbound `ENCAP * REDACT`, per-server; mirrors recording — a `+z` op-moderated/"shown to ops" attempt is never recorded so never deleted; authorization stays a stateless relay; 274 = IRCv3 `draft/chathistory` — SQLite-backed (`rusqlite`, bundled) channel message history behind an optional `ircd.kdl` `database {}` block; all channels auto-enrolled (PRIVMSG/NOTICE + JOIN/PART/QUIT recorded at the delivery plane), joining clients auto-replayed `replay-lines` recent lines unless they negotiated `draft/chathistory` (then exempt, per spec), full `CHATHISTORY LATEST/BEFORE/AFTER/BETWEEN/AROUND/TARGETS` (timestamp=/msgid= selectors) framed as a `chathistory` batch, `draft/chathistory`+`draft/event-playback` dynamic caps + `CHATHISTORY`/`MSGREFTYPES` ISUPPORT, age-based `retention-days` prune ticker; `Message` gained serde; 273 = registration-time nick `RESV` enforcement — a connection may no longer *register* directly as a reserved nick, only the post-registration `NICK`/JOIN were gated before; refuse `432` + reopen registration, mirroring the `433` nick-in-use path; 272 = charybdis `RESV`/`UNRESV` nick/channel reservation — JOIN→`437`/NICK→`432` gates, opers exempt, new `OperPrivilege::Resv`; defaults declared in `resv {}` config blocks, boot-seeded + REHASH-re-seeded keeping runtime `RESV`s). The slices span the full client command surface (registration; channels + the complete channel-mode surface; WHO/WHOIS/WHOX; OPER/STATS; and the query/util commands incl. OPERWALL/LOCOPS/USERIP); 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; CertFP; the **IRCv3 cap 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 **complete charybdis channel-mode set** (`+C/+c/+S/+j/+z/+T/+g/+r/+f/+Q/+F/+P/+L`, with `+L` the last standard one) and **user-mode set** (`+D/+R/+Q/+G/+z/+S/+l`, plus the product decision that leveva has no local-only umodes — `SEND_UMODES` is every umode); and the `+s` server-notice **snomask** (a charybdis category mask with a producer wired for every category — the matrix is complete). Most const limits (`max-penalty`/`max-bans`/`service-string`/`default-snomask`/…) have been promoted to REHASH-able `options { … }` knobs. **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 ---
+58
docs/progress-log/p11.md
··· 15970 15970 **Gate:** `cargo test -p leveva` (lib command::findforwards + channel forwards_to + findforwards_proptest 15971 15971 + golden_findforwards + command::help; full lib suite 2643 pass) green; `cargo clippy -p leveva --tests` 15972 15972 clean; `cargo build --workspace` 0 warnings. 15973 + 15974 + ## P11 slice 292 — OPME (oper self-op of an opless channel), 2026-06-19 15975 + 15976 + `OPME <channel>` — leveva's port of charybdis `extensions/m_opme.c`: an IRC operator grants 15977 + **themselves** channel-operator (`+o`) status in a channel that currently has no operators (an 15978 + *opless* channel). The recovery tool for a channel whose last op left, and the constrained, 15979 + safety-gated sibling of `SAMODE +o` (which can force `+o` on anyone, anywhere): OPME only fires 15980 + when the channel is genuinely opless and only ops the requester. Completes the charybdis oper 15981 + channel family (`SAJOIN`/`SAPART`/`SANICK`/`SAMODE`/`OJOIN`) with the recovery verb. 15982 + 15983 + ### Mechanism 15984 + - **New atomic seam** `Channels::opme(name, uid) -> OpmeOutcome` (the fuzz target): under a single 15985 + `by_name` lock — channel missing → `NoSuchChannel`; `uid` not a member → `NotOnChannel`; any 15986 + member at op rank → `NotOpless`; else set the requester's `+o` bit and return `Opped { display }`. 15987 + Atomic so the opless check and the grant cannot race (a concurrent OPME sees the first grant and 15988 + is refused). No persistence hook — member status never persists (mirroring `Channels::deop`). 15989 + - **`command/opme.rs`** gate ladder: not an operator (`+o`/`+O`) → `481` (mirrors `SAJOIN`/`KILL`); 15990 + no arg → `461`; then the seam routes `403`/`442`/the not-opless `NOTICE`/success. On `Opped`: 15991 + build `:<server> MODE <chan> +o <nick>`, fan to every local member (`deliver_uid`, skipping the 15992 + oper), relay server-sourced via `s2s::relay::server_channel_mode` (single `Member +o` change), 15993 + post a `+s` audit snomask `<oper> used OPME on <chan>`, and return the MODE echo to the oper. 15994 + - Wired `"OPME" => opme::opme(...)` + `mod opme;` in `command/mod.rs`; `OpmeOutcome` added to the 15995 + `command` channel re-export. Help: `help/OPME.md` + `OPME` in the `help.rs` COMMANDS allowlist + 15996 + the operator line in `help/index.md`. 15997 + 15998 + ### Divergences (documented) 15999 + - **leveva-native** — the C oracle is gone; IRCnet 2.11 has no OPME (charybdis extension). No 16000 + `leveva-integration` differential. The "is not opless"/audit wording is leveva's. 16001 + - **Membership before opless** — charybdis checks opless *before* membership; leveva checks 16002 + membership first (`442` before the not-opless `NOTICE`) so a non-member oper never learns the 16003 + channel's op state (the FINDFORWARDS existence/membership precedent). 16004 + - **`+s` audit snomask, not `+w` wallops** — charybdis announces OPME via `sendto_wallops_flags`; 16005 + leveva routes oper-override audit notices through the `+s` server-notice snomask, consistent with 16006 + the whole SA*/OJOIN family (slices 184/186). 16007 + - **Local-only** — a read+grant of *this* server's channel table (charybdis's OPME is likewise 16008 + per-server); the grant still broadcasts to local members and relays to peers. 16009 + 16010 + ### Tests (TDD; inverse invariants + fuzzing) 16011 + - `channel.rs` unit `opme_ops_only_when_opless`: opless member → `Opped` + now `is_op`; inverses — 16012 + a channel with an op → `NotOpless` (grants nothing), a non-member → `NotOnChannel`, a missing 16013 + channel → `NoSuchChannel`; fold-insensitive query; a second OPME (no longer opless) → `NotOpless`. 16014 + - `command/opme.rs` units (4): `481` non-oper (channel untouched); `461`/`403`/`442` operand & 16015 + lookup errors; the not-opless `NOTICE` (oper still not opped); success ops the oper, returns the 16016 + `MODE #rust +o alice` echo, a co-member's mailbox sees the same server-sourced MODE, and the 16017 + inverse — a second OPME now returns the not-opless NOTICE. 16018 + - `tests/opme_proptest.rs` (2 props): `opme_outcome_matches_channel_state` (model: random 16019 + base-member/requester membership + op presence + optional ghost target → exact outcome variant + 16020 + the op bit set iff already-op-or-`Opped`) and `arbitrary_args_never_panic` (replies ⊆ 16021 + `{481, 461, 403, 442, NOTICE, MODE}`). 16022 + - `tests/golden_opme.rs` (real binary, `dline.kdl` oper): non-oper bob `481`; alice deops herself 16023 + then `OPME #rust` → `:leveva.test MODE #rust +o alice` seen by alice **and** bob; a second OPME → 16024 + `NOTICE :Channel #rust is not opless`; `442` non-member; `403` nonexistent; `461` no-arg. 16025 + 16026 + **Plan:** `docs/superpowers/plans/2026-06-19-p11-slice292-opme.md`. 16027 + 16028 + **Gate:** `cargo test -p leveva` green (lib `command::opme` + `channel::opme` + `opme_proptest` + 16029 + `golden_opme` + `command::help`; full lib suite 2648 pass); `cargo clippy -p leveva --tests` clean; 16030 + `cargo build --workspace` 0 warnings.
+75
docs/superpowers/plans/2026-06-19-p11-slice292-opme.md
··· 1 + # P11 slice 292 — OPME (oper self-op of an opless channel) 2 + 3 + ## What 4 + 5 + Port charybdis `extensions/m_opme.c` resolved to single-server leveva: `OPME <channel>` lets an 6 + IRC operator grant **themselves** channel-operator (`+o`) status in a channel that currently has 7 + **no** operators (an "opless" channel) — the standard recovery tool for a channel whose last op 8 + left. The constrained, safety-gated sibling of `SAMODE +o` (which can force `+o` on anyone, 9 + anywhere): OPME only works when the channel is genuinely opless and only ops the requester, so it 10 + is the *legitimate-recovery* path rather than a blanket override. Completes the charybdis oper 11 + channel family (`SAJOIN`/`SAPART`/`SANICK`/`SAMODE`/`OJOIN`) with the recovery verb. 12 + 13 + ## Behaviour & gate ordering 14 + 15 + 1. Not an IRC operator (`+o`/`+O`) → `481 ERR_NOPRIVILEGES` (mirrors `SAJOIN`/`KILL`). 16 + 2. No channel argument → `461 ERR_NEEDMOREPARAMS`. 17 + 3. Channel does not exist → `403 ERR_NOSUCHCHANNEL`. 18 + 4. Requester is not a member of the channel → `442 ERR_NOTONCHANNEL`. 19 + 5. Channel already has at least one operator → `NOTICE <nick> :Channel <chan> is not opless` 20 + (a NOTICE, charybdis-faithful — `m_opme` emits a notice, not a numeric; nothing changes). 21 + 6. Success → grant the requester `+o`, broadcast `:<server> MODE <chan> +o <nick>` to every local 22 + member (incl. an echo to the oper), relay the grant to the linked network server-sourced 23 + (`s2s::relay::server_channel_mode`), and post a `+s` audit snomask 24 + `<oper> used OPME on <chan>` (the SAMODE/SAJOIN audit-notice precedent). 25 + 26 + **Divergence from charybdis order:** charybdis checks opless *before* membership; leveva checks 27 + membership first (442 before the not-opless NOTICE) so a non-member oper never learns the channel's 28 + op state — the FINDFORWARDS "existence/membership before the read" precedent. 29 + 30 + ## Divergences (documented) 31 + 32 + - **leveva-native** — the C oracle is gone; IRCnet 2.11 has no OPME (charybdis extension). No 33 + `leveva-integration` differential. The "is not opless" / audit-notice wording is leveva's. 34 + - **`+s` audit snomask, not `+w` wallops** — charybdis announces OPME via `sendto_wallops_flags`; 35 + leveva routes oper-override audit notices through the `+s` server-notice snomask, consistent with 36 + the whole SA*/OJOIN family (slices 184/186). Documented, deliberate. 37 + - **Local-only** — the grant broadcasts to local members and relays to peers, but OPME itself is a 38 + read+grant of *this* server's channel table; there is no remote-channel routing (charybdis OPME 39 + is likewise per-server). 40 + 41 + ## Mechanism / seams 42 + 43 + - **New atomic seam** `Channels::opme(&self, name, uid) -> OpmeOutcome` (the fuzz target): under a 44 + single `by_name` lock — channel missing → `NoSuchChannel`; `uid` not a member → `NotOnChannel`; 45 + any member at op rank → `NotOpless`; else set the requester's `+o` bit and return 46 + `Opped { display }`. Atomic so the opless-check and the grant cannot race. No persistence hook 47 + (member status never persists, mirroring `deop`). 48 + - `command/opme.rs` — the gate ladder above; on `Opped`, build the server-prefixed MODE echo, fan 49 + to local members (`deliver_uid`, skipping the oper), `server_channel_mode` relay with a single 50 + `ModeChange::Member { add: true, mode: Op, uid }`, `snotice::server_notice`, and return the echo 51 + to the oper. Local `notice()` helper (findforwards shape). 52 + - Wire-up: `mod opme;` + `"OPME" => opme::opme(client, msg, ctx)` in `command/mod.rs`; `help/OPME.md` 53 + + `OPME` in the `help.rs` COMMANDS allowlist + the operator line in `help/index.md`. 54 + 55 + ## Tests (TDD — RED first; inverse invariants + fuzzing) 56 + 57 + - `channel.rs` unit `opme_ops_only_when_opless`: opless member → `Opped` and now `is_op`; the 58 + inverses — a channel with an op → `NotOpless` (requester stays non-op), a non-member → `NotOnChannel`, 59 + a missing channel → `NoSuchChannel`; fold-insensitive channel arg; idempotence is irrelevant 60 + (after success the channel is no longer opless, so a second OPME → `NotOpless`). 61 + - `command/opme.rs` units: `481` non-oper (channel untouched), `461` no-arg, `403` nonexistent, 62 + `442` non-member, "not opless" NOTICE when an op exists (+ oper still not opped), success ops the 63 + oper + emits the MODE echo + the inverse that a second OPME now returns the not-opless NOTICE. 64 + - `tests/opme_proptest.rs` (2 props): `opme_outcome_matches_channel_state` (model: random membership 65 + + op presence → exact outcome variant and the op bit set iff `Opped`) and `arbitrary_args_never_panic` 66 + (replies ⊆ `{481, 461, 403, 442, NOTICE, MODE}`). 67 + - `tests/golden_opme.rs` (real binary, oper config): non-oper `481`; oper OPME on an opless channel 68 + → `:<server> MODE #chan +o oper` seen by the oper and a co-member; OPME on a channel with an op → 69 + `NOTICE … is not opless`; `442` non-member; `403` nonexistent; `461` no-arg. 70 + 71 + ## Gate 72 + 73 + `cargo test -p leveva` green (lib `command::opme` + `channel::opme` + `opme_proptest` + 74 + `golden_opme` + `command::help`); `cargo clippy -p leveva --tests` clean; `cargo build --workspace` 75 + 0 warnings.
+23
leveva/help/OPME.md
··· 1 + OPME <channel> 2 + 3 + Grants you channel-operator (+o) status in <channel> — but only if the channel 4 + is "opless", meaning no member currently holds +o. It is the recovery tool for a 5 + channel whose last operator has left: instead of force-setting a mode on anyone 6 + anywhere (SAMODE), OPME only ops you, and only when the channel genuinely has no 7 + operators. 8 + 9 + You must be an IRC operator (otherwise 481) and you must already be a member of 10 + the channel (otherwise 442). The channel argument is required (without it you get 11 + 461) and the channel must exist (otherwise 403). If the channel still has at 12 + least one operator you get a NOTICE saying it is not opless, and nothing changes. 13 + 14 + On success the grant is broadcast to the channel as 15 + :<server> MODE <channel> +o <you> 16 + seen by every member (including you) and relayed across the network. Operators 17 + watching the +s server-notice snomask see an audit line recording the OPME. 18 + 19 + Examples: 20 + OPME #help 21 + OPME #abandoned 22 + 23 + See also: MODE, SAMODE, OJOIN, CHANNEL_MODES.
+1 -1
leveva/help/index.md
··· 10 10 User state: NICK AWAY MODE ACCEPT 11 11 Server info: VERSION TIME ADMIN INFO LUSERS USERS MOTD 12 12 LINKS MAP STATS 13 - Operator commands: OPER WALLOPS KILL CHGHOST TRACE ETRACE FINDFORWARDS TKLINE UNTKLINE 13 + Operator commands: OPER WALLOPS KILL CHGHOST OPME TRACE ETRACE FINDFORWARDS TKLINE UNTKLINE 14 14 DLINE UNDLINE XLINE UNXLINE RESV UNRESV REHASH RESTART DIE 15 15 SQUIT MKPASSWD 16 16
+80
leveva/src/channel.rs
··· 461 461 Invited { display: String, stored: bool }, 462 462 } 463 463 464 + /// The outcome of a [`Channels::opme`] attempt — `OPME <channel>` (P11 slice 292, charybdis 465 + /// `extensions/m_opme.c`): an operator self-ops in an *opless* channel. 466 + #[derive(Debug, Clone, PartialEq, Eq)] 467 + pub enum OpmeOutcome { 468 + /// No channel by that (folded) name exists → `403 ERR_NOSUCHCHANNEL`. 469 + NoSuchChannel, 470 + /// The requester is not a member of the channel → `442 ERR_NOTONCHANNEL`. Checked before 471 + /// the opless test so a non-member never learns the channel's op state. 472 + NotOnChannel, 473 + /// The channel already has at least one operator → a `NOTICE` (charybdis emits no numeric); 474 + /// nothing is granted. 475 + NotOpless, 476 + /// The requester was a member of a genuinely opless channel and has been granted `+o`. 477 + /// `display` is the canonical channel name. 478 + Opped { display: String }, 479 + } 480 + 464 481 /// Why a JOIN was refused by [`Channels::check_join`] (each maps to a numeric). 465 482 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 466 483 pub enum JoinReject { ··· 1760 1777 .unwrap_or_default() 1761 1778 } 1762 1779 1780 + /// `OPME <name>` by `uid` (P11 slice 292, charybdis `extensions/m_opme.c`): grant `uid` 1781 + /// channel-operator (`+o`) status **iff** the channel is opless — has no member at op rank. 1782 + /// Atomic (single lock) so the opless check and the grant cannot race: a concurrent OPME 1783 + /// sees the first grant and is refused [`OpmeOutcome::NotOpless`]. 1784 + /// 1785 + /// Gate order — channel missing → [`OpmeOutcome::NoSuchChannel`]; `uid` not a member → 1786 + /// [`OpmeOutcome::NotOnChannel`] (before the opless test, so a non-member never learns op 1787 + /// state); any member already at op rank → [`OpmeOutcome::NotOpless`]; else set `uid`'s `+o` 1788 + /// bit and return [`OpmeOutcome::Opped`]. No persistence hook — member status never persists 1789 + /// (mirroring [`Channels::deop`]). 1790 + pub fn opme(&self, name: &str, uid: &Uid) -> OpmeOutcome { 1791 + let mut g = self.by_name.lock().expect("channels mutex poisoned"); 1792 + let Some(chan) = g.get_mut(&casemap::fold(name)) else { 1793 + return OpmeOutcome::NoSuchChannel; 1794 + }; 1795 + if !chan.members.contains_key(uid) { 1796 + return OpmeOutcome::NotOnChannel; 1797 + } 1798 + if chan 1799 + .members 1800 + .values() 1801 + .any(|s| s.rank() >= ChanMode::Op.rank()) 1802 + { 1803 + return OpmeOutcome::NotOpless; 1804 + } 1805 + let status = chan.members.get_mut(uid).expect("membership checked above"); 1806 + *status = status.with(ChanMode::Op, true); 1807 + OpmeOutcome::Opped { 1808 + display: chan.name.clone(), 1809 + } 1810 + } 1811 + 1763 1812 /// The ban masks on `name` (in add order), or `None` if no such channel. 1764 1813 pub fn bans(&self, name: &str) -> Option<Vec<String>> { 1765 1814 self.list_masks(name, ChanMode::Ban) ··· 2798 2847 set_fwd("#wheel", &bob(), Some("#somewhere")); 2799 2848 assert_eq!(ch.forwards_to("#hub"), vec!["#OTHER"]); 2800 2849 assert_eq!(ch.forwards_to("#somewhere"), vec!["#elsewhere", "#wheel"]); 2850 + } 2851 + 2852 + /// `opme` (P11 slice 292) grants `+o` only when the channel is genuinely opless, and only to 2853 + /// a member. The success case + every inverse (has-op, non-member, missing channel) and the 2854 + /// fold-insensitive query. 2855 + #[test] 2856 + fn opme_ops_only_when_opless() { 2857 + let ch = Channels::new(); 2858 + // #opless: alice creates it (chanop), then is deopped → no member at op rank. 2859 + ch.join("#opless", &alice(), 0).unwrap(); 2860 + ch.join("#opless", &bob(), 0).unwrap(); // bob is a plain member 2861 + assert!(ch.deop("#opless", &alice())); 2862 + assert!(ch.op_members("#opless").is_empty(), "channel is now opless"); 2863 + 2864 + // bob (a member of an opless channel) self-ops. 2865 + match ch.opme("#OPLESS", &bob()) { 2866 + // fold-insensitive query 2867 + OpmeOutcome::Opped { display } => assert_eq!(display, "#opless"), 2868 + other => panic!("expected Opped, got {other:?}"), 2869 + } 2870 + assert!(ch.is_op("#opless", &bob()), "bob now holds +o"); 2871 + 2872 + // Inverse 1 — no longer opless: a second OPME (by alice, still a member) is refused. 2873 + assert_eq!(ch.opme("#opless", &alice()), OpmeOutcome::NotOpless); 2874 + assert!(!ch.is_op("#opless", &alice()), "the refused OPME granted nothing"); 2875 + 2876 + // Inverse 2 — non-member: carol is not on #opless. 2877 + assert_eq!(ch.opme("#opless", &carol()), OpmeOutcome::NotOnChannel); 2878 + 2879 + // Inverse 3 — missing channel. 2880 + assert_eq!(ch.opme("#ghost", &alice()), OpmeOutcome::NoSuchChannel); 2801 2881 } 2802 2882 2803 2883 /// `wipe_list_masks` (P11 slice 174) clears **all four** list modes — `+b`/`+e`/`+I`/`+R` —
+1 -1
leveva/src/command/help.rs
··· 90 90 /// leveva's implemented command set — the canonical list the help pages and the 91 91 /// dispatch router must agree on. Test-only: it powers the two invariants below. 92 92 const COMMANDS: &[&str] = &[ 93 - "NICK", "SANICK", "PRIVMSG", "NOTICE", "TAGMSG", "JOIN", "SAJOIN", "OJOIN", "PART", 93 + "NICK", "SANICK", "PRIVMSG", "NOTICE", "TAGMSG", "JOIN", "SAJOIN", "OJOIN", "OPME", "PART", 94 94 "SAPART", "MODE", "SAMODE", "TOPIC", "KICK", "REMOVE", "INVITE", "KNOCK", "NAMES", "LIST", 95 95 "WHO", 96 96 "WHOIS", "WHOWAS", "USERHOST", "USERIP", "ISON", "MONITOR", "ACCEPT", "METADATA",
+4 -1
leveva/src/command/mod.rs
··· 19 19 pub(crate) use crate::casemap; 20 20 pub(crate) use crate::channel::{ 21 21 ApplyOutcome, CanSend, ChannelListing, InviteOutcome, JoinReject, KickGate, KickVictim, 22 - MemberStatus, ModeChange, NamesChannel, NamesReply, PartResult, SetTopic, Topic, TopicQuery, 22 + MemberStatus, ModeChange, NamesChannel, NamesReply, OpmeOutcome, PartResult, SetTopic, Topic, 23 + TopicQuery, 23 24 }; 24 25 pub(crate) use crate::clock; 25 26 pub(crate) use crate::config::OperPrivilege; ··· 119 120 "SANICK" => sanick::sanick(client, msg, ctx), 120 121 "SAJOIN" => sajoin::sajoin(client, msg, ctx), 121 122 "OJOIN" => ojoin::ojoin(client, msg, ctx), 123 + "OPME" => opme::opme(client, msg, ctx), 122 124 "SAPART" => sapart::sapart(client, msg, ctx), 123 125 "DIE" => die::die(client, msg, ctx), 124 126 "RESTART" => restart::restart(client, msg, ctx), ··· 624 626 pub(crate) mod nick; 625 627 mod ojoin; 626 628 mod oper; 629 + mod opme; 627 630 pub(crate) mod part; 628 631 mod ping; 629 632 mod redact;
+195
leveva/src/command/opme.rs
··· 1 + use crate::command::*; 2 + use crate::UserMode; 3 + 4 + /// `OPME <channel>` — leveva's port of charybdis `extensions/m_opme.c` (P11 slice 292). An IRC 5 + /// operator grants **themselves** channel-operator (`+o`) status in a channel that currently has 6 + /// no operators (an *opless* channel) — the recovery tool for a channel whose last op left. The 7 + /// constrained, safety-gated sibling of [`SAMODE +o`](crate::command::mode::samode) (which can 8 + /// force `+o` on anyone, anywhere): OPME only fires when the channel is genuinely opless and only 9 + /// ops the requester, so it is the *legitimate-recovery* path. Completes the charybdis oper 10 + /// channel family (`SAJOIN`/`SAPART`/`SANICK`/`SAMODE`/`OJOIN`) with the recovery verb. 11 + /// 12 + /// Gate order: 13 + /// - Not an IRC operator (`+o`/`+O`) → `481 ERR_NOPRIVILEGES` (mirrors `SAJOIN`/`KILL`). 14 + /// - No channel argument → `461 ERR_NEEDMOREPARAMS`. 15 + /// - Channel does not exist → `403 ERR_NOSUCHCHANNEL`. 16 + /// - Requester is not a member → `442 ERR_NOTONCHANNEL`. (Checked **before** the opless test so a 17 + /// non-member never learns the channel's op state — the FINDFORWARDS existence/membership 18 + /// precedent; charybdis checks opless first.) 19 + /// - Channel already has an operator → `NOTICE <nick> :Channel <chan> is not opless` (charybdis 20 + /// emits a notice, not a numeric); nothing changes. 21 + /// - Success → grant `+o`, broadcast `:<server> MODE <chan> +o <nick>` to every local member (incl. 22 + /// an echo back to the oper), relay the grant to the linked network server-sourced 23 + /// ([`crate::s2s::relay::server_channel_mode`]), and post a `+s` audit snomask 24 + /// `<oper> used OPME on <chan>`. 25 + /// 26 + /// Divergences (documented): leveva-native (no oracle — 2.11 has no OPME); the audit notice rides 27 + /// the `+s` snomask, not `+w` wallops (charybdis), matching the SA*/OJOIN family (slices 184/186); 28 + /// the "is not opless"/audit wording is leveva's; local-only (a read+grant of this server's 29 + /// channel table, like charybdis's per-server OPME). 30 + pub(crate) fn opme(client: &Registered, msg: &Message, ctx: &ServerContext) -> Vec<Message> { 31 + if client.modes & (UserMode::Oper.bit() | UserMode::LocalOp.bit()) == 0 { 32 + return vec![no_privileges(ctx, &client.nick)]; 33 + } 34 + let Some(chan) = msg.params().first().filter(|s| !s.is_empty()) else { 35 + return vec![need_more_params(ctx, &client.nick, "OPME")]; 36 + }; 37 + 38 + match ctx.channels.opme(chan, &client.uid) { 39 + OpmeOutcome::NoSuchChannel => vec![no_such_channel(ctx, &client.nick, chan)], 40 + OpmeOutcome::NotOnChannel => vec![Message::builder(Numeric::ErrNotonchannel) 41 + .prefix(&ctx.name) 42 + .param(&client.nick) 43 + .param(chan) 44 + .trailing("You're not on that channel") 45 + .build()], 46 + OpmeOutcome::NotOpless => vec![notice( 47 + ctx, 48 + &client.nick, 49 + format!("Channel {chan} is not opless"), 50 + )], 51 + OpmeOutcome::Opped { display } => { 52 + // The granted change, rendered once for the local broadcast and the S2S relay. 53 + let change = ModeChange::Member { 54 + add: true, 55 + mode: ChanMode::Op, 56 + uid: client.uid.clone(), 57 + }; 58 + let echo = Message::builder("MODE") 59 + .prefix(&ctx.name) 60 + .param(&display) 61 + .param("+o") 62 + .param(&client.nick) 63 + .build(); 64 + let wire = echo.to_wire(); 65 + if let Some(members) = ctx.channels.members(&display) { 66 + for m in &members { 67 + if *m != client.uid && ctx.is_local_uid(m) { 68 + ctx.registry.deliver_uid(m, wire.clone()); 69 + } 70 + } 71 + } 72 + // Relay the grant to the linked network (server-sourced, UID param). 73 + crate::s2s::relay::server_channel_mode(ctx, &display, std::slice::from_ref(&change)); 74 + // `+s` audit notice, mirroring the SAMODE/SAJOIN family. 75 + crate::snotice::server_notice(ctx, &format!("{} used OPME on {display}", client.nick)); 76 + vec![echo] 77 + } 78 + } 79 + } 80 + 81 + /// `:<server> NOTICE <nick> :<text>` — OPME's "not opless" refusal is a NOTICE, not a numeric 82 + /// (charybdis-faithful). 83 + fn notice(ctx: &ServerContext, nick: &str, text: impl Into<String>) -> Message { 84 + Message::builder("NOTICE") 85 + .prefix(&ctx.name) 86 + .param(nick) 87 + .trailing(text.into()) 88 + .build() 89 + } 90 + 91 + #[cfg(test)] 92 + mod tests { 93 + use super::*; 94 + use crate::command::testutil::*; 95 + 96 + /// An opered `alice` connection (claimed in the registry) carrying `+o`. 97 + fn opered(ctx: &ServerContext) -> Registered { 98 + let _ = claim_observed(ctx, "alice"); 99 + let mut c = client(); 100 + c.modes |= UserMode::Oper.bit(); 101 + c 102 + } 103 + 104 + fn drive(ctx: &ServerContext, c: &Registered, line: &str) -> Vec<Message> { 105 + dispatch(&mut c.clone(), &Message::parse(line).unwrap(), ctx) 106 + } 107 + 108 + /// Make `nick` a non-op member of `chan` (the channel must already exist). 109 + fn join_plain(ctx: &ServerContext, nick: &str, chan: &str) { 110 + let _ = claim_observed(ctx, nick); 111 + ctx.channels.join(chan, &uid_for(nick), 0); 112 + } 113 + 114 + /// The gate (and its inverse): a non-operator's OPME is `481` and grants nothing. 115 + #[test] 116 + fn opme_requires_oper() { 117 + let ctx = ctx(); 118 + join_plain(&ctx, "alice", "#rust"); // alice creates #rust as chanop… 119 + ctx.channels.deop("#rust", &uid_for("alice")); // …then is deopped → opless 120 + let r = drive(&ctx, &client(), "OPME #rust"); 121 + assert_eq!(codes(&r), &["481"]); 122 + assert!( 123 + !ctx.channels.is_op("#rust", &uid_for("alice")), 124 + "a gated OPME grants nothing" 125 + ); 126 + } 127 + 128 + /// Operand / existence / membership errors land on the oper. 129 + #[test] 130 + fn opme_operand_and_lookup_errors() { 131 + let ctx = ctx(); 132 + let oper = opered(&ctx); 133 + assert_eq!(codes(&drive(&ctx, &oper, "OPME")), &["461"]); 134 + assert_eq!(codes(&drive(&ctx, &oper, "OPME #ghost")), &["403"]); 135 + // alice is an oper but not a member of bob's channel. 136 + join_plain(&ctx, "bob", "#elsewhere"); 137 + ctx.channels.deop("#elsewhere", &uid_for("bob")); // opless, but alice isn't on it 138 + assert_eq!(codes(&drive(&ctx, &oper, "OPME #elsewhere")), &["442"]); 139 + } 140 + 141 + /// A channel that still has an operator → the "not opless" NOTICE, and nothing is granted. 142 + #[test] 143 + fn opme_refuses_a_channel_with_an_op() { 144 + let ctx = ctx(); 145 + let oper = opered(&ctx); 146 + // bob creates #rust (chanop) and stays opped; alice joins as a plain member. 147 + join_plain(&ctx, "bob", "#rust"); 148 + ctx.channels.join("#rust", &uid_for("alice"), 0); 149 + let r = drive(&ctx, &oper, "OPME #rust"); 150 + assert_eq!(codes(&r), &["NOTICE"]); 151 + assert!(r[0].trailing().unwrap().contains("is not opless")); 152 + assert!( 153 + !ctx.channels.is_op("#rust", &uid_for("alice")), 154 + "a refused OPME grants nothing" 155 + ); 156 + } 157 + 158 + /// Success: the oper is opped, gets the MODE echo, and a co-member sees the broadcast. 159 + #[test] 160 + fn opme_ops_the_oper_in_an_opless_channel() { 161 + let ctx = ctx(); 162 + let oper = opered(&ctx); 163 + ctx.channels.join("#rust", &uid_for("alice"), 0); // alice (oper) creates it… 164 + ctx.channels.deop("#rust", &uid_for("alice")); // …deopped → opless 165 + let mut bob_rx = join_plain_rx(&ctx, "bob", "#rust"); // a co-member with a mailbox 166 + 167 + let r = drive(&ctx, &oper, "OPME #rust"); 168 + assert_eq!(codes(&r), &["MODE"]); 169 + assert_eq!(r[0].command(), "MODE"); 170 + assert_eq!(r[0].params(), &["#rust", "+o", "alice"]); 171 + assert_eq!(r[0].prefix(), Some(ctx.name.as_str())); 172 + assert!(ctx.channels.is_op("#rust", &uid_for("alice")), "alice opped"); 173 + 174 + // The co-member sees the same server-sourced MODE. 175 + let seen = delivered(&mut bob_rx); 176 + assert_eq!(seen.command(), "MODE"); 177 + assert_eq!(seen.params(), &["#rust", "+o", "alice"]); 178 + 179 + // Inverse: a second OPME is now refused (the channel is no longer opless). 180 + let r2 = drive(&ctx, &oper, "OPME #rust"); 181 + assert_eq!(codes(&r2), &["NOTICE"]); 182 + assert!(r2[0].trailing().unwrap().contains("is not opless")); 183 + } 184 + 185 + /// Like [`join_plain`] but returns the new member's mailbox receiver. 186 + fn join_plain_rx( 187 + ctx: &ServerContext, 188 + nick: &str, 189 + chan: &str, 190 + ) -> tokio::sync::mpsc::UnboundedReceiver<crate::registry::Envelope> { 191 + let rx = claim_observed(ctx, nick); 192 + ctx.channels.join(chan, &uid_for(nick), 0); 193 + rx 194 + } 195 + }
+95
leveva/tests/golden_opme.rs
··· 1 + //! Boot-level golden: `OPME` end-to-end through the real `leveva` binary (slice 292). 2 + //! 3 + //! `OPME <channel>` lets an operator self-op in an *opless* channel. One booted server 4 + //! (`dline.kdl`, whose `root` operator suffices — OPME needs only the oper bit), two clients: 5 + //! 6 + //! 1. `alice` and `bob` register; `alice` opers up. 7 + //! 2. `alice` JOINs `#rust` (→ `@op`), `bob` JOINs (a plain member). 8 + //! 3. `bob` (not an operator) `OPME #rust` → `481 ERR_NOPRIVILEGES`. 9 + //! 4. `alice` deops herself (`MODE #rust -o alice`) → `#rust` is now opless. 10 + //! 5. `alice` `OPME #rust` → `:leveva.test MODE #rust +o alice`, seen by both `alice` and `bob`. 11 + //! 6. `alice` `OPME #rust` again → `NOTICE :Channel #rust is not opless` (she is op now). 12 + //! 7. `bob` creates `#bobland` (→ `@op`); `alice` (not a member) `OPME #bobland` → `442`. 13 + //! 8. `alice` `OPME #ghost` (does not exist) → `403`; `alice` `OPME` (no arg) → `461`. 14 + //! 15 + //! Lines are server- or numeric-prefixed and deterministic, so only the shared masks apply. 16 + 17 + mod harness; 18 + use harness::{boot_fixture, canonicalize, Client, PORT}; 19 + 20 + fn register(name: &str) -> Client { 21 + let mut c = Client::connect(); 22 + c.send(&format!("NICK {name}")); 23 + c.send(&format!("USER {name} 0 * :{name} Tester")); 24 + c.read_until(" 422 "); // drain the welcome burst (MOTD-missing 422) 25 + c 26 + } 27 + 28 + #[test] 29 + fn opme_end_to_end() { 30 + let _srv = boot_fixture("dline.kdl", PORT); 31 + 32 + let mut alice = register("alice"); 33 + let mut bob = register("bob"); 34 + 35 + // alice opers up (any operator suffices for OPME). 36 + alice.send("OPER root hunter2"); 37 + alice.read_until(" 381 "); 38 + 39 + // alice creates #rust (→ @op); bob joins as a plain member. 40 + alice.send("JOIN #rust"); 41 + alice.read_until(" 366 "); 42 + bob.send("JOIN #rust"); 43 + bob.read_until(" 366 "); 44 + alice.read_until("JOIN #rust"); // alice sees bob's JOIN 45 + 46 + // 3. non-oper bob → 481 (and the channel keeps its op). 47 + bob.send("OPME #rust"); 48 + let bob_481 = bob.read_until(" 481 "); 49 + 50 + // 4. alice deops herself → #rust is opless. 51 + alice.send("MODE #rust -o alice"); 52 + alice.read_until("MODE #rust"); 53 + bob.read_until("MODE #rust"); // bob sees the deop too 54 + 55 + // 5. alice OPME → server-sourced MODE +o, seen by both. 56 + alice.send("OPME #rust"); 57 + let alice_mode = alice.read_until("MODE #rust"); 58 + let bob_mode = bob.read_until("MODE #rust"); 59 + 60 + // 6. now opped → a second OPME is "not opless". 61 + alice.send("OPME #rust"); 62 + let not_opless = alice.read_until("is not opless"); 63 + 64 + // 7. bob owns #bobland; alice (not a member) → 442. 65 + bob.send("JOIN #bobland"); 66 + bob.read_until(" 366 "); 67 + alice.send("OPME #bobland"); 68 + let alice_442 = alice.read_until(" 442 "); 69 + 70 + // 8. nonexistent → 403; no arg → 461. 71 + alice.send("OPME #ghost"); 72 + let alice_403 = alice.read_until(" 403 "); 73 + alice.send("OPME"); 74 + let alice_461 = alice.read_until(" 461 "); 75 + 76 + let mut transcript = String::new(); 77 + let mut keep = |src: &str, needle: &str, label: &str| { 78 + for line in canonicalize(src).lines() { 79 + if line.contains(needle) { 80 + transcript.push_str(label); 81 + transcript.push_str(line); 82 + transcript.push('\n'); 83 + } 84 + } 85 + }; 86 + keep(&bob_481, " 481 ", "bob 481: "); 87 + keep(&alice_mode, "MODE #rust +o", "alice sees MODE: "); 88 + keep(&bob_mode, "MODE #rust +o", "bob sees MODE: "); 89 + keep(&not_opless, "is not opless", "not opless: "); 90 + keep(&alice_442, " 442 ", "alice 442: "); 91 + keep(&alice_403, " 403 ", "alice 403: "); 92 + keep(&alice_461, " 461 ", "alice 461: "); 93 + 94 + insta::assert_snapshot!(transcript); 95 + }
+155
leveva/tests/opme_proptest.rs
··· 1 + //! Property-based fuzzing of `OPME` (slice 292) — operator self-op of an opless channel. 2 + //! 3 + //! Two surfaces: 4 + //! 5 + //! - **`opme_outcome_matches_channel_state`** — the pure atomic seam [`Channels::opme`]. For a 6 + //! randomly-constructed channel (a base member that may or may not be opped; the requester that 7 + //! may or may not be a member, may or may not already be opped) and an optionally-nonexistent 8 + //! target, the returned outcome must match the model — `NoSuchChannel` / `NotOnChannel` / 9 + //! `NotOpless` / `Opped` — and the requester ends up opped **iff** it was already opped or the 10 + //! call returned `Opped`. The conservation invariant: OPME grants `+o` only on a genuinely 11 + //! opless channel the requester belongs to, and never otherwise. 12 + //! - **`arbitrary_args_never_panic`** — `OPME` at the [`dispatch`] layer with arbitrary printable 13 + //! args never panics and only ever replies `{481, 461, 403, 442, NOTICE, MODE}`. 14 + 15 + use std::sync::Arc; 16 + 17 + use leveva::channel::{Channels, ModeChange, OpmeOutcome}; 18 + use leveva::command::{dispatch, Registered}; 19 + use leveva::config::Admin; 20 + use leveva::mode::ChanMode; 21 + use leveva::registry::Registry; 22 + use leveva::server::{Counters, ServerContext}; 23 + use leveva::{Message, Sid, Uid}; 24 + use proptest::prelude::*; 25 + 26 + const SERVER: &str = "leveva.test"; 27 + 28 + fn ctx() -> Arc<ServerContext> { 29 + Arc::new(ServerContext { 30 + name: SERVER.to_string(), 31 + description: "T".to_string(), 32 + network: "TestNet".to_string(), 33 + version: "leveva-0.0.0", 34 + created: "FIXED".to_string(), 35 + motd: None, 36 + counters: Counters::default(), 37 + registry: Registry::new(), 38 + channels: Channels::new(), 39 + whowas: leveva::whowas::WhowasHistory::default(), 40 + uids: leveva::uid::UidGenerator::new(Sid::try_from("0ABC").unwrap()), 41 + default_user_modes: 0, 42 + default_channel_modes: 0, 43 + operators: Vec::new(), 44 + stats_conf: leveva::server::StatsConf::default(), 45 + klines: leveva::kline::KlineStore::new(), 46 + dlines: leveva::dline::DlineStore::new(), 47 + xlines: leveva::xline::XlineStore::new(), 48 + net: leveva::s2s::Network::new(), 49 + peers: leveva::s2s::PeerLinks::new(), 50 + monitors: leveva::monitor::Monitors::new(), 51 + conn_limits: leveva::connlimit::ConnLimits::new(), 52 + linking: leveva::link::LinkingSet::new(), 53 + knock_throttle: leveva::knock_throttle::KnockThrottle::new(), 54 + knock_user_throttle: leveva::knock_throttle::KnockThrottle::new(), 55 + resvs: leveva::resv::ResvStore::new(), 56 + history: leveva::history::History::disabled(), 57 + auth: leveva::server::AuthConfig::default(), 58 + admin: Admin::default(), 59 + }) 60 + } 61 + 62 + fn uid(i: usize) -> Uid { 63 + let c = (b'A' + i as u8) as char; 64 + Uid::try_from(format!("0ABCAAAA{c}").as_str()).unwrap() 65 + } 66 + 67 + /// The requester — an operator, so the `481` gate never masks the outcome path at dispatch. 68 + fn oper() -> Registered { 69 + Registered { 70 + uid: uid(0), 71 + nick: "alice".into(), 72 + user: "u".into(), 73 + host: "h".into(), 74 + realname: "R".into(), 75 + modes: leveva::UserMode::Oper.bit(), 76 + privileges: Vec::new(), 77 + caps: Default::default(), 78 + } 79 + } 80 + 81 + /// Set or clear `uid`'s `+o` on `chan` (the member must already exist). 82 + fn set_op(ctx: &ServerContext, chan: &str, who: Uid, on: bool) { 83 + if on { 84 + ctx.channels.apply_modes_as_server( 85 + chan, 86 + &[ModeChange::Member { 87 + add: true, 88 + mode: ChanMode::Op, 89 + uid: who, 90 + }], 91 + ); 92 + } else { 93 + ctx.channels.deop(chan, &who); 94 + } 95 + } 96 + 97 + proptest! { 98 + /// The seam's outcome is exactly what the channel state dictates, and the `+o` grant happens 99 + /// only on the `Opped` arm. 100 + #[test] 101 + fn opme_outcome_matches_channel_state( 102 + base_opped in any::<bool>(), 103 + requester_member in any::<bool>(), 104 + requester_opped in any::<bool>(), 105 + query_ghost in any::<bool>(), 106 + ) { 107 + let ctx = ctx(); 108 + // A base member keeps the channel alive; it auto-ops on creation, so its op state is then 109 + // set explicitly. uid(99) maps to a distinct fixture UID. 110 + ctx.channels.join("#c", &uid(25), 0); 111 + set_op(&ctx, "#c", uid(25), base_opped); 112 + if requester_member { 113 + ctx.channels.join("#c", &uid(0), 0); // non-op (channel already exists) 114 + set_op(&ctx, "#c", uid(0), requester_opped); 115 + } 116 + 117 + let target = if query_ghost { "#ghost" } else { "#c" }; 118 + let outcome = ctx.channels.opme(target, &uid(0)); 119 + 120 + let expected = if query_ghost { 121 + OpmeOutcome::NoSuchChannel 122 + } else if !requester_member { 123 + OpmeOutcome::NotOnChannel 124 + } else if base_opped || requester_opped { 125 + OpmeOutcome::NotOpless 126 + } else { 127 + OpmeOutcome::Opped { display: "#c".to_string() } 128 + }; 129 + prop_assert_eq!(&outcome, &expected, "state→outcome mismatch"); 130 + 131 + // The requester holds +o afterwards iff it was already opped or the call just opped it. 132 + let opped_now = ctx.channels.is_op("#c", &uid(0)); 133 + let should_be_op = requester_member 134 + && (requester_opped || matches!(outcome, OpmeOutcome::Opped { .. })); 135 + prop_assert_eq!(opped_now, should_be_op, "op-bit conservation"); 136 + } 137 + 138 + /// Arbitrary args never panic; the reply is always in the allowed set. 139 + #[test] 140 + fn arbitrary_args_never_panic(args in "[#a-zA-Z0-9 ,:*!@._-]{0,40}") { 141 + let ctx = ctx(); 142 + // An opless channel the oper belongs to, so the success (MODE) path is reachable for the 143 + // matching query. 144 + ctx.channels.join("#c", &uid(0), 0); 145 + ctx.channels.deop("#c", &uid(0)); 146 + 147 + let line = format!("OPME {args}"); 148 + let Ok(msg) = Message::parse(&line) else { return Ok(()); }; 149 + let r = dispatch(&mut oper(), &msg, &ctx); 150 + const ALLOWED: &[&str] = &["481", "461", "403", "442", "NOTICE", "MODE"]; 151 + for c in r.iter().map(|m| m.command()) { 152 + prop_assert!(ALLOWED.contains(&c), "unexpected reply {c} for {line:?}"); 153 + } 154 + } 155 + }
+11
leveva/tests/snapshots/golden_opme__opme_end_to_end.snap
··· 1 + --- 2 + source: leveva/tests/golden_opme.rs 3 + expression: transcript 4 + --- 5 + bob 481: :leveva.test 481 bob :Permission Denied- You're not an IRC operator 6 + alice sees MODE: :leveva.test MODE #rust +o alice 7 + bob sees MODE: :leveva.test MODE #rust +o alice 8 + not opless: :leveva.test NOTICE alice :Channel #rust is not opless 9 + alice 442: :leveva.test 442 alice #bobland :You're not on that channel 10 + alice 403: :leveva.test 403 alice #ghost :No such channel 11 + alice 461: :leveva.test 461 alice OPME :Not enough parameters