NA-0678 (D614): relay invite slots, capability gate, and a durability proof that can fail - #66
Merged
Merged
Conversation
… proof that can fail
Messaging epic Slice 1 -- the relay-side primitives the client slices will need,
built with no client dependency. Three additive routes, one new table, and a
slot-scoped admission check on the existing push path.
The invite subsystem
--------------------
POST /v1/invite/{create,redeem,revoke}. All three carry invite_id and every
secret in the JSON BODY, never a path or query parameter: invite_id IS the
mailbox route key, and D-0008/D-0009/D-0010 already retired URI-carried route
tokens because they leak through proxy logs, shell history and traces.
The CLIENT mints the redemption capability and uploads only SHA-256(cap). There
is deliberately no mint endpoint, so no relay-side path holds a capability in
plaintext before a redeemer presents one -- a relay operator cannot silently
burn an invite it hosts. invite_id, cap, revoke_token and the handshake ticket
are persisted only as digests and compared with the existing ct_eq_secret
(D-0014); no new primitive, no new dependency.
Redemption is an atomic compare-and-set -- exactly one winner, every loser gets
ERR_INVITE_ALREADY_USED -- and issues a one-shot handshake ticket. A push to an
invite slot without a live ticket is refused, so the slot accepts exactly one
handshake from the party that actually redeemed rather than from anyone who saw
the code and lost the race. A 128-bit revoke_token returned once at create
authorizes revoke; without it an open relay would let any code-holder destroy
any invite.
Consumed and revoked slots are TOMBSTONED until expiry with their blobs cleared,
so invite-already-used stays distinguishable from invite-not-found. A deleted
slot would report "never existed" when the truth is "someone got here first" --
the interception signal the design exists to surface.
The slot cap bounds storage and never evicts when full; a GLOBAL create-rate
bucket bounds denial. Global because an invite has no route token until it
exists, so the per-route push bucket structurally cannot cover it. The two are
not substitutes: the availability of invite-create is a security property.
/v1/push, /v1/pull and /v1/pull/ack are UNCHANGED for every route the invite
system did not create -- one indexed lookup misses and the pre-existing path
runs as before. That is what lets the shipped client, the spine's pinned e2e and
the attachments contract keep working while the client slices are unwritten.
Two defects the census found, fixed here
----------------------------------------
The store's schema-version marker was written with INSERT OR IGNORE, a no-op on
an existing key, so a forward migration never advanced it and D-0011's
fail-closed downgrade guard had been inert since it was written. The migration
now advances it, with a positive and a negative control.
tests/na0642_durability_restart.rs was cited as the proof that "a 200 means
fsynced" and cannot be: SIGKILL destroys a process, not the page cache, so
synchronous=FULL and synchronous=OFF are indistinguishable to it -- that suite
passes 3/3 with the pragma OFF. Its header comment is corrected and the test is
kept unchanged for the process-crash durability it genuinely proves. The fsync
claim is discharged instead by tests/na0678_invite_durability.rs, which counts
real fsync syscalls, asserts the fsync precedes the 200 on the wire, and skips
WITH A STATED REASON when strace is absent rather than passing silently.
Verified locally: 29 suites / 129 tests green at RUST_TEST_THREADS=2; clippy
--all-targets -D warnings clean; fmt clean; infra-literal tree scan clean (76
files, 15254 lines examined); Cargo.toml and Cargo.lock diff EMPTY.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Goals: G1, G4
Messaging epic Slice 1 — the relay-side invite primitives, built with no client dependency. Executes QSL-DIR-2026-07-26-614 (D614), APPROVED 2026-07-26 (sha256
383dc366…016269, 600 lines) under qsl-protocol lane NA-0678. Records D-0016.What lands
Three additive routes —
POST /v1/invite/{create,redeem,revoke}— plus a slot-scoped admission check on the existing push path, aninvitestable atSCHEMA_VERSION2, and five resource controls.SHA-256(cap). There is no mint endpoint, so no relay-side path holds a capability in plaintext before a redeemer presents one — a relay operator cannot silently burn an invite it hosts.invite_idand every secret in the JSON body.invite_idis the mailbox route key, and D-0008/D-0009/D-0010 already retired URI-carried route tokens.ERR_INVITE_ALREADY_USED— and issues a one-shot handshake ticket. Without a live ticket a push to a slot is refused, so the slot accepts exactly one handshake from the party that actually redeemed, not from anyone who saw the code and lost the race.already-usedstays distinguishable fromnot-found— a deleted slot reports "never existed" when the truth is "someone got here first".invite_id,cap,revoke_tokenand the ticket are stored only as digests and compared with the existingct_eq_secret(D-0014). No new primitive;Cargo.tomlandCargo.lockdiff is EMPTY.The compatibility guarantee
/v1/push,/v1/pull,/v1/pull/ackare unchanged for every route the invite system did not create — one indexed lookup misses and the pre-existing path runs as before. This is what lets the shipped qsc client, the spine's pinned in-process e2e and the qsl-attachments interop contract keep working while the client slices are still unwritten.tests/qsl_attachments_integration_contract.rsandtests/na0642_backward_compat.rsboth pass unchanged and are the named proof.⚠ Two defects the census found, fixed here
The schema-version guard had been inert since it was written. The marker used
INSERT OR IGNORE, a no-op on an existing key, so a forward migration never advanced it — D-0011's fail-closed downgrade guard could not fire after any schema change, and this is the first schema change since D-0011. Now fixed, with a positive and a negative control (tests/na0678_schema_version.rs).tests/na0642_durability_restart.rscannot prove the claim it is cited for. It is the project's stated proof that "a 200 means fsynced". SIGKILL destroys a process, not the page cache, sosynchronous=FULLandsynchronous=OFFare indistinguishable to it — measured: that suite passes 3/3 with the pragma OFF. Its header comment is corrected and the test is kept unchanged for the process-crash durability it genuinely proves. The fsync claim is discharged instead bytests/na0678_invite_durability.rs, which counts realfsyncsyscalls, asserts the fsync precedes the 200 on the wire, and skips with a stated reason whenstraceis absent rather than passing silently.Both arms measured on the invite-create path: shipped config 5 creates → 5 fsyncs with fsync-before-200 ordering observed; a
synchronous=OFFcontrol built outside the tree 5 creates → 0 fsyncs. The instrument moves in both directions, so its green carries information.Also in scope per D614 §4b
DECISIONS.mdD-0015 gains a mark-don't-rewrite correction: its text still saidpublic-safetyandadvisorieswere advisory-only, which was true at landing and false since the NA-0677 closeout promoted all three contexts.TRACEABILITY.mdgains this lane's row only — the ENG-0066 backfill is deliberately not attempted here, so a wire-contract change is not buried under a documentation sweep.Verified locally
RUST_TEST_THREADS=2 cargo test -q→ exit 0, 29 suites / 129 tests (⚠ at full parallelismtests::logs_do_not_contain_raw_channelflakes — ENG-0065, pre-existing, unrelated) ·cargo clippy --all-targets -- -D warningsclean ·cargo fmt --checkclean ·infra_literal_scan --mode treeclean (76 files, 15,254 lines examined) and--mode diffclean (13 files, 2,258 lines examined).The spine's qsl-server dev-dependency pin is not bumped here — that is a named step between Slice 1 and Slice 2 (ENG-0041 / NA-0640 discipline).