feat(leveva): caller-id (+g user mode + ACCEPT) — P11 slice 216
Add charybdis-style caller-id ("server-side ignore"): a user sets +g to
receive private PRIVMSG/NOTICE only from clients on their ACCEPT list. A
blocked PRIVMSG bounces 716 to the sender and notifies the target once
(717 to sender + 718 to target); a NOTICE is silently dropped.
The pure CallerId store (accept set + once-per-pair notify gate) lives
inside Registry — the metadata precedent — so it costs no ServerContext
churn; accept entries are stable UIDs (survive nick changes, quit-filtered
on read), capped at 30. +g is purely local (not in SEND_UMODES): the gate
runs on the target's home server, so a transit/origin server needs no copy.
ACCEPT supports add / -remove (mixed) / list (281+282), with 401/456/457/458;
self-accept is a no-op. CALLERID=g advertised in 005; 8 new numerics.
Picked after confirming no clean documented deferral remained open — the
STS-REHASH / S2S-KILL-notice / throttle-knob / MODE+VERSION-proptest /
UTF8ONLY "deferred" doc comments were all stale, closed by later slices.
Remote-sender gating (S2S) and +G soft caller-id are documented follow-ons.
Tests: callerid unit (6), accept (8), message gate (5), numeric round-trip,
isupport token, s2s/umode excludes +g; golden_callerid (2); callerid_proptest
(2, accept-list + notify model lockstep). Boot snapshots regenerated for the
new 004 'g' umode letter and the 005 CALLERID=g token.
Assisted-by: Claude Opus 4.8 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
feat(leveva): CHANTRACE channel-scoped extended trace (P11 slice 287)
The channel-scoped sibling of TRACE/ETRACE/MASKTRACE, completing the trace
quartet: CHANTRACE <#channel> reports every member of a named channel in
ETRACE's extended 708 column format (folded-nick order), closing 262.
Gate (charybdis-faithful ordering): 461 (missing channel) -> 403 (no such
channel, existence before membership) -> 442 unless the requester is a member
or holds OperPrivilege::Trace (the operspy-equivalent bypass). Reuses
etrace::etrace_line, no new numeric.
Divergences (leveva-native, no oracle): emits 708 not charybdis 709; reports
the full roster incl. remote members (unlike local-only ETRACE/MASKTRACE) but
no S2S propagation; no IP-hiding (host==host, no separate IP column).
Tests (TDD, inverse invariants + fuzzing): 11 unit (403-before-442, PART
removes match, non-member admitted once joined, outsider never appears),
golden_chantrace (real binary), chantrace_proptest (4 properties, 512 cases).
feat(leveva): CYCLE — self-only channel refresh (P11 slice 295)
charybdis-style `CYCLE <channel>` (extensions/m_cycle.c): part and rejoin a
channel *without races*. Faithful to the real m_cycle, the effect is entirely
client-side for the issuer — `:source PART <chan> :Cycling`, `:source JOIN
<chan>`, then a fresh 353/366 — and nothing changes server-side, so the user
keeps op/voice and place and cannot be locked out by +i/+k/+l or lose a glare.
No new Channels seam (CYCLE never mutates): it composes the read-only
display_name/is_member/names_query accessors. Per-channel gate ladder:
461 no param → 403 nonexistent → 442 not-a-member → success, existence before
membership; the re-sent NAMES honours the issuer's multi-prefix/userhost-in-names.
Divergences (leveva-native, no oracle): self-only/observationally-pure (NOT
member-visible — that would drop ops, destroy a sole-member channel, and desync
peers), fixed :Cycling reason, local-only.
7 command units + cycle_proptest (observational-purity + arbitrary-args) +
golden_cycle; CYCLE.md help + COMMANDS allowlist + index.md.
feat(leveva): DIE + RESTART + REHASH oper control-plane (P11 slice 25)
The first leveva commands that act on the whole process. Introduces a server
control plane (leveva/src/control.rs: a process-global ServerControl =
Notify + latching AtomicU8) — the process-level analogue of slice 19's
per-connection eject plane.
Faithful to the IRCnet 2.11 m_die/m_restart/m_rehash msgtab gate
({ m_nop, m_nopriv, m_<cmd>, m_nop, m_unreg }): a non-oper -> 481; an oper ->
the handler, which re-gates on is_allowed(ACL_DIE|RESTART|REHASH) -> also 481
without the flag. This wakes the dormant OperPrivilege machinery (parsed since
slice 16): Registered gains a `privileges` set, populated from the matched
block on a successful OPER, and each control command gates on its OperPrivilege
(a non-oper's empty set => 481, reproducing both oracle arms in one check).
- DIE/RESTART (shutdown_all): eject every registered client (issuer included)
with a `Server (Terminating|Restarting). <nick>[<user>@<host>]` NOTICE as the
eject wire, then control::request(Die|Restart). main's select! drains the
ejects over a 200ms grace then exits (Die) or reexec()s the binary (Restart,
CommandExt::exec; CLOEXEC sockets re-bind cleanly).
- REHASH: 382 RPL_REHASHING + re-read/validate the on-disk config.
Documented divergences: killer as <nick>[<user>@<host>]; single-server (no
server-link ERROR branch); RESTART re-exec is review-covered not golden-tested;
REHASH only validates the re-read (applying it to the immutable ServerContext
is deferred); no SCH_NOTICE; KILL/WALLOPS still gate on the oper bit only. No
leveva-integration differential (all three are impure into global buffers).
Verified: unit (control 8; die/restart/rehash 3 each w/ inverses; OPER stores
the set) + control_proptest (gate / eject-conservation / panic-freedom) +
golden_control (mortal 481, oper 382, oper DIE -> NOTICE then exit) + live
boots (DIE->EOF->exit; RESTART->re-exec->port re-bind). cargo test -p leveva
green (lib 468 + all golden/proptest bins), clippy/fmt clean, workspace 0
warnings, leveva-integration unchanged.
feat(leveva): D-lines (DLINE/UNDLINE) — IP-level operator bans (P11 slice 284)
charybdis-style D-lines, the IP/CIDR sibling of the temporary K-line family
(slices 281–283): `DLINE <duration> <ip/cidr> [:reason]` bans a raw IP or CIDR
block, `UNDLINE <ip/cidr>` lifts it. Built as a near-clone of the K-line plane
keyed on a single IP/CIDR `mask` (matched with the CIDR-aware
`matching::host_component_matches`) instead of `user@host`.
- `dline.rs`: `DlineStore` mirrors `KlineStore` (Vec+Mutex, NOCASE dedup,
`find_active`, same `database {}` SQLite write-through + boot reload as slice
282); `Dline::covers` = `host_component_matches`. Reuses the kline duration
grammar/clock/clamp.
- `command/dline.rs`: `dline`/`undline` mirror `tkline`/`untkline` (461→481 gate,
empty/`*` mask → Incorrect format, no success reply); `reap_matching_ip` ejects
local matching clients by `orighost` (real connect IP), skipping remote +
`kline-exempt`. New `OperPrivilege::Dline` (bit 0x400000).
- `session.rs`: registration gate checked before the K-line gate, matching the
pre-cloak connect IP → 465+ERROR+REJ snomask, never counted/claimed.
- `s2s/dline.rs`+`forward.rs`+`burst.rs`: `ENCAP * DLINE`/`UNDLINE` propagation
(server-prefixed, remaining-seconds, slice-281 KLINE shape) + burst
re-assertion.
- `stats.rs`: `STATS d` → `250 RPL_STATSDLINE` via `dline_report`.
- `server.rs`: `ctx.dlines` opens against the same `database {}` file (persists
across restart); boot test + inverse extended.
- help `DLINE.md`/`UNDLINE.md` + COMMANDS allowlist.
Tests (TDD, inverse invariants, mandated fuzzing): unit+proptest per module;
`tests/golden_dline.rs` (real binary, `dline.kdl` fixture) and
`tests/dline_proptest.rs` (6 properties incl. model-lockstep registration gate
over `Session::feed`). leveva-native, no oracle differential.
`cargo test -p leveva` green (2530 lib); clippy clean; workspace 0 warnings.
feat(leveva): charybdis alias{} services command aliases (P11 slice 303)
Port charybdis's alias{} mechanism (modules/m_alias.c + newconf.c alias_entry):
an `alias "<name>" { target "<t>" }` block makes <name> a command that rewrites
`<name> <text...>` into `PRIVMSG <target> :<text...>`, delivered through the message
plane. A real command always takes precedence (the hook is the existing 421 fallback).
Found by comparing against a charybdis checkout: the 'configurable IDENTIFY nicks'
originally scoped does not exist in charybdis (the nicks are hardcoded #defines); the
alias{} block is the real, faithful, more general mechanism.
- alias.rs pure seam: parse_target (split on first @ -> nick vs user@server),
combined_args / reconstruct; fuzzed by tests/alias_proptest.rs.
- new top-level alias{} config block (model/parse/from_kdl), held live + REHASH-able
through ConfStore (live_aliases, the service-string chain); main.rs boot seed.
- command/alias.rs: deliver_to_service_nick shared core (target must be a present +S
service else 440 ERR_SERVICESDOWN; empty text -> 412; else synth PRIVMSG +
message::message, so a remote service is reached by UID routing). new numeric 440.
- folds in the slice-302 IDENTIFY faithfulness fix the comparison surfaced: empty args
-> 412 (was 461), absent/non-+S agent -> 440 (was 401), via the shared core.
- user@server target parsed/fuzzed but not yet routed (-> 440, documented follow-on).
Tests: alias seam + command + config-parse units; identify units updated; extended
rehash_conf_proptest alias reload; golden_alias (S2S +S burst: route + 440 + 421
inverses); golden_identify rewritten (S2S +S NickServ/ChanServ).
cargo test -p leveva green / clippy clean / build --workspace 0 warnings.
feat(leveva): TIME + ADMIN + INFO commands (P11 slice 22)
Server-information query commands, ported as a pure read-only trio (previously
caught by dispatch's 421 fallback). All three reach the CLIENT and OPER msgtab
columns (no oper gate) and ignore the single-server hunt_server target arg.
- TIME -> a single 391; the time renders as unix seconds (leveva's wire-time
convention, like 333/312/314). Pure time_at(now) core + a thin clock-reading
wrapper.
- ADMIN -> the 256/257/258/259 block from a new ServerContext.admin config
field (257=name, 258=location, 259=email); 423 when all fields are empty
(unreachable -- the admin block is mandatory with name+email required).
- INFO -> leveva's own 371* description block (with an On-line since <created>
line) then 374; leveva content, oracle shape (as with VERSION's URL).
The new admin field touched the 19 proptest struct-literal sites. Canonicalizer
gained a 391-seconds + On-line-since mask. Verified by unit tests, a
golden_time_admin_info boot golden, and a 4-property server_query_proptest
(512 cases): TIME shape, ADMIN block-xor-423, INFO 371*-then-374, and
arbitrary-input panic-freedom.
No leveva-integration differential (m_time/m_admin/m_info sendto_one into global
buffers -> no pure oracle entry point).
Gate: cargo test -p leveva green (lib 429 + all golden/proptest binaries);
clippy --all-targets clean; fmt clean; build --workspace 0 warnings.
feat(leveva): IRCv3 draft/read-marker + the MARKREAD command (P11 slice 300)
The read-state companion to draft/chathistory (slice 274). MARKREAD
<target> [timestamp=<iso8601>] records or queries how far a client has
read a target; a set stores max(existing, new) (monotonic — never
regresses), echoes the effective value, and on a genuine advance fans
the MARKREAD to the user's other capable connections (cross-device
read-state sync). A bare MARKREAD <target> queries the stored marker.
Markers are owned by the services account (synced across connections,
survives a reconnect) when logged in, else by the connection UID
(per-connection, dropped on Session::release). The cap is statically
advertised and valueless; the command is processed regardless of the
issuer's own cap — the cap gates only the cross-connection fan, per spec.
leveva-native: the C 2.11 oracle predates IRCv3, so the gate is unit +
golden + proptest (no differential).
- readmarker.rs: pure parse/format seam (parse_set_timestamp /
format_value) + the monotonic ReadMarkers store (account|UID owner key)
- command/markread.rs: the handler (NEED_MORE_PARAMS / query / set /
INVALID_PARAMS / advance + fan)
- cap.rs: DRAFT_READ_MARKER const + SUPPORTED entry + read_marker
ClientCaps bit; CAP LS snapshots refreshed (a long SASL mechlist now
tips the 302 LS into a spec-legal multiline split)
- server.rs: read_markers field; session.rs: forget_connection release hook
- help/MARKREAD.md + COMMANDS allowlist + index.md Queries line
- tests: readmarker/markread units (inverse invariants), golden_markread
(single-conn round-trip + cross-device push vs uncapped sibling),
readmarker_proptest (total parse + round-trip; advance keeps-max,
order-independent + idempotent)
feat(leveva): MASKTRACE mask-filtered extended trace (P11 slice 286)
Charybdis-style `MASKTRACE <nick!user@host mask> [<gecos mask>]` — the
mask-filtered sibling of TRACE (26) and ETRACE: reports every local client whose
nick!user@host glob-matches (and optionally whose realname matches a 2nd gecos
glob) in ETRACE's extended column format. 461 param gate before the
OperPrivilege::Trace oper gate (481); one reused 708 RPL_ETRACEFULL line per
match in folded-nick order via matching::HostMask + the promoted
etrace::etrace_line/class_name, closing with 262 RPL_TRACEEND (no new numeric).
Leveva-native (IRCnet 2.11 has no MASKTRACE), local-only (no S2S, mirroring the
charybdis model). Emits 708 not charybdis's 709 (same divergence ETRACE
documents); class/ip/XLINE columns identical to ETRACE.
Tests (TDD, inverse invariants + fuzzing): 11 unit tests (gates, folded-order
sweep, full columns, and the inverses — non-matching host excluded, QUIT removes
the match, gecos filter excludes/re-includes, no-match is just the 262);
golden_masktrace.rs (real binary snapshot); masktrace_proptest.rs (4 properties
x 512 cases). Help page + COMMANDS allowlist entry.
Gate: cargo test -p leveva green; clippy clean; cargo build --workspace 0
warnings.
refactor(leveva): split command.rs into command/ folder, one handler per file
The post-registration dispatch grew to a 2.4k-line monolith. Break it into
src/command/<cmd>.rs (one client command handler per file) with each handler's
unit tests local to that file, behind the shared command/mod.rs that owns
Registered, dispatch, the cross-handler reply helpers (mask, need_more_params,
no_such_channel, topic_reply, names_reply, …) re-exported pub(crate), and a
#[cfg(test)] testutil module with the shared fixtures (ctx/client/run/uid_for/
claim_observed/delivered/claim_and_join/force_flag).
Pure mechanical move: no behavior change. cargo test -p leveva 301 lib +
golden/proptest green, clippy clean, build 0 warnings.
feat(leveva): DIE + RESTART + REHASH oper control-plane (P11 slice 25)
The first leveva commands that act on the whole process. Introduces a server
control plane (leveva/src/control.rs: a process-global ServerControl =
Notify + latching AtomicU8) — the process-level analogue of slice 19's
per-connection eject plane.
Faithful to the IRCnet 2.11 m_die/m_restart/m_rehash msgtab gate
({ m_nop, m_nopriv, m_<cmd>, m_nop, m_unreg }): a non-oper -> 481; an oper ->
the handler, which re-gates on is_allowed(ACL_DIE|RESTART|REHASH) -> also 481
without the flag. This wakes the dormant OperPrivilege machinery (parsed since
slice 16): Registered gains a `privileges` set, populated from the matched
block on a successful OPER, and each control command gates on its OperPrivilege
(a non-oper's empty set => 481, reproducing both oracle arms in one check).
- DIE/RESTART (shutdown_all): eject every registered client (issuer included)
with a `Server (Terminating|Restarting). <nick>[<user>@<host>]` NOTICE as the
eject wire, then control::request(Die|Restart). main's select! drains the
ejects over a 200ms grace then exits (Die) or reexec()s the binary (Restart,
CommandExt::exec; CLOEXEC sockets re-bind cleanly).
- REHASH: 382 RPL_REHASHING + re-read/validate the on-disk config.
Documented divergences: killer as <nick>[<user>@<host>]; single-server (no
server-link ERROR branch); RESTART re-exec is review-covered not golden-tested;
REHASH only validates the re-read (applying it to the immutable ServerContext
is deferred); no SCH_NOTICE; KILL/WALLOPS still gate on the oper bit only. No
leveva-integration differential (all three are impure into global buffers).
Verified: unit (control 8; die/restart/rehash 3 each w/ inverses; OPER stores
the set) + control_proptest (gate / eject-conservation / panic-freedom) +
golden_control (mortal 481, oper 382, oper DIE -> NOTICE then exit) + live
boots (DIE->EOF->exit; RESTART->re-exec->port re-bind). cargo test -p leveva
green (lib 468 + all golden/proptest bins), clippy/fmt clean, workspace 0
warnings, leveva-integration unchanged.
feat(leveva): IRCv3 draft/read-marker + the MARKREAD command (P11 slice 300)
The read-state companion to draft/chathistory (slice 274). MARKREAD
<target> [timestamp=<iso8601>] records or queries how far a client has
read a target; a set stores max(existing, new) (monotonic — never
regresses), echoes the effective value, and on a genuine advance fans
the MARKREAD to the user's other capable connections (cross-device
read-state sync). A bare MARKREAD <target> queries the stored marker.
Markers are owned by the services account (synced across connections,
survives a reconnect) when logged in, else by the connection UID
(per-connection, dropped on Session::release). The cap is statically
advertised and valueless; the command is processed regardless of the
issuer's own cap — the cap gates only the cross-connection fan, per spec.
leveva-native: the C 2.11 oracle predates IRCv3, so the gate is unit +
golden + proptest (no differential).
- readmarker.rs: pure parse/format seam (parse_set_timestamp /
format_value) + the monotonic ReadMarkers store (account|UID owner key)
- command/markread.rs: the handler (NEED_MORE_PARAMS / query / set /
INVALID_PARAMS / advance + fan)
- cap.rs: DRAFT_READ_MARKER const + SUPPORTED entry + read_marker
ClientCaps bit; CAP LS snapshots refreshed (a long SASL mechlist now
tips the 302 LS into a spec-legal multiline split)
- server.rs: read_markers field; session.rs: forget_connection release hook
- help/MARKREAD.md + COMMANDS allowlist + index.md Queries line
- tests: readmarker/markread units (inverse invariants), golden_markread
(single-conn round-trip + cross-device push vs uncapped sibling),
readmarker_proptest (total parse + round-trip; advance keeps-max,
order-independent + idempotent)
feat(leveva): enforce advertised TOPICLEN on TOPIC set — grapheme-cap truncation — P11 slice 270
leveva advertised TOPICLEN=255 in 005/ISUPPORT but command::topic recorded a
TOPIC set of any length verbatim — the cap was cosmetic. charybdis/the IRCnet
oracle enforce it by copying the new topic into a fixed char topic[TOPICLEN+1]
buffer (silent truncation). leveva now honours its own advertised cap.
- isupport::truncate_topic(text) -> &str: pure, returns the longest prefix that
is at most TOPICLEN grapheme clusters (UAX #29), whole when it fits. Mirrors the
LINELEN cut in Message::to_wire; a grapheme boundary is a char boundary, so the
result is always valid UTF-8.
- command::topic truncates once at the top of the set branch, so the echo, the
local-member fan-out, the stored topic, the later 332 query, and the S2S relay
all carry the same text. A topic arriving over S2S is trusted (its origin
already truncated it), exactly as charybdis trusts a remote TOPIC.
Divergences: grapheme cap not byte cap (leveva's uniform length unit, slice 121);
home-server enforcement only; truncate, not reject (no ERR_* for "topic too long").
Tests (RED first): topic_set_truncates_to_topiclen / _at_exactly_topiclen_is_untouched
/ _truncates_multibyte_on_a_grapheme_boundary; isupport truncate_topic_caps_and_
passes_through + fuzz truncate_topic_proptest (byte-prefix, <= cap, untouched-iff-fits,
idempotent over arbitrary input).
Gate: cargo test -p leveva green; cargo clippy -p leveva --tests clean; cargo build
--workspace 0 warnings.