Skip to content

Contract hardening: RosterCap, wire revision provenance, crossverify gates, kittest chaos knob (arch uplift)#32

Merged
BCook98 merged 6 commits into
mainfrom
arch-uplift/kit
Jun 10, 2026
Merged

Contract hardening: RosterCap, wire revision provenance, crossverify gates, kittest chaos knob (arch uplift)#32
BCook98 merged 6 commits into
mainfrom
arch-uplift/kit

Conversation

@BCook98

@BCook98 BCook98 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

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 in wire/ (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 Rust ROSTER_CAP source constant matches. The shellcade host adopts wire.RosterCap after the next kit release.

K2 — wire-revision provenance (defd4c2): presence-guarded trailing u16 wireRevision in 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 .dgld golden 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 the license = "MIT" already declared in rust/Cargo.toml — flag if you'd prefer otherwise); GUIDE player-bounds language unified on 1..1024 via wire.RosterCap, smoke-seats clarified.

K6 — CI pin guards (867ac89): SHELLCADE_KIT_VERSION 2.2.0 → 2.3.0 with verified checksum, plus a kit-pin job that mechanically fails when the pinned binary's embedded kit version drifts; pinned govulncheck job added.

Notes for review

  • The Rust-side changes (revision trailer encode, golden replay tests) were CI-verified only — no local Rust toolchain in the dev environment. Watch the Rust jobs on this PR.
  • A changeset is included describing the wire additions for the next release; cutting that release unblocks the shellcade host adopting RosterCap/Revision and a 2.7.x-matching shellcade-kit binary for the games repo pin.

🤖 Generated with Claude Code

BCook98 and others added 6 commits June 10, 2026 01:48
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>
@BCook98
BCook98 merged commit a0cefdf into main Jun 10, 2026
7 checks passed
@BCook98
BCook98 deleted the arch-uplift/kit branch June 10, 2026 05:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant