The wire layer of design/per-client-access.md: quic/access.rs carries the
GRANT_* bits (u32, reserved-must-be-zero), the three presets, and the
exhaustive InputKind -> GrantClass classifier whose wildcard-free match is
the default-deny mechanism — a new input kind now fails to compile until
someone decides its grant class.
AccessUpdate { grants, remaining_secs } rides the control stream as 0x58
(verified free; both peers' dispatch loops drop unknown ids with a warn,
so old clients just miss the courtesy). reject.rs grows the 0x69/0x6A
close codes -> RejectReason::{AccessExpired, LaunchNotPermitted}, mirrored
into PunktfunkStatus -30/-31 and the shared client-facing sentences.
The Welcome advertises grants + expires_in_secs as trailing fields one
link past mgmt_port, with the same placeholder discipline: emitting the
advert forces the cipher byte and the mgmt port so the two u32s land at a
deterministic offset, while a full-control permanent session stays
byte-identical to the pre-grants wire form. Absent fields decode to
GRANT_ALL / permanent — exactly what an old host enforces. No
WIRE_VERSION bump (trailing fields, per the v20 mgmt_port precedent), no
ABI bump (no new C symbols; the header gains prefixed defines and two
appended status values only). The host sends GRANT_ALL until WP2/WP3
wire the trust store in.