Commits
Each triple now also gets leveva-<ver>-<triple>.tar.gz carrying the
same payload as the package asset tables in leveva/Cargo.toml (binary,
systemd unit, default config + MOTD, example config, license), for
platforms without dpkg/rpm. Collected into var/dist/ with the packages.
Assisted-by: Claude Fable 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Update the user-mode-wiring checklist: the self-settable char arm and
umode_diff echo table are no longer two hardcoded letter lists but derive
from UserMode::is_self_settable / is_client_settable, and the operator
check is now mode::is_oper. Reflects the wave-2 review cleanups so future
umode work targets the predicates instead of re-pairing allowlists.
Assisted-by: Claude Fable 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
The freely client-settable umode set was hardcoded twice in command/mode
.rs — the MODE char match arm and the umode_diff echo array — which must
agree or MODE silently applies a mode while echoing an empty diff (the
slice-251 +G bug). Derive both from predicates on UserMode:
is_self_settable (the plain set iwBxgGDRQC that both sites share, the
drift-prone part) and is_client_settable (that set plus the oper-gated
s/z/l, for the echo diff). Letter sets are byte-identical; add an
invariant test pinning both and asserting self_settable ⇒ client_settable
so no accepted letter can ever echo empty again.
The channel PRIVMSG/NOTICE fanout took the process-wide registry mutex
three times per recipient (modes_of, caps_of, deliver_uid) and
re-rendered/re-serialized the wire per member, so a message to a large
channel did ~3N lock cycles plus N serializations and collapsed on lock
contention. Add Registry::deliver_batch, which snapshots each member's
modes+caps and enqueues under a single lock pass, and have message.rs
memoize the rendered wire by capability-combination (at most 16 variants)
so it renders once per distinct cap combo instead of once per member.
Recipients, deaf/op-moderated/echo behavior, and ordering unchanged.
The compound operator test modes & (Oper.bit() | LocalOp.bit()) != 0
was hand-inlined at ~34 sites across the command and s2s planes, so any
change to what counts as an operator meant editing all of them and
risking drift. Add mode::is_oper beside the existing is_deaf/is_restricted
/is_operwall predicates and route every site (both != 0 and == 0
polarities) through it. Semantics-preserving.
History::record ran a synchronous rusqlite INSERT + JSON serialization
behind one process-wide Mutex<Connection>, inline on the async serve loop
for every channel PRIVMSG/NOTICE/JOIN/PART/QUIT; with a database{} block
enabled all traffic stalled behind the disk fsync and clients hit ping
timeouts. Hand rows to a dedicated writer thread over an mpsc channel
(coalescing queued inserts into one transaction); reads and REDACT
deletes dispatch over the same FIFO channel so ordering stays correct.
TAGMSG's target loop was a hand-copy of message()'s that dropped both
the MAX_TARGETS cap (ERR_TOOMANYTARGETS/407) and the canonicalize_targets
dedup, so TAGMSG #c,#c,#c,... re-fanned to every member and re-relayed to
every peer per duplicate with no cap — a flood-amplification vector that
PRIVMSG refuses. Reuse message.rs's MAX_TARGETS and canonicalize_targets
(bumped to pub(crate)) so TAGMSG shares the exact same limit and dedup.
AuthPipeline::run awaited each module (ident/dnsbl/socks/webproxy) in
sequence, each with its own timeout, so worst-case registration latency
was the sum of the per-module timeouts instead of the max. Spawn the
independent probes on a JoinSet, then fold outcomes by original module
index so the ordering semantics (first-Username-wins, Deny short-circuit,
NoOpinion/timeout ignored) are byte-for-byte identical.
handle_webirc overwrote the single self.host field with the gateway
-supplied spoofed hostname and discarded the real client IP, so the
IP-keyed registration ban gates tested a hostname that never parses as
a CIDR — a client on a banned IP tunneling through a trusted WEBIRC
gateway evaded D-lines, IP K-lines, and config bans. Retain the real
end-user IP (webirc_real_ip, the charybdis sockhost analog) and check
both it and the spoofed display host at all three registration gates.
The Password::Plain verify branch used a short-circuiting byte-slice
compare (candidate.as_bytes() == p.as_bytes()) that leaked password
length and the matching-prefix length via response timing, letting an
attacker recover a plaintext OPER/link/allow/WEBIRC secret byte-by-byte.
Compare via fixed-width SHA-256 digests folded with subtle's
ConstantTimeEq so timing is independent of both prefix and length.
Promote subtle to a direct dependency (already resolved transitively).
The IRCv3 CAP-negotiation framework post: the one corner of IRC that
negotiates instead of assuming, and the locked scope rule that the
negotiation mechanism and each capability's behavior are separate slices.
Covers CapState (version-never-downgrades, the registration-suspend brake
that feeds post 21's two-gate finalize), the all-or-nothing REQ as a
prepare-then-commit transaction, the 25-field ClientCaps payoff, and the
honest wart: sasl/chathistory/sts break the static-table abstraction and
get injected at render time. leveva-native, no oracle.
Assisted-by: Claude Opus 4.8 (claude-opus-4-8)
Signed-off-by: Xe Iaso <me@xeiaso.net>
Reindent with tabs and drop the spaces inside the subshell parens.
Formatting only; no behavior change.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
The navbar was a flat row of ten links. Collapse the eight
documentation pages into a no-JS <details> "Docs" disclosure menu,
leaving a tidy Docs / Blog / Source bar. Mark the current page and its
group via current_path so visitors know where they are.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Relocate the helper out of the .claude directory; no content change.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Update the blog-series conventions memory: mark the Cadey/Mara
blockquote aside device retired (the whole back-catalog was
de-asided) so the next post matches. Also carries the earlier
working-tree notes documenting posts 018-022 and the move to the
zola site with TOML +++ frontmatter.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Update the series skill to match the de-asided posts and the audit
findings.
- Replace the "character asides as blockquotes" convention with an
explicit no-asides rule; both technical tangents and reaction beats
go inline in one first-person voice.
- Drop the now-obsolete blockquote clause from the hard paragraph rule.
- Expand the sound-like-a-person step with the five recurring tells the
series-wide audit surfaced, each with its fix.
- Add reaching-for-asides and the five tells to the anti-patterns.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Two voice changes applied across all 22 posts in the series.
First, a sound-like-a-person pass: a series-wide audit found five
recurring AI-shaped tells clustered at section seams — balanced
"not X, it's Y" antitheses, restate-the-point closers, recap
tricolons, honesty-signaling, and meta-narration. Flattened the
constructed instances while keeping the lived-in, opinionated spine.
Second, removed the Cadey/Mara blockquote asides (70 of them) and
folded each into the surrounding first-person narrative. Technical
tangents (file:line anchors, symbol names, the C-vs-Rust details)
and reaction beats now read as continuous prose. Re-checked the
no-successive-same-opening-letter rule after both passes.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
The next post in the C->Rust port series. Covers leveva's
charybdis-style extbans: how a ban stops being a static
nick!user@host string and becomes a predicate the server
evaluates fresh each time. Walks the three-state Verdict
(Invalid as a property of the mask), the JOIN-gate plus
speech-plane enforcement, the questions too unsafe to invert
into an exception ($r/$c), the $&/$| combinators, and the
documented divergences ($m=usermode, no $q quiet list). No
oracle behind it: ported from Elemental-IRCd by reading.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
The native-async-iauth post: the standalone iauth (post 005) and the
fork/execl launcher (post 013) both existed only to keep blocking auth
probes off the C ircd's single-threaded select loop. Under leveva's tokio
runtime a blocking probe is just an `.await`, so the whole process boundary
— pipe protocol, socketpair, launcher — is deleted, not ported. Covers the
two-method Module trait (the pipe-protocol baggage that fell off), the
concurrent-spawn + two-stash try_finalize seam (order-independent finalize),
the `~` username rule, the process-vs-task isolation trade given up on
purpose, and (like 019/020) trusting it with the oracle already switched off.
Assisted-by: Claude Opus 4.8 (claude-opus-4-8)
Signed-off-by: Xe Iaso <me@xeiaso.net>
The P8v finale: the actual switch-off of the differential oracle. Two
commits (P8u drops all C compilation, P8v deletes the C source tree +
freezes bindings.rs into ircd-common via the extern-crate-self alias),
the irreversibility of running the oracle for the last time, the final
run's one red being the reference's own uninitialized-sendq STATS bug
(Rust is the correct side), and the honest downgrade from differential
to snapshot (weaker net, sufficient once faithfulness stops being the
goal). Grounded in docs/progress-log/p8.md + commits c1f72bb6/b88264f3.
Assisted-by: Claude Opus 4.8 (claude-opus-4-8)
Signed-off-by: Xe Iaso <me@xeiaso.net>
Post 19 — the first leveva-feature post with no oracle behind it. Netsplit-merge
arbitration: channel-TS and user-TS, oldest (lowest) timestamp wins by every
server independently comparing the same two integers (no coordinator, the whole
consensus protocol is `their_ts < our_ts`). The dark turn: the loser is wiped, not
merged — younger chanops deopped, the loser's +b/+e/+I AND +R lists wiped wholesale
(the reop-list hard rule I overruled myself on, plus the EOB reop sweep that fix
forced), and a younger nick renamed to its UID via the cross-task ForceNick
primitive. The equal-timestamp SaveBoth case (tie = mutual destruction, nick
vacated). Honest close: no differential here — confidence comes from proptests,
the boot-golden, and matching the decades-old TS6 design intent.
Assisted-by: Claude <noreply@anthropic.com>
Signed-off-by: Xe Iaso <me@xeiaso.net>
Post 018 narrates the roadmap pivot: with all C deleted (P8v), the planned
in-place idiomatic cleanup (P9) is retired in favor of a greenfield rewrite
(leveva) pinned differentially against the now-finished faithful port, which
becomes the behavioral oracle. The strangler pattern re-aimed one level up.
Assisted-by: Claude (claude-opus-4-8)
Signed-off-by: Xe Iaso <me@xeiaso.net>
The chantrace/masktrace golden tests snapshot the 262 RPL_TRACEEND line,
which embeds VERSION (concat!("leveva-", CARGO_PKG_VERSION)). The v0.3.0
bump changed that token from leveva-0.2.0 to leveva-0.3.0, breaking both
snapshots on every release.
Mask the version column to V, mirroring how these tests already mask the
volatile host/ip columns of 708 lines, so future version bumps no longer
break them.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Continue the leaf-crate carve-out (after leveva-casemap / leveva-string /
leveva-message / leveva-patricia / leveva-cidr / leveva-cloak) by moving the IRC
glob matcher + nick!user@host hostmasks out of the monolith into its own
workspace crate:
- leveva-matching: the C match.c cluster (matches / Pattern / collapse / HostMask
/ host_component_matches / allow_host_matches, 498 LoC). The survey-flagged
"natural next pick": once leveva-cidr split, its only intra-crate edges were
casemap::to_lower and cidr::cidr_match — both already crates — plus nom. Deps =
leveva-casemap + leveva-cidr + nom.
Body moved verbatim via git mv; five repoints only: `use crate::casemap;` ->
`use leveva_casemap as casemap;`, `crate::cidr::cidr_match` ->
`leveva_cidr::cidr_match`, the module-header intra-doc link (crate::casemap) ->
(leveva_casemap), and two doctests `use leveva::matching::…` ->
`use leveva_matching::…`. leveva re-exports it as leveva::matching
(`pub use leveva_matching as matching;`), so the flat
`pub use matching::{collapse, matches, HostMask, Pattern};` and all ~13 consumers
(xline / kline / resv / webirc / metadata / registration +
command/{oper,map,mask,hunt,dline,tkline}) keep their crate::matching::* paths.
No dependency cycle — the code edges all run into matching.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching the other leaves. leveva's matcher deliberately
diverges from match.c (RFC 1459 case-fold, not ASCII tolowertab; #=any-digit), so
there is NO byte-faithful differential oracle — the 13 proptest invariants ARE the
gate. Fed arbitrary (incl. non-ASCII) strings + glob masks: totality/no-panic over
every entry point, `*` is universal, `?`^k iff exact byte-length, case-insensitivity
under leveva_casemap::fold of the name, compile == matches, collapse is
matching-preserving + idempotent + non-growing, HostMask re-parses its own Display,
matches_target == matches_parts, allow_host_matches decomposes into user-glob &&
host-component, and a glob host component falls back to matches.
Gate: cargo build --workspace (0 warnings) · cargo clippy --workspace --tests
(clean) · cargo test -p leveva-matching (28 unit/collapse/escaping + 13 fuzz, + 2
doctests) green · leveva matching-consumer tests (xline/kline/resv/webirc, 181) green.
What else can move out: uid (only crate::ident, blocked on extracting ident — ident
itself blocked by a single crate::command reference worth auditing, the next unlock);
msgid pulls cap/clock/ident; clock is a small candidate leaf. Recorded in the plan +
progress log.
Continue the leaf-crate carve-out (after leveva-casemap / leveva-string /
leveva-message / leveva-patricia / leveva-cidr) by moving the +x hidden-host
transform out of the monolith into its own workspace crate:
- leveva-cloak: the deterministic FNV-keyed IP/host cloak (cloak / cloak_ip /
cloak_host), a faithful port of Elemental-IRCd's ip_cloaking.c. The cleanest
remaining leaf, exactly as the leveva-cidr survey predicted ("zero intra-crate
deps — perfect standalone leaf, strongest next pick"): it depends only on
std::net::IpAddr and on nothing in the workspace. Its only crate::-references
were doc comments — the actual code edges all run *into* it
(command::mode::apply_cloak_change at mode.rs:916 and session.rs:1786 call
crate::cloak::cloak), so no cycle.
leveva re-exports it as leveva::cloak (`pub use leveva_cloak as cloak;`), so every
crate::cloak::* / leveva::cloak::* path is unchanged across consumers. Body moved
verbatim via git mv — no doctest, and every intra-doc link (cloak/cloak_ip/
cloak_host/IP_CHARTABLE/HOST_ALPHABET + the FNV reference) resolves in-module, so
nothing repointed.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching casemap/string/cidr. And since the C 2.11 oracle has
no +x usermode, there is NO differential oracle here — the 10 proptest invariants
ARE the gate, not a supplement. Fed arbitrary (incl. non-ASCII) strings and the
full v4/v6 address space: determinism (the ban-on-a-cloak-keeps-biting contract),
the leading-colon wire-safety biconditional (a non-: input never grows a leading
colon, a :-input is returned verbatim), length preservation for both cloak_ip and
cloak_host, separator/dot byte-index invariance, IP-literal dispatch to cloak_ip,
the g-z IP alphabet (v4 keeps its first two octets; v6 is hex/:/g-z with no leading
colon), and fnv_hash_32 == charybdis's explicit shift-decomposition over arbitrary
bytes.
What else can move out: matching (now a clean casemap+cidr leaf, pulls nom — the C
match.c cluster, natural next pick); uid (only crate::ident, blocked on extracting
ident); msgid pulls cap/clock/ident. Recorded in the plan + progress log.
Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests clean;
leveva-cloak 18 (8 unit + 10 fuzz) green; leveva +x consumer tests (command::mode
plus_x suite + session registers-cloaked + the s2s +s+x spy, 8) green.
Carve the pure CIDR network-mask helpers (parse_cidr / contains /
network_base / cidr_match) out of leveva into a self-contained
leveva-cidr leaf crate, re-exported as `leveva::cidr` — continuing the
post-P12 leaf-crate carve-out after casemap/string/message/patricia.
cidr is a genuinely pure leaf: zero workspace deps, only std::net. This
corrects the earlier leveva-string survey, which filed matching+cidr as
"mutually referential, not a clean single leaf yet" — cidr's references
to matching/connlimit are doc-comment-only; the real edge is one-way
matching -> cidr. Extracting cidr therefore *unblocks* matching, whose
only remaining intra-crate deps are now casemap + cidr (both crates).
git mv leveva/src/cidr.rs -> leveva-cidr/src/lib.rs verbatim except the
doctest import and two cross-crate doc links; leveva/src/lib.rs swaps
`pub mod cidr;` for `pub use leveva_cidr as cidr;`. All four call sites
use fully-qualified crate::cidr::*, so zero consumer churn.
Fuzzing (the user ask): 10 proptest invariants over the full v4/v6
address space and arbitrary in-range prefixes, all consistency/inverse
properties — parse round-trip + out-of-range reject, contains reflexive
through network_base, network_base idempotent, contains prefix-monotone,
/0 universal + full-length exact, family mismatch never contains, and
cidr_match == parse + contains.
Next extraction candidates: matching (now a clean leaf, the C match.c
cluster), cloak (zero intra-crate deps), uid (blocked on ident).
Gate: cargo build -p leveva 0 warnings; cargo clippy -p leveva-cidr
--tests clean; cargo test -p leveva-cidr 18 (8 unit + 10 fuzz) + 1
doctest green; leveva cidr-consumer tests (matching/connlimit/websocket,
54) green.
Continue the leaf-crate carve-out (after leveva-message / leveva-patricia /
leveva-casemap) by moving the casemapping-aware IRC string types out of the
monolith into their own workspace crate:
- leveva-string: IrcStr / IrcString (str/String wrappers whose equality,
ordering, and hashing fold under RFC 1459) + IrcError + IrcStringBuilder. The
architectural #2 leaf in the string cluster — now that casemap is its own crate,
string depends on exactly one workspace crate (leveva-casemap) plus core/std,
while ident/channel/registry/whowas all depend on it. No cycle: leveva-message
uses neither, leveva-casemap depends only on core.
leveva re-exports it as leveva::string (`pub use leveva_string as string;`), and
the existing `pub use string::{IrcError, IrcStr, IrcString, IrcStringBuilder};`
re-exports straight out of the new crate — so every crate::string::* /
leveva::string::* path and the four flat re-exports are unchanged across all
consumers (the full dependent build confirms zero churn). Body moved verbatim
except `use crate::casemap;` -> `use leveva_casemap as casemap;` and the two
doctests' `use leveva::...;` -> `use leveva_string::...;`.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching casemap/message. Added 6 consistency/inverse
invariants fed arbitrary (incl. non-ASCII) strings: eq lifts casemap (the wrapper
adds no equality of its own), Ord consistent-with-Eq + antisymmetric, Hash agrees
with Eq (the HashMap-key contract behind the nick/channel tables), Borrow<IrcStr>
round-trip (insert under one casing, find by a case-different borrowed key),
validation soundness (accept iff no NUL/CR/LF, FromStr agrees, storage
byte-identical on success), and builder = deferred try_from.
What else can move out: cloak (zero intra-crate deps — perfect standalone leaf,
strongest next pick); uid (only crate::ident); matching+cidr are a coupled pair;
msgid pulls cap/clock/ident. Recorded in the plan + progress log.
Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests clean;
leveva-string 13 (7 unit + 6 fuzz) + 2 doctests green; full leveva suite (413
binaries) green.
Continue the leaf-crate carve-out (after leveva-message / leveva-patricia) by
moving the RFC 1459 case-folding module out of the monolith into its own
workspace crate:
- leveva-casemap: byte/slice fold, compare, hash, and the owned-key `fold`. It
imports only `core` (Ordering + Hasher) and is the lowest leaf in the string
cluster — string/ident/matching/channel/registry/whowas depend on it, it on
nothing. std-only lib, proptest dev-dep.
leveva re-exports it as leveva::casemap (`pub use leveva_casemap as casemap;`),
so every crate::casemap::* / leveva::casemap::* path across ~40 source and test
files is unchanged — no consumer churn.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching leveva-message. Added 7 consistency/inverse
invariants fed arbitrary bytes and arbitrary (incl. non-ASCII) strings:
fold ⇔ eq_ignore_case both directions, cmp==Equal ⇔ eq, cmp antisymmetry,
hash-agrees-with-eq (prefix-free 0xff marker exercised), fold idempotent +
UTF-8-safe + byte-length-preserving, high bytes (≥0x80) identity, and
to_upper∘to_lower identity on the uppercase domain over the full byte range.
Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests
clean; leveva-casemap 13 (6 unit + 7 fuzz) and the full leveva suite (60
binaries) green.
Carve the two most self-contained, runtime-free modules out of the monolithic
leveva crate into their own workspace crates:
- leveva-message: the RFC 1459 / IRCv3 wire Message, its builder and parser.
The API is generic over impl Display, so it carries no dependency on leveva's
typed identifiers or server runtime (only std + unicode-segmentation + serde).
- leveva-patricia: the generic, safe, arena-backed Patricia (radix) trie for
longest-prefix matching. Zero intra-crate coupling, std-only.
leveva re-exports each as leveva::message / leveva::patricia (pub use ... as ...),
so every existing crate::message::* and crate::patricia::* path and the crate-root
re-exports are unchanged — no consumer churn. Doc intra-links to other leveva
modules became plain code spans and the doctest/test imports point at the new
crate names; the message test's typed-id stand-in is a local Display newtype.
Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests clean;
leveva-message 40+3, leveva-patricia 14+1, and the full leveva suite all green.
Slice 309 made +M (operpeace) oper-only and updated the mode.rs unit
assertion, but this duplicate check in exlimit_proptest.rs still asserted
that only +P and +L are oper-only, so it failed once OperPeace joined the
set. Fold OperPeace into the expectation and rename to match.
operpeace_proptest (3 props: total + model-equal over the boolean cube; an oper
kicker is never blocked; a non-+M channel never blocks) + golden_operpeace
(boot: a non-oper chanop's KICK of an oper on +M -> 482; an oper kicker bypasses).
Port charybdis extensions/chm_operpeace.c: a +M channel flag prohibiting a
non-operator from kicking an IRC operator. New ChanMode::OperPeace (letter M,
high-half bit, oper-only to set like +P/+L); pure seam channel::operpeace_blocks
+ Channels::operpeace_enabled; shared command::{operpeace_protects,
operpeace_refusal} enforced in KICK and REMOVE (482 + a +s audit snotice, victim
untouched). OKICK bypasses (oper-gated). Refusal reuses 482 not charybdis 484
(leveva 484 = ERR_RESTRICTED). isupport/registration asserts + 10 CHANMODES/
MYINFO snapshots regenerated for the new M token.
Each triple now also gets leveva-<ver>-<triple>.tar.gz carrying the
same payload as the package asset tables in leveva/Cargo.toml (binary,
systemd unit, default config + MOTD, example config, license), for
platforms without dpkg/rpm. Collected into var/dist/ with the packages.
Assisted-by: Claude Fable 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Update the user-mode-wiring checklist: the self-settable char arm and
umode_diff echo table are no longer two hardcoded letter lists but derive
from UserMode::is_self_settable / is_client_settable, and the operator
check is now mode::is_oper. Reflects the wave-2 review cleanups so future
umode work targets the predicates instead of re-pairing allowlists.
Assisted-by: Claude Fable 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
The freely client-settable umode set was hardcoded twice in command/mode
.rs — the MODE char match arm and the umode_diff echo array — which must
agree or MODE silently applies a mode while echoing an empty diff (the
slice-251 +G bug). Derive both from predicates on UserMode:
is_self_settable (the plain set iwBxgGDRQC that both sites share, the
drift-prone part) and is_client_settable (that set plus the oper-gated
s/z/l, for the echo diff). Letter sets are byte-identical; add an
invariant test pinning both and asserting self_settable ⇒ client_settable
so no accepted letter can ever echo empty again.
The channel PRIVMSG/NOTICE fanout took the process-wide registry mutex
three times per recipient (modes_of, caps_of, deliver_uid) and
re-rendered/re-serialized the wire per member, so a message to a large
channel did ~3N lock cycles plus N serializations and collapsed on lock
contention. Add Registry::deliver_batch, which snapshots each member's
modes+caps and enqueues under a single lock pass, and have message.rs
memoize the rendered wire by capability-combination (at most 16 variants)
so it renders once per distinct cap combo instead of once per member.
Recipients, deaf/op-moderated/echo behavior, and ordering unchanged.
The compound operator test modes & (Oper.bit() | LocalOp.bit()) != 0
was hand-inlined at ~34 sites across the command and s2s planes, so any
change to what counts as an operator meant editing all of them and
risking drift. Add mode::is_oper beside the existing is_deaf/is_restricted
/is_operwall predicates and route every site (both != 0 and == 0
polarities) through it. Semantics-preserving.
History::record ran a synchronous rusqlite INSERT + JSON serialization
behind one process-wide Mutex<Connection>, inline on the async serve loop
for every channel PRIVMSG/NOTICE/JOIN/PART/QUIT; with a database{} block
enabled all traffic stalled behind the disk fsync and clients hit ping
timeouts. Hand rows to a dedicated writer thread over an mpsc channel
(coalescing queued inserts into one transaction); reads and REDACT
deletes dispatch over the same FIFO channel so ordering stays correct.
TAGMSG's target loop was a hand-copy of message()'s that dropped both
the MAX_TARGETS cap (ERR_TOOMANYTARGETS/407) and the canonicalize_targets
dedup, so TAGMSG #c,#c,#c,... re-fanned to every member and re-relayed to
every peer per duplicate with no cap — a flood-amplification vector that
PRIVMSG refuses. Reuse message.rs's MAX_TARGETS and canonicalize_targets
(bumped to pub(crate)) so TAGMSG shares the exact same limit and dedup.
AuthPipeline::run awaited each module (ident/dnsbl/socks/webproxy) in
sequence, each with its own timeout, so worst-case registration latency
was the sum of the per-module timeouts instead of the max. Spawn the
independent probes on a JoinSet, then fold outcomes by original module
index so the ordering semantics (first-Username-wins, Deny short-circuit,
NoOpinion/timeout ignored) are byte-for-byte identical.
handle_webirc overwrote the single self.host field with the gateway
-supplied spoofed hostname and discarded the real client IP, so the
IP-keyed registration ban gates tested a hostname that never parses as
a CIDR — a client on a banned IP tunneling through a trusted WEBIRC
gateway evaded D-lines, IP K-lines, and config bans. Retain the real
end-user IP (webirc_real_ip, the charybdis sockhost analog) and check
both it and the spoofed display host at all three registration gates.
The Password::Plain verify branch used a short-circuiting byte-slice
compare (candidate.as_bytes() == p.as_bytes()) that leaked password
length and the matching-prefix length via response timing, letting an
attacker recover a plaintext OPER/link/allow/WEBIRC secret byte-by-byte.
Compare via fixed-width SHA-256 digests folded with subtle's
ConstantTimeEq so timing is independent of both prefix and length.
Promote subtle to a direct dependency (already resolved transitively).
The IRCv3 CAP-negotiation framework post: the one corner of IRC that
negotiates instead of assuming, and the locked scope rule that the
negotiation mechanism and each capability's behavior are separate slices.
Covers CapState (version-never-downgrades, the registration-suspend brake
that feeds post 21's two-gate finalize), the all-or-nothing REQ as a
prepare-then-commit transaction, the 25-field ClientCaps payoff, and the
honest wart: sasl/chathistory/sts break the static-table abstraction and
get injected at render time. leveva-native, no oracle.
Assisted-by: Claude Opus 4.8 (claude-opus-4-8)
Signed-off-by: Xe Iaso <me@xeiaso.net>
The navbar was a flat row of ten links. Collapse the eight
documentation pages into a no-JS <details> "Docs" disclosure menu,
leaving a tidy Docs / Blog / Source bar. Mark the current page and its
group via current_path so visitors know where they are.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Update the blog-series conventions memory: mark the Cadey/Mara
blockquote aside device retired (the whole back-catalog was
de-asided) so the next post matches. Also carries the earlier
working-tree notes documenting posts 018-022 and the move to the
zola site with TOML +++ frontmatter.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Update the series skill to match the de-asided posts and the audit
findings.
- Replace the "character asides as blockquotes" convention with an
explicit no-asides rule; both technical tangents and reaction beats
go inline in one first-person voice.
- Drop the now-obsolete blockquote clause from the hard paragraph rule.
- Expand the sound-like-a-person step with the five recurring tells the
series-wide audit surfaced, each with its fix.
- Add reaching-for-asides and the five tells to the anti-patterns.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Two voice changes applied across all 22 posts in the series.
First, a sound-like-a-person pass: a series-wide audit found five
recurring AI-shaped tells clustered at section seams — balanced
"not X, it's Y" antitheses, restate-the-point closers, recap
tricolons, honesty-signaling, and meta-narration. Flattened the
constructed instances while keeping the lived-in, opinionated spine.
Second, removed the Cadey/Mara blockquote asides (70 of them) and
folded each into the surrounding first-person narrative. Technical
tangents (file:line anchors, symbol names, the C-vs-Rust details)
and reaction beats now read as continuous prose. Re-checked the
no-successive-same-opening-letter rule after both passes.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
The next post in the C->Rust port series. Covers leveva's
charybdis-style extbans: how a ban stops being a static
nick!user@host string and becomes a predicate the server
evaluates fresh each time. Walks the three-state Verdict
(Invalid as a property of the mask), the JOIN-gate plus
speech-plane enforcement, the questions too unsafe to invert
into an exception ($r/$c), the $&/$| combinators, and the
documented divergences ($m=usermode, no $q quiet list). No
oracle behind it: ported from Elemental-IRCd by reading.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
The native-async-iauth post: the standalone iauth (post 005) and the
fork/execl launcher (post 013) both existed only to keep blocking auth
probes off the C ircd's single-threaded select loop. Under leveva's tokio
runtime a blocking probe is just an `.await`, so the whole process boundary
— pipe protocol, socketpair, launcher — is deleted, not ported. Covers the
two-method Module trait (the pipe-protocol baggage that fell off), the
concurrent-spawn + two-stash try_finalize seam (order-independent finalize),
the `~` username rule, the process-vs-task isolation trade given up on
purpose, and (like 019/020) trusting it with the oracle already switched off.
Assisted-by: Claude Opus 4.8 (claude-opus-4-8)
Signed-off-by: Xe Iaso <me@xeiaso.net>
The P8v finale: the actual switch-off of the differential oracle. Two
commits (P8u drops all C compilation, P8v deletes the C source tree +
freezes bindings.rs into ircd-common via the extern-crate-self alias),
the irreversibility of running the oracle for the last time, the final
run's one red being the reference's own uninitialized-sendq STATS bug
(Rust is the correct side), and the honest downgrade from differential
to snapshot (weaker net, sufficient once faithfulness stops being the
goal). Grounded in docs/progress-log/p8.md + commits c1f72bb6/b88264f3.
Assisted-by: Claude Opus 4.8 (claude-opus-4-8)
Signed-off-by: Xe Iaso <me@xeiaso.net>
Post 19 — the first leveva-feature post with no oracle behind it. Netsplit-merge
arbitration: channel-TS and user-TS, oldest (lowest) timestamp wins by every
server independently comparing the same two integers (no coordinator, the whole
consensus protocol is `their_ts < our_ts`). The dark turn: the loser is wiped, not
merged — younger chanops deopped, the loser's +b/+e/+I AND +R lists wiped wholesale
(the reop-list hard rule I overruled myself on, plus the EOB reop sweep that fix
forced), and a younger nick renamed to its UID via the cross-task ForceNick
primitive. The equal-timestamp SaveBoth case (tie = mutual destruction, nick
vacated). Honest close: no differential here — confidence comes from proptests,
the boot-golden, and matching the decades-old TS6 design intent.
Assisted-by: Claude <noreply@anthropic.com>
Signed-off-by: Xe Iaso <me@xeiaso.net>
Post 018 narrates the roadmap pivot: with all C deleted (P8v), the planned
in-place idiomatic cleanup (P9) is retired in favor of a greenfield rewrite
(leveva) pinned differentially against the now-finished faithful port, which
becomes the behavioral oracle. The strangler pattern re-aimed one level up.
Assisted-by: Claude (claude-opus-4-8)
Signed-off-by: Xe Iaso <me@xeiaso.net>
The chantrace/masktrace golden tests snapshot the 262 RPL_TRACEEND line,
which embeds VERSION (concat!("leveva-", CARGO_PKG_VERSION)). The v0.3.0
bump changed that token from leveva-0.2.0 to leveva-0.3.0, breaking both
snapshots on every release.
Mask the version column to V, mirroring how these tests already mask the
volatile host/ip columns of 708 lines, so future version bumps no longer
break them.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Continue the leaf-crate carve-out (after leveva-casemap / leveva-string /
leveva-message / leveva-patricia / leveva-cidr / leveva-cloak) by moving the IRC
glob matcher + nick!user@host hostmasks out of the monolith into its own
workspace crate:
- leveva-matching: the C match.c cluster (matches / Pattern / collapse / HostMask
/ host_component_matches / allow_host_matches, 498 LoC). The survey-flagged
"natural next pick": once leveva-cidr split, its only intra-crate edges were
casemap::to_lower and cidr::cidr_match — both already crates — plus nom. Deps =
leveva-casemap + leveva-cidr + nom.
Body moved verbatim via git mv; five repoints only: `use crate::casemap;` ->
`use leveva_casemap as casemap;`, `crate::cidr::cidr_match` ->
`leveva_cidr::cidr_match`, the module-header intra-doc link (crate::casemap) ->
(leveva_casemap), and two doctests `use leveva::matching::…` ->
`use leveva_matching::…`. leveva re-exports it as leveva::matching
(`pub use leveva_matching as matching;`), so the flat
`pub use matching::{collapse, matches, HostMask, Pattern};` and all ~13 consumers
(xline / kline / resv / webirc / metadata / registration +
command/{oper,map,mask,hunt,dline,tkline}) keep their crate::matching::* paths.
No dependency cycle — the code edges all run into matching.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching the other leaves. leveva's matcher deliberately
diverges from match.c (RFC 1459 case-fold, not ASCII tolowertab; #=any-digit), so
there is NO byte-faithful differential oracle — the 13 proptest invariants ARE the
gate. Fed arbitrary (incl. non-ASCII) strings + glob masks: totality/no-panic over
every entry point, `*` is universal, `?`^k iff exact byte-length, case-insensitivity
under leveva_casemap::fold of the name, compile == matches, collapse is
matching-preserving + idempotent + non-growing, HostMask re-parses its own Display,
matches_target == matches_parts, allow_host_matches decomposes into user-glob &&
host-component, and a glob host component falls back to matches.
Gate: cargo build --workspace (0 warnings) · cargo clippy --workspace --tests
(clean) · cargo test -p leveva-matching (28 unit/collapse/escaping + 13 fuzz, + 2
doctests) green · leveva matching-consumer tests (xline/kline/resv/webirc, 181) green.
What else can move out: uid (only crate::ident, blocked on extracting ident — ident
itself blocked by a single crate::command reference worth auditing, the next unlock);
msgid pulls cap/clock/ident; clock is a small candidate leaf. Recorded in the plan +
progress log.
Continue the leaf-crate carve-out (after leveva-casemap / leveva-string /
leveva-message / leveva-patricia / leveva-cidr) by moving the +x hidden-host
transform out of the monolith into its own workspace crate:
- leveva-cloak: the deterministic FNV-keyed IP/host cloak (cloak / cloak_ip /
cloak_host), a faithful port of Elemental-IRCd's ip_cloaking.c. The cleanest
remaining leaf, exactly as the leveva-cidr survey predicted ("zero intra-crate
deps — perfect standalone leaf, strongest next pick"): it depends only on
std::net::IpAddr and on nothing in the workspace. Its only crate::-references
were doc comments — the actual code edges all run *into* it
(command::mode::apply_cloak_change at mode.rs:916 and session.rs:1786 call
crate::cloak::cloak), so no cycle.
leveva re-exports it as leveva::cloak (`pub use leveva_cloak as cloak;`), so every
crate::cloak::* / leveva::cloak::* path is unchanged across consumers. Body moved
verbatim via git mv — no doctest, and every intra-doc link (cloak/cloak_ip/
cloak_host/IP_CHARTABLE/HOST_ALPHABET + the FNV reference) resolves in-module, so
nothing repointed.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching casemap/string/cidr. And since the C 2.11 oracle has
no +x usermode, there is NO differential oracle here — the 10 proptest invariants
ARE the gate, not a supplement. Fed arbitrary (incl. non-ASCII) strings and the
full v4/v6 address space: determinism (the ban-on-a-cloak-keeps-biting contract),
the leading-colon wire-safety biconditional (a non-: input never grows a leading
colon, a :-input is returned verbatim), length preservation for both cloak_ip and
cloak_host, separator/dot byte-index invariance, IP-literal dispatch to cloak_ip,
the g-z IP alphabet (v4 keeps its first two octets; v6 is hex/:/g-z with no leading
colon), and fnv_hash_32 == charybdis's explicit shift-decomposition over arbitrary
bytes.
What else can move out: matching (now a clean casemap+cidr leaf, pulls nom — the C
match.c cluster, natural next pick); uid (only crate::ident, blocked on extracting
ident); msgid pulls cap/clock/ident. Recorded in the plan + progress log.
Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests clean;
leveva-cloak 18 (8 unit + 10 fuzz) green; leveva +x consumer tests (command::mode
plus_x suite + session registers-cloaked + the s2s +s+x spy, 8) green.
Carve the pure CIDR network-mask helpers (parse_cidr / contains /
network_base / cidr_match) out of leveva into a self-contained
leveva-cidr leaf crate, re-exported as `leveva::cidr` — continuing the
post-P12 leaf-crate carve-out after casemap/string/message/patricia.
cidr is a genuinely pure leaf: zero workspace deps, only std::net. This
corrects the earlier leveva-string survey, which filed matching+cidr as
"mutually referential, not a clean single leaf yet" — cidr's references
to matching/connlimit are doc-comment-only; the real edge is one-way
matching -> cidr. Extracting cidr therefore *unblocks* matching, whose
only remaining intra-crate deps are now casemap + cidr (both crates).
git mv leveva/src/cidr.rs -> leveva-cidr/src/lib.rs verbatim except the
doctest import and two cross-crate doc links; leveva/src/lib.rs swaps
`pub mod cidr;` for `pub use leveva_cidr as cidr;`. All four call sites
use fully-qualified crate::cidr::*, so zero consumer churn.
Fuzzing (the user ask): 10 proptest invariants over the full v4/v6
address space and arbitrary in-range prefixes, all consistency/inverse
properties — parse round-trip + out-of-range reject, contains reflexive
through network_base, network_base idempotent, contains prefix-monotone,
/0 universal + full-length exact, family mismatch never contains, and
cidr_match == parse + contains.
Next extraction candidates: matching (now a clean leaf, the C match.c
cluster), cloak (zero intra-crate deps), uid (blocked on ident).
Gate: cargo build -p leveva 0 warnings; cargo clippy -p leveva-cidr
--tests clean; cargo test -p leveva-cidr 18 (8 unit + 10 fuzz) + 1
doctest green; leveva cidr-consumer tests (matching/connlimit/websocket,
54) green.
Continue the leaf-crate carve-out (after leveva-message / leveva-patricia /
leveva-casemap) by moving the casemapping-aware IRC string types out of the
monolith into their own workspace crate:
- leveva-string: IrcStr / IrcString (str/String wrappers whose equality,
ordering, and hashing fold under RFC 1459) + IrcError + IrcStringBuilder. The
architectural #2 leaf in the string cluster — now that casemap is its own crate,
string depends on exactly one workspace crate (leveva-casemap) plus core/std,
while ident/channel/registry/whowas all depend on it. No cycle: leveva-message
uses neither, leveva-casemap depends only on core.
leveva re-exports it as leveva::string (`pub use leveva_string as string;`), and
the existing `pub use string::{IrcError, IrcStr, IrcString, IrcStringBuilder};`
re-exports straight out of the new crate — so every crate::string::* /
leveva::string::* path and the four flat re-exports are unchanged across all
consumers (the full dependent build confirms zero churn). Body moved verbatim
except `use crate::casemap;` -> `use leveva_casemap as casemap;` and the two
doctests' `use leveva::...;` -> `use leveva_string::...;`.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching casemap/message. Added 6 consistency/inverse
invariants fed arbitrary (incl. non-ASCII) strings: eq lifts casemap (the wrapper
adds no equality of its own), Ord consistent-with-Eq + antisymmetric, Hash agrees
with Eq (the HashMap-key contract behind the nick/channel tables), Borrow<IrcStr>
round-trip (insert under one casing, find by a case-different borrowed key),
validation soundness (accept iff no NUL/CR/LF, FromStr agrees, storage
byte-identical on success), and builder = deferred try_from.
What else can move out: cloak (zero intra-crate deps — perfect standalone leaf,
strongest next pick); uid (only crate::ident); matching+cidr are a coupled pair;
msgid pulls cap/clock/ident. Recorded in the plan + progress log.
Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests clean;
leveva-string 13 (7 unit + 6 fuzz) + 2 doctests green; full leveva suite (413
binaries) green.
Continue the leaf-crate carve-out (after leveva-message / leveva-patricia) by
moving the RFC 1459 case-folding module out of the monolith into its own
workspace crate:
- leveva-casemap: byte/slice fold, compare, hash, and the owned-key `fold`. It
imports only `core` (Ordering + Hasher) and is the lowest leaf in the string
cluster — string/ident/matching/channel/registry/whowas depend on it, it on
nothing. std-only lib, proptest dev-dep.
leveva re-exports it as leveva::casemap (`pub use leveva_casemap as casemap;`),
so every crate::casemap::* / leveva::casemap::* path across ~40 source and test
files is unchanged — no consumer churn.
Fuzzing: stable toolchain only (no nightly / no cargo-fuzz), so proptest is the
runnable fuzz tier, matching leveva-message. Added 7 consistency/inverse
invariants fed arbitrary bytes and arbitrary (incl. non-ASCII) strings:
fold ⇔ eq_ignore_case both directions, cmp==Equal ⇔ eq, cmp antisymmetry,
hash-agrees-with-eq (prefix-free 0xff marker exercised), fold idempotent +
UTF-8-safe + byte-length-preserving, high bytes (≥0x80) identity, and
to_upper∘to_lower identity on the uppercase domain over the full byte range.
Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests
clean; leveva-casemap 13 (6 unit + 7 fuzz) and the full leveva suite (60
binaries) green.
Carve the two most self-contained, runtime-free modules out of the monolithic
leveva crate into their own workspace crates:
- leveva-message: the RFC 1459 / IRCv3 wire Message, its builder and parser.
The API is generic over impl Display, so it carries no dependency on leveva's
typed identifiers or server runtime (only std + unicode-segmentation + serde).
- leveva-patricia: the generic, safe, arena-backed Patricia (radix) trie for
longest-prefix matching. Zero intra-crate coupling, std-only.
leveva re-exports each as leveva::message / leveva::patricia (pub use ... as ...),
so every existing crate::message::* and crate::patricia::* path and the crate-root
re-exports are unchanged — no consumer churn. Doc intra-links to other leveva
modules became plain code spans and the doctest/test imports point at the new
crate names; the message test's typed-id stand-in is a local Display newtype.
Gate: cargo build --workspace 0 warnings; cargo clippy --workspace --tests clean;
leveva-message 40+3, leveva-patricia 14+1, and the full leveva suite all green.
Port charybdis extensions/chm_operpeace.c: a +M channel flag prohibiting a
non-operator from kicking an IRC operator. New ChanMode::OperPeace (letter M,
high-half bit, oper-only to set like +P/+L); pure seam channel::operpeace_blocks
+ Channels::operpeace_enabled; shared command::{operpeace_protects,
operpeace_refusal} enforced in KICK and REMOVE (482 + a +s audit snotice, victim
untouched). OKICK bypasses (oper-gated). Refusal reuses 482 not charybdis 484
(leveva 484 = ERR_RESTRICTED). isupport/registration asserts + 10 CHANMODES/
MYINFO snapshots regenerated for the new M token.