Contract hardening: RosterCap, wire revision provenance, crossverify gates, kittest chaos knob (arch uplift)#32
Merged
Merged
Conversation
The 1024 roster cap was a protocol invariant hand-mirrored as literals in three codebases (Go SDK codec, Rust SDK broadcast.rs, and the host's gameabi), tied together only by comments. Define wire.RosterCap = 1024 in the contract package with lockstep docs, adopt it in internal/game's rosterCap, document the bound normatively in ABI.md §3, and add wire.TestRustRosterCapMatchesWire, which parses rust/src/broadcast.rs's pub(crate) ROSTER_CAP and asserts it equals wire.RosterCap — runnable by plain `go test` (no Rust toolchain), complementing the crossverify golden vectors that guard the encodings. Also repair broadcast.rs's truncated doc comment while pointing it at the contract constant. The host's gameabi/delta.go adopts wire.RosterCap after the next kit release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reject-unknown evolution strategy (ABI.md §5 rule 5) rested on an operational rule — "the host always upgrades before artifacts" — with no mechanical anchor: no artifact, catalog row, or handshake carried any minor-level contract provenance, so a host could not detect, warn, or refuse when an artifact assumed a wire revision newer than it implements (mid-rolling-deploy that means every container dropped and a frozen screen with no fault). Add a presence-guarded trailing u16 wireRevision to the meta payload, following the thrice-used additive-trailer pattern (config-specs, large-room, lifecycle): wire.Revision is a single monotonic constant (4, with a documented ledger of past wire-visible minors) bumped in the same change as every future wire-visible minor; both guest SDKs stamp it automatically (Go: internal/game encodeMeta; Rust: wire.rs WIRE_REVISION, pinned to wire.Revision by the new source-parsing wire.TestRustWireRevisionMatchesWire, no Rust toolchain needed). Old hosts ignore the bytes; pre-field artifacts decode as revision 0 = unknown. ABI.md §4.2 documents the field, the ledger, and the normative host semantics (refuse at verify time / skip-with-warning at load time for revisions above the host's own); §5 rule 5 now points at the anchor and mandates the bump-with-the-minor rule. Rust meta goldens regenerated from the Go encoder; new round-trip/truncation tests cover the trailer in both the wire package and the SDK encode path. Host-side enforcement (FetchAndVerify refuse, loadOne/RefreshLive skip, game_versions provenance column) lands in shellcade after the next kit release, like the wire.RosterCap adoption. Rust changes are CI-verified (no local Rust toolchain). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…alar golden vectors The crossverify harness proved Go/Rust byte-identity only for the delta encoder, and even there against a SNAPSHOT: the committed .dgld vectors were never re-emitted in CI, so a Go encoder byte-output change would leave the Rust golden job green against historical bytes. Meanwhile the rest of the wire surface (meta with its positional presence-guarded trailers, ctx sentinel forms, results) was pinned only by hand-pasted hex in Rust unit tests, which catch Rust drifting from its own past but never a Go-side change. Close both links of the delta reference chain: - CI test job re-emits the .dgld vectors from the current Go reference via TestEmitGolden into a temp dir and diffs against the committed crossverify/tests/golden, failing with a regenerate instruction (emission is deterministic, so this is non-flaky). - internal/diffbench/parity_test.go asserts the emitter's encodeRunList / encodeKeyframe / encodeRunListOrKeyframe are byte-identical to the production wire.BuildFrameDelta / wire.BuildKeyframe (epoch 0) across every committed capture and synthetic — the gate can only police the production encoder if the two copies agree. Extend the generated-vector approach to the scalar encodings, direction-aware (wire/scalar_golden_test.go emits and freshness-gates the committed rust/tests/golden/scalars.txt; rust/src/wire.rs mod scalar_golden replays it in the existing rust CI job): - guest-encoded payloads: a default-valued meta (Meta::DEFAULT + slug) and a fully-populated one (tags, labels, leaderboard, config specs incl. JSON + schema, ctx features, heartbeat, lifecycle, stamped wireRevision), plus a mixed-status multi-player result — Rust asserts encode_meta / encode_outcome byte-identity (encode_outcome previously had no Go golden at all). - host-encoded ctx payloads: legacy, full-sentinel, and unchanged-sentinel forms with trailing event-extra bytes — Rust runs decode_ctx over the Go bytes asserting every field and the reader position (previously those tests hand-built bytes with Rust's own Buf, verifying nothing cross-language). - decoder presence guards: truncated older-form meta vectors (pre-config, pre-large-room, pre-lifecycle, pre-revision) pin DecodeMeta's absent-trailer tolerance; the Rust SDK has no meta decoder, so these are Go-side only. TestScalarGoldenFresh regenerates the vector content on every plain go test run and fails if the committed file is stale, converting each future trailing-section addition from "remember to regenerate hex by hand" into a failing test on whichever side moved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The kittest double's KV always succeeded, so no game author could simulate the real host behavior where a transient store failure makes Get report a saved key as missing and silently drops Set/Delete — the production path that turns the natural 'Get -> missing -> initialize -> Set' wallet pattern into a silent state reset. Add an opt-in Room.KVUnavailable flag with exactly those semantics (Get returns nil,false,nil; Set/Delete return nil without persisting; the backing maps survive the blip), pin them with a degradation test plus an example demonstrating the read-absent-reinit hazard, and document the hazard and conservative-missing-read guidance in GUIDE.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lity Ship an MIT LICENSE at the repo root (rust/Cargo.toml already declared license = "MIT"; the holder/choice is flagged for maintainer confirmation). GUIDE.md now states the platform player bound explicitly as 1..1024 (wire.RosterCap) in the Multiplayer and Large rooms sections, and documents that smoke scripts drive at most 8 seats — the runner clamps MinPlayers to the seat count, so large-room games still pass smoke, with large-room behavior covered by check's budget gates. The Rust README notes that a directory-accepting shellcade-kit play lands in the next shellcade-kit release, collapsing the Rust iterate loop to 'shellcade-kit play .'. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bump SHELLCADE_KIT_VERSION 2.2.0 -> 2.3.0 (the newest published binary release) with the matching linux/amd64 sha256, and add a kit-pin job that keeps the pin honest mechanically: it parses the kit line of 'shellcade-kit version' (the embedded kit module version, not the binary's own version stream) and fails on a tag/embed lockstep violation, and it compares the pin against the newest release carrying shellcade-kit binaries, failing with a bump instruction when the pin falls behind. Also add a govulncheck workflow (push/PR plus a nightly schedule, since new vuln-DB entries arrive without commits). Verified clean locally. The changesets for the wire additions this release carries (wire.RosterCap, wire.Revision, kittest KVUnavailable) already landed with K1/K2/K4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Lands the kit-side contract work from the 2026-06-09 architecture review (PR'd from
arch-uplift/kit; companions: shellcade/games#31 and a shellcade integration PR to follow). All additions are wire-additive — no ABI major, existing artifacts unaffected.What's here
K1 —
wire.RosterCap(f840af2): the 1024 roster ceiling becomes an exported contract constant inwire/(previously hand-mirrored as a magic number across kit, the Rust SDK, and the shellcade host). Kit's internal codec now derives from it; a Go-side cross-check asserts the RustROSTER_CAPsource constant matches. The shellcade host adoptswire.RosterCapafter the next kit release.K2 — wire-revision provenance (
defd4c2): presence-guarded trailingu16 wireRevisionin the meta payload (wire.Revision = 4, with a documented monotonic ledger and bump-on-every-wire-visible-minor rule), encoded by both SDKs via the established additive-trailer pattern, ABI.md updated with host warn/refuse semantics. This gives the deploy-order rule ("host upgrades before artifacts") its first mechanical anchor: hosts and audits can finally answer which wire revision was this artifact built against.K3 — crossverify chain closed (
49681c9): CI now re-emits the.dgldgolden vectors and diffs against the committed set (stale goldens fail the build with a regenerate instruction), and the generated-vector approach is extended beyond the delta encoder to the meta/ctx/result scalar encodings (Go emits committed vectors incl. default-valued and fully-populated fixtures; Rust replays them in CI).K4 —
kittest.KVUnavailable(98cde80): opt-in chaos knob mirroring the host's exact KV degradation semantics (Get → absent without error; Set/Delete silently dropped; backing map survives). Lets game authors test the wallet read-absent-reinit hazard that previously only existed in production.K5 — LICENSE + doc truth (
6c42e7a): MIT LICENSE (matching thelicense = "MIT"already declared inrust/Cargo.toml— flag if you'd prefer otherwise); GUIDE player-bounds language unified on 1..1024 viawire.RosterCap, smoke-seats clarified.K6 — CI pin guards (
867ac89):SHELLCADE_KIT_VERSION2.2.0 → 2.3.0 with verified checksum, plus akit-pinjob that mechanically fails when the pinned binary's embedded kit version drifts; pinnedgovulncheckjob added.Notes for review
RosterCap/Revisionand a 2.7.x-matchingshellcade-kitbinary for the games repo pin.🤖 Generated with Claude Code