Commits
Append a totalCostUSD key at the bottom of the JSON output, summing
every model's cost and formatting it as US dollars via Intl.NumberFormat
so the all-time spend is readable at a glance. Models with unknown
pricing are excluded from the sum (they already warn on stderr).
Assisted-by: Claude Fable 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Sums Claude Code token usage across .claude/sessions/**/*.jsonl and
emits a per-model JSON report to stdout: cached/uncached/total input,
output, and total tokens, plus USD costs at current API list prices.
Cache writes are priced at their TTL premium (2x for 1h, 1.25x for 5m)
using the usage.cache_creation breakdown; cache reads at 0.1x input
rate. Sonnet 5 uses its introductory price through 2026-08-31. The
<synthetic> pseudo-model is skipped and unknown models report null
costs with a stderr warning instead of guessing.
Assisted-by: Claude Fable 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
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.
Append a totalCostUSD key at the bottom of the JSON output, summing
every model's cost and formatting it as US dollars via Intl.NumberFormat
so the all-time spend is readable at a glance. Models with unknown
pricing are excluded from the sum (they already warn on stderr).
Assisted-by: Claude Fable 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Sums Claude Code token usage across .claude/sessions/**/*.jsonl and
emits a per-model JSON report to stdout: cached/uncached/total input,
output, and total tokens, plus USD costs at current API list prices.
Cache writes are priced at their TTL premium (2x for 1h, 1.25x for 5m)
using the usage.cache_creation breakdown; cache reads at 0.1x input
rate. Sonnet 5 uses its introductory price through 2026-08-31. The
<synthetic> pseudo-model is skipped and unknown models report null
costs with a stderr warning instead of guessing.
Assisted-by: Claude Fable 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
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.