diff --git a/deploy/compose/appsettings.Production.json b/deploy/compose/appsettings.Production.json index ae8e1f4..a5d3c80 100644 --- a/deploy/compose/appsettings.Production.json +++ b/deploy/compose/appsettings.Production.json @@ -1,5 +1,5 @@ { - "AllowedHosts": "localhost;127.0.0.1", + "AllowedHosts": "localhost;127.0.0.1;rendezvous", "Rendezvous": { "Deployment": { "PublicHttpBaseUrl": "https://localhost/", @@ -52,7 +52,7 @@ "MaxListingsPerPrincipal": 10, "MaxAnonymousListingsPerAddress": 0, "MaxActiveJoinAttempts": 100, - "FallbackPolicy": "Disabled" + "FallbackPolicy": "DedicatedEndpointAllowed" } ] } diff --git a/docs/deployment/linux.md b/docs/deployment/linux.md index a805c41..4f721ed 100644 --- a/docs/deployment/linux.md +++ b/docs/deployment/linux.md @@ -42,9 +42,11 @@ test "$RENDEZVOUS_UID" -ne 0 docker compose -f deploy/compose/compose.yaml up --build --detach ``` -`deploy/compose/appsettings.Production.json` is an isolated loopback smoke -profile, not an Internet template: it deliberately opts into private advertised -endpoints and has no TLS proxy. Its random key is ignored by Git and must be +`deploy/compose/appsettings.Production.json` is a local/private-bridge smoke +profile, not an Internet template: TCP is published only on host loopback, the +explicit `rendezvous` host name serves isolated clients on the Compose network, +and the profile deliberately opts into private advertised endpoints without a +TLS proxy. Its random key is ignored by Git and must be deleted after use. Its deliberately long key window only keeps this disposable local fixture usable; production keys require short, reviewed rotation windows. Production configuration must use its real public names and must leave diff --git a/docs/evidence/consumers/spacegame.json b/docs/evidence/consumers/spacegame.json index 4d36d24..d372093 100644 --- a/docs/evidence/consumers/spacegame.json +++ b/docs/evidence/consumers/spacegame.json @@ -3,60 +3,84 @@ "recordedAt": "2026-07-16", "issue": 21, "consumerIssue": "Kyuubi/SpaceGame#3", - "result": "checkpoint-pass-with-open-gates", - "rendezvousCommit": "7fb85059fb7003ca0984ebd0cc9541218d33ae93", - "consumerCommit": "86424ebde69a7eeae110106ab5f84c68f04af89b", + "result": "checkpoint-pass-with-external-gates", + "rendezvousBaseCommit": "ebb5eb617c0bbb170418afab396b68584b7f992e", + "consumerCommit": "f3f5bc29810c362656cd7143bec1ddc2cfaf9f22", + "consumerIssueComment": 11469, "packages": { "FinalFactory.Rendezvous.Client": { "version": "1.0.0", "source": "local-candidate", - "sha256": "606711e4961867ca8fe95fde973aa85f2b6e7483cafe4867ca8daa918aa78b6b" + "sourceCommit": "07004cd75fe172aa5dfdb3edda22fc280a4c4477", + "sha256": "fb156cf48b49f75c244dd25ea7cc4aa9fc6fab0a878393bb7efd5d9b131d0395" }, "FinalFactory.Rendezvous.Contracts": { "version": "1.0.0", "source": "local-candidate", - "sha256": "394acf0a1419690829ff38eb214b1833cf64edbc85bbb7182345bfe77b8ec1f0" + "sourceCommit": "07004cd75fe172aa5dfdb3edda22fc280a4c4477", + "sha256": "a82ba986d3905d599096d1d8ce8f32cd4feb104abfca37b0f65e0d2ef3df9a6f" }, "LiteNetLib": { "version": "2.1.4" } }, "localRun": { - "processes": ["Rendezvous", "SpaceGame host pilot", "SpaceGame client pilot"], + "processes": ["Rendezvous", "Godot SpaceGame host", "two sequential Godot SpaceGame clients"], "typedOutcome": "Connected", "gameAdmission": "Accepted", "directGameplay": true, - "directInputs": 1, - "directSnapshots": 1, - "rendezvousGameplayPayloads": 0, + "authenticatedSessions": 2, + "directInputs": 2, + "directSnapshots": 2, + "lifecyclePackets": 4, + "gameplayTransport": "caller-owned-litenetlib", + "rendezvousGameplayPayloadPath": "none", + "hostLeaseRenewed": true, + "reconnected": true, "deregistered": true }, + "linuxRun": { + "runtime": "Godot 4.7 .NET Linux x86_64", + "freshExport": true, + "sourceDirty": false, + "optimized": true, + "dedicatedHostNamespace": "docker", + "remoteClientNamespace": "docker", + "topology": "private-bridge", + "directGameplay": true, + "fallback": "PunchTimedOut to explicit Docker-gateway endpoint, then Accepted game admission and direct gameplay", + "artifactHashes": "SpaceGame issue #3 comment 11469" + }, "negativePaths": { "incompatibleProtocol": "proven", "staleHostPresence": "proven", "punchTimeout": "proven", - "invalidAdmission": "unit-and-protocol-boundary-proven", + "invalidAdmission": "integration-proven", "capacity": "regression-tested", - "fallbackConnection": "open", - "reconnect": "open" + "fallbackConnection": "godot-and-isolated-linux-proven", + "reconnect": "proven" }, "verification": { "debugBuild": "passed", "releaseBuild": "passed", - "debugTests": { "passed": 29, "failed": 0 }, - "releaseTests": { "passed": 29, "failed": 0 }, + "debugTests": { "passed": 31, "failed": 0 }, + "releaseTests": { "passed": 31, "failed": 0 }, + "exportRelease": "optimized-without-debug-symbols", "format": "passed", "shellcheck": "passed", - "twoProcessHarness": "passed", + "godotReconnectHarness": "passed", + "godotFallbackHarness": "passed", + "linuxContainerHarness": "passed-clean-source", + "failureMatrix": "passed", "adversarialReview": "passed-after-fixes" }, "openGates": [ "public-package-restore", - "actual-godot-process-integration", - "dedicated-fallback-connection", - "disconnect-reconnect", - "remote-linux-dedicated", - "representative-external-nat", - "spacegame-imgui-native-runtime-dependency" + "representative-external-nat" + ], + "relatedSpaceGameGates": [ + "production-enet-replacement", + "capacity-profiles-64-and-128", + "sigterm-drain-save" ] } diff --git a/docs/integration/spacegame-pilot.md b/docs/integration/spacegame-pilot.md index 0db6015..a52d8a2 100644 --- a/docs/integration/spacegame-pilot.md +++ b/docs/integration/spacegame-pilot.md @@ -2,20 +2,20 @@ Tracking: Rendezvous #21 and SpaceGame #3. -The first SpaceGame checkpoint proves that the v1 client boundary can establish +The current SpaceGame checkpoint proves that the v1 client boundary establishes authenticated direct LiteNetLib traffic without taking ownership of the game's -protocol, admission, player identity, entity identity, capacity, or gameplay -payloads. It does not yet pass the complete consumer-pilot gate. The public -package restore, actual Godot-process integration, connected fallback transport, -remote Linux dedicated run, representative external NAT run, and reconnect path -remain required before #21 can close. +protocol, admission, player identity, entity identity, capacity, lifecycle, or +gameplay payloads. Real Godot processes, reconnect, an explicit dedicated +fallback, and a fresh Linux export now pass. The public package restore and a +representative external NAT/CGNAT canary remain required before #21 can close. ## Pinned checkpoint | Input | Value | | --- | --- | -| Rendezvous source | `7fb85059fb7003ca0984ebd0cc9541218d33ae93` plus the #21 compatibility changes | -| SpaceGame source | `86424ebde69a7eeae110106ab5f84c68f04af89b` | +| Rendezvous compatibility source | `ebb5eb617c0bbb170418afab396b68584b7f992e` plus the current #21 configuration/evidence changes | +| Rendezvous package source | `07004cd75fe172aa5dfdb3edda22fc280a4c4477` | +| SpaceGame source | `f3f5bc29810c362656cd7143bec1ddc2cfaf9f22` | | Client package | `FinalFactory.Rendezvous.Client` `1.0.0` | | Contracts package | `FinalFactory.Rendezvous.Contracts` `1.0.0` | | LiteNetLib | `2.1.4` | @@ -45,19 +45,28 @@ out-of-sequence, and over-rate traffic. It assigns a canonical game entity ID only after admission. The player ID, entity ID, listing ID, join-attempt ID, and LiteNetLib peer ID remain distinct values. -The bounded two-process harness observed: +The bounded real-process harnesses observed: - host publication and lease maintenance; - browser compatibility filtering and join authorization; - typed traversal outcome `Connected`; - successful audience-bound game admission; -- one reliable ordered input and one sequenced state snapshot on the direct - gameplay socket; -- zero SpaceGame gameplay payloads handled by Rendezvous; and -- successful host deregistration. +- reliable ordered frame-definition and spawn lifecycle records, reliable + ordered input, and sequenced state snapshots on the caller-owned gameplay + socket; +- disconnect and a new authenticated session for the same durable player while + LiteNetLib peers and canonical entity IDs change; +- immediate host lease renewal and successful host deregistration; +- a fresh optimized Linux export running the host and client in distinct + hardened container namespaces; and +- a forced punch timeout that connects the isolated client to an explicitly + advertised, non-loopback Docker-gateway fallback and repeats game admission. -Both Debug and Release builds passed. Both Debug and Release test runs passed 29 -tests with zero failures. The focused formatter, shell checker, clean +Rendezvous exposes no gameplay relay API; all lifecycle, command, and snapshot +bytes are sent by SpaceGame through its caller-owned `NetManager`. Both Debug +and Release builds passed. Both Debug and Release test runs passed 31 tests with +zero failures. ExportRelease is optimized with debug symbols removed. The +focused formatter, shell checker, fresh-export provenance gate, clean candidate-package restore, and adversarial branch review also passed. ## Failure evidence @@ -67,11 +76,11 @@ candidate-package restore, and adversarial branch review also passed. | Incompatible protocol | protocol `999` returns no compatible listing and starts no traversal | Proven | | Stale/no host presence | typed `NoHostPresence/RendezvousService/HostPresence/Mediation` | Proven | | Traversal timeout | non-listening mediator produces typed `PunchTimedOut/LocalTraversal/NatTraversal/NatTraversal` | Proven | -| Rejected game admission | invalid signature and wrong audience deny gameplay; expiry and replay are regression-tested | Proven at protocol/unit boundary | +| Rejected game admission | invalid signature denies gameplay in the process matrix; wrong audience, expiry, and replay are regression-tested | Proven | | Capacity and duplicate player | game-owned roster rejects both and publishes current capacity | Regression-tested | -| Configured fallback | policy excludes authorization, compatibility, and capacity failures | Policy only; transport path open | -| Disconnect | direct session ends and host deregisters | Proven for checkpoint shutdown | -| Reconnect | a second authenticated session after disconnect | Open | +| Configured fallback | typed `PunchTimedOut`, explicit non-loopback endpoint, same game admission, direct gameplay | Proven locally and across Linux namespaces | +| Disconnect | host observes zero active players and final admitted count zero | Proven | +| Reconnect | same durable player enters a second authenticated session with new peer/entity IDs | Proven | ## Rendezvous-side compatibility fixes @@ -80,6 +89,8 @@ repository: - the local production-shaped smoke tenant accepts SpaceGame gameplay protocol `2` and the bounded `mode` metadata key; +- the Compose smoke tenant explicitly allows its private-network service name + and enables only the dedicated-endpoint fallback policy; - SDK guidance requires games using multiple LiteNetLib QoS channels to set `ChannelsCount` before `Start` and states that Rendezvous reserves no gameplay channel; and @@ -90,18 +101,12 @@ Documentation contract tests cover these generic requirements. ## Remaining acceptance gates -Do not mark #21 passed until all of these have direct evidence: +Do not mark #21 passed until both remaining external gates have direct evidence: -1. restore the exact immutable `1.0.0` packages from the public Gitea feed; -2. drive the host and client through actual Godot game processes, not only the - console pilot; -3. implement and exercise the configured dedicated fallback connection with the - same game admission and identity rules; -4. prove disconnect followed by a fresh authenticated reconnect; -5. run a remote client against a Linux dedicated host and capture normal - termination/drain behavior; -6. run representative external NAT canaries and record the network topology and - typed outcome; and -7. clear the current SpaceGame Linux runtime blocker: its pre-existing ImGui - GDExtension native library is absent. A headless process exit code of zero - while that load error is present is not runtime proof. +1. restore the exact immutable `1.0.0` packages from the public Gitea feed; and +2. run representative external NAT/CGNAT canaries and record the network + topology and typed outcome. + +SpaceGame #3 remains open independently for the production ENet replacement, +64/128-player profiles, and SIGTERM/drain/save evidence. The consumer pilot +does not claim those broader game-migration gates. diff --git a/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs b/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs index 99800fd..0b1858d 100644 --- a/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs +++ b/tests/FinalFactory.Rendezvous.Tests/Documentation/DocumentationContractTests.cs @@ -194,21 +194,37 @@ public sealed partial class DocumentationContractTests { string root = FindRepositoryRoot(); string guide = File.ReadAllText(Path.Combine(root, "docs", "integration", "spacegame-pilot.md")); + string deploymentGuide = File.ReadAllText(Path.Combine(root, "docs", "deployment", "linux.md")); using JsonDocument evidence = JsonDocument.Parse(File.ReadAllText( Path.Combine(root, "docs", "evidence", "consumers", "spacegame.json"))); JsonElement record = evidence.RootElement; - Assert.Equal("checkpoint-pass-with-open-gates", record.GetProperty("result").GetString()); - Assert.Equal(0, record.GetProperty("localRun").GetProperty("rendezvousGameplayPayloads").GetInt32()); + Assert.Equal("checkpoint-pass-with-external-gates", record.GetProperty("result").GetString()); + Assert.Equal("none", record.GetProperty("localRun").GetProperty("rendezvousGameplayPayloadPath").GetString()); + Assert.Equal("caller-owned-litenetlib", record.GetProperty("localRun").GetProperty("gameplayTransport").GetString()); Assert.True(record.GetProperty("localRun").GetProperty("directGameplay").GetBoolean()); - Assert.Equal(29, record.GetProperty("verification").GetProperty("debugTests").GetProperty("passed").GetInt32()); - Assert.Equal(29, record.GetProperty("verification").GetProperty("releaseTests").GetProperty("passed").GetInt32()); + Assert.True(record.GetProperty("localRun").GetProperty("reconnected").GetBoolean()); + Assert.True(record.GetProperty("linuxRun").GetProperty("freshExport").GetBoolean()); + Assert.False(record.GetProperty("linuxRun").GetProperty("sourceDirty").GetBoolean()); + Assert.Equal(31, record.GetProperty("verification").GetProperty("debugTests").GetProperty("passed").GetInt32()); + Assert.Equal(31, record.GetProperty("verification").GetProperty("releaseTests").GetProperty("passed").GetInt32()); Assert.Contains("public-package-restore", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString())); - Assert.Contains("actual-godot-process-integration", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString())); - Assert.Contains("dedicated-fallback-connection", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString())); + Assert.Contains("representative-external-nat", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString())); + Assert.DoesNotContain("actual-godot-process-integration", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString())); + Assert.DoesNotContain("dedicated-fallback-connection", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString())); Assert.Contains("Do not mark #21 passed", guide, StringComparison.Ordinal); Assert.Contains("Rendezvous reserves no gameplay", guide, StringComparison.Ordinal); - Assert.Contains("not runtime proof", guide, StringComparison.Ordinal); + Assert.Contains("private-network service name", guide, StringComparison.Ordinal); + Assert.Contains("local/private-bridge smoke", deploymentGuide, StringComparison.Ordinal); + Assert.Contains("explicit `rendezvous` host name", deploymentGuide, StringComparison.Ordinal); + + using JsonDocument composeSettings = JsonDocument.Parse(File.ReadAllText( + Path.Combine(root, "deploy", "compose", "appsettings.Production.json"))); + JsonElement compose = composeSettings.RootElement; + Assert.Contains("rendezvous", compose.GetProperty("AllowedHosts").GetString()!.Split(';')); + JsonElement game = compose.GetProperty("Rendezvous").GetProperty("Provisioning").GetProperty("Games")[0]; + Assert.Contains(2, game.GetProperty("ProtocolVersions").EnumerateArray().Select(static version => version.GetInt32())); + Assert.Equal("DedicatedEndpointAllowed", game.GetProperty("FallbackPolicy").GetString()); Assert.DoesNotMatch(ReusableCredential(), guide); }