Compare commits

..

4 Commits

Author SHA1 Message Date
KyuubiYoru 6bad659c12 docs(integration): record Unscouted pilot evidence (#22)
quality-gate / quality (push) Failing after 1m35s
quality-gate / container (push) Has been skipped
2026-07-16 21:53:48 +02:00
KyuubiYoru f368fec6eb feat(deploy): provision Unscouted smoke tenant (#22) 2026-07-16 21:49:17 +02:00
KyuubiYoru 9e863ebf64 docs(integration): verify Godot and Linux SpaceGame pilot (#21)
quality-gate / quality (push) Failing after 1m40s
quality-gate / container (push) Has been skipped
2026-07-16 20:32:53 +02:00
KyuubiYoru ebb5eb617c docs(integration): record SpaceGame pilot checkpoint (#21)
quality-gate / quality (push) Failing after 1m26s
quality-gate / container (push) Has been skipped
2026-07-16 19:18:03 +02:00
12 changed files with 573 additions and 18 deletions
+4
View File
@@ -119,6 +119,10 @@ versioning, and secure rollout seams are in the
The always-on three-party scenarios, optional Linux namespace topology, and The always-on three-party scenarios, optional Linux namespace topology, and
simulation limits are documented in the simulation limits are documented in the
[deterministic topology harness](docs/integration/topology-harness.md). [deterministic topology harness](docs/integration/topology-harness.md).
The current consumer evidence and remaining external gates are tracked in the
[SpaceGame consumer pilot](docs/integration/spacegame-pilot.md) and independent
[Unscouted consumer pilot](docs/integration/unscouted-pilot.md).
## Development ## Development
+38 -5
View File
@@ -1,5 +1,5 @@
{ {
"AllowedHosts": "localhost;127.0.0.1", "AllowedHosts": "localhost;127.0.0.1;rendezvous",
"Rendezvous": { "Rendezvous": {
"Deployment": { "Deployment": {
"PublicHttpBaseUrl": "https://localhost/", "PublicHttpBaseUrl": "https://localhost/",
@@ -32,6 +32,16 @@
"NotBefore": "2026-01-01T00:00:00Z", "NotBefore": "2026-01-01T00:00:00Z",
"SignUntil": "2100-01-01T00:00:00Z", "SignUntil": "2100-01-01T00:00:00Z",
"VerifyUntil": "2100-01-02T00:00:00Z" "VerifyUntil": "2100-01-02T00:00:00Z"
},
{
"KeyId": "local-smoke-unscouted-1",
"SecretReference": "file:/run/secrets/rendezvous-signing-key",
"CredentialKinds": ["DedicatedPublisher"],
"GameId": "unscouted",
"EnvironmentId": "smoke",
"NotBefore": "2026-01-01T00:00:00Z",
"SignUntil": "2100-01-01T00:00:00Z",
"VerifyUntil": "2100-01-02T00:00:00Z"
} }
], ],
"Games": [ "Games": [
@@ -39,18 +49,41 @@
"GameId": "space-game", "GameId": "space-game",
"EnvironmentId": "smoke", "EnvironmentId": "smoke",
"Enabled": true, "Enabled": true,
"ProtocolVersions": [1, 2],
"Regions": ["local"],
"VisibilityModes": ["Public"],
"PublisherTrustModes": ["ManagedDedicated"],
"MetadataValueMaxBytes": {
"mode": 32
},
"RequiredMetadataKeys": [],
"MetadataMaxBytes": 512,
"MetadataMaxKeys": 1,
"MaxListingsPerPrincipal": 10,
"MaxAnonymousListingsPerAddress": 0,
"MaxActiveJoinAttempts": 100,
"FallbackPolicy": "DedicatedEndpointAllowed"
},
{
"GameId": "unscouted",
"EnvironmentId": "smoke",
"Enabled": true,
"ProtocolVersions": [1], "ProtocolVersions": [1],
"Regions": ["local"], "Regions": ["local"],
"VisibilityModes": ["Public"], "VisibilityModes": ["Public"],
"PublisherTrustModes": ["ManagedDedicated"], "PublisherTrustModes": ["ManagedDedicated"],
"MetadataValueMaxBytes": {}, "MetadataValueMaxBytes": {
"RequiredMetadataKeys": [], "mode": 32,
"world": 64,
"mods": 64
},
"RequiredMetadataKeys": ["mode", "world", "mods"],
"MetadataMaxBytes": 512, "MetadataMaxBytes": 512,
"MetadataMaxKeys": 0, "MetadataMaxKeys": 3,
"MaxListingsPerPrincipal": 10, "MaxListingsPerPrincipal": 10,
"MaxAnonymousListingsPerAddress": 0, "MaxAnonymousListingsPerAddress": 0,
"MaxActiveJoinAttempts": 100, "MaxActiveJoinAttempts": 100,
"FallbackPolicy": "Disabled" "FallbackPolicy": "DedicatedEndpointAllowed"
} }
] ]
} }
+5 -3
View File
@@ -42,9 +42,11 @@ test "$RENDEZVOUS_UID" -ne 0
docker compose -f deploy/compose/compose.yaml up --build --detach docker compose -f deploy/compose/compose.yaml up --build --detach
``` ```
`deploy/compose/appsettings.Production.json` is an isolated loopback smoke `deploy/compose/appsettings.Production.json` is a local/private-bridge smoke
profile, not an Internet template: it deliberately opts into private advertised profile, not an Internet template: TCP is published only on host loopback, the
endpoints and has no TLS proxy. Its random key is ignored by Git and must be 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 deleted after use. Its deliberately long key window only keeps this disposable
local fixture usable; production keys require short, reviewed rotation windows. local fixture usable; production keys require short, reviewed rotation windows.
Production configuration must use its real public names and must leave Production configuration must use its real public names and must leave
+86
View File
@@ -0,0 +1,86 @@
{
"schemaVersion": "1.0",
"recordedAt": "2026-07-16",
"issue": 21,
"consumerIssue": "Kyuubi/SpaceGame#3",
"result": "checkpoint-pass-with-external-gates",
"rendezvousBaseCommit": "ebb5eb617c0bbb170418afab396b68584b7f992e",
"consumerCommit": "f3f5bc29810c362656cd7143bec1ddc2cfaf9f22",
"consumerIssueComment": 11469,
"packages": {
"FinalFactory.Rendezvous.Client": {
"version": "1.0.0",
"source": "local-candidate",
"sourceCommit": "07004cd75fe172aa5dfdb3edda22fc280a4c4477",
"sha256": "fb156cf48b49f75c244dd25ea7cc4aa9fc6fab0a878393bb7efd5d9b131d0395"
},
"FinalFactory.Rendezvous.Contracts": {
"version": "1.0.0",
"source": "local-candidate",
"sourceCommit": "07004cd75fe172aa5dfdb3edda22fc280a4c4477",
"sha256": "a82ba986d3905d599096d1d8ce8f32cd4feb104abfca37b0f65e0d2ef3df9a6f"
},
"LiteNetLib": {
"version": "2.1.4"
}
},
"localRun": {
"processes": ["Rendezvous", "Godot SpaceGame host", "two sequential Godot SpaceGame clients"],
"typedOutcome": "Connected",
"gameAdmission": "Accepted",
"directGameplay": true,
"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": "integration-proven",
"capacity": "regression-tested",
"fallbackConnection": "godot-and-isolated-linux-proven",
"reconnect": "proven"
},
"verification": {
"debugBuild": "passed",
"releaseBuild": "passed",
"debugTests": { "passed": 31, "failed": 0 },
"releaseTests": { "passed": 31, "failed": 0 },
"exportRelease": "optimized-without-debug-symbols",
"format": "passed",
"shellcheck": "passed",
"godotReconnectHarness": "passed",
"godotFallbackHarness": "passed",
"linuxContainerHarness": "passed-clean-source",
"failureMatrix": "passed",
"adversarialReview": "passed-after-fixes"
},
"openGates": [
"public-package-restore",
"representative-external-nat"
],
"relatedSpaceGameGates": [
"production-enet-replacement",
"capacity-profiles-64-and-128",
"sigterm-drain-save"
]
}
+82
View File
@@ -0,0 +1,82 @@
{
"schemaVersion": "1.0",
"recordedAt": "2026-07-16",
"issue": 22,
"consumerIssue": "HeiKyu/Unscouted#459",
"result": "checkpoint-pass-with-external-gates",
"rendezvousConfigurationCommit": "f368fec6eb4344a6042974f58f888cf0f1ac8e8e",
"consumerImplementationCommit": "1e5886aa7f1e44689b4c75e32693eb7b19fd72d7",
"consumerEvidenceCommit": "f0574a7de82aadff6495ca5657dfc19cf7c2f67c",
"consumerIssueComment": 11499,
"packages": {
"FinalFactory.Rendezvous.Client": {
"version": "1.0.0",
"source": "local-candidate",
"sourceCommit": "07004cd75fe172aa5dfdb3edda22fc280a4c4477",
"sha256": "fb156cf48b49f75c244dd25ea7cc4aa9fc6fab0a878393bb7efd5d9b131d0395"
},
"FinalFactory.Rendezvous.Contracts": {
"version": "1.0.0",
"source": "local-candidate",
"sourceCommit": "07004cd75fe172aa5dfdb3edda22fc280a4c4477",
"sha256": "a82ba986d3905d599096d1d8ce8f32cd4feb104abfca37b0f65e0d2ef3df9a6f"
},
"LiteNetLib": {
"version": "2.1.4"
}
},
"configuration": {
"gameId": "unscouted",
"environmentId": "smoke",
"regionId": "local",
"protocolVersion": 1,
"publisherTrust": "ManagedDedicated",
"fallbackPolicy": "DedicatedEndpointAllowed",
"metadataKeys": ["mode", "world", "mods"],
"metadataMaxKeys": 3,
"metadataMaxBytes": 512
},
"godotRun": {
"runtime": "Godot 4.7 .NET Linux x86_64",
"processes": [
"Rendezvous hardened Compose service",
"Godot Unscouted host",
"Godot incompatible-protocol client",
"Godot direct client",
"Godot fallback client"
],
"gameplayTransport": "unscouted-litenetlib",
"rendezvousGameplayPayloadPath": "none",
"directGameplay": true,
"fallbackGameplay": true,
"authenticatedSessions": 2,
"gameplayExchanges": 2,
"hostLeaseRenewed": true,
"deregistered": true,
"playerIdentityOwner": "unscouted",
"canonicalGameStateOwner": "unscouted"
},
"negativePaths": {
"incompatibleProtocol": "proven-no-compatible-listing",
"wrongGame": "proven-exact-NotFound",
"wrongEnvironment": "proven-exact-NotFound",
"punchTimeout": "proven-typed-failure-then-game-owned-fallback",
"unexpectedMetadata": "consumer-regression-tested"
},
"verification": {
"rendezvousDebugTests": { "passed": 299, "failed": 0 },
"rendezvousReleaseTests": { "passed": 299, "failed": 0 },
"consumerDebugTests": { "passed": 3310, "skipped": 15, "failed": 0 },
"consumerReleaseTests": { "passed": 3310, "skipped": 15, "failed": 0 },
"consumerGdUnitTests": { "passed": 360, "skipped": 0, "failed": 0 },
"consumerExport": "not-applicable-no-export-presets",
"format": "passed",
"shellcheck": "passed",
"godotPilot": "passed",
"adversarialReview": "passed-after-fixes"
},
"openGates": [
"public-package-restore",
"representative-external-nat"
]
}
+5
View File
@@ -70,6 +70,7 @@ thread and do not also call `NetManager.PollEvents()` during that period.
```csharp ```csharp
RendezvousNetListener networkEvents = new(); RendezvousNetListener networkEvents = new();
NetManager gameplayNetwork = networkEvents.CreateManager(); NetManager gameplayNetwork = networkEvents.CreateManager();
gameplayNetwork.ChannelsCount = 3; // set the game's required count before Start
if (!gameplayNetwork.Start(gameplayPort)) if (!gameplayNetwork.Start(gameplayPort))
{ {
throw new InvalidOperationException("Gameplay UDP socket could not start."); throw new InvalidOperationException("Gameplay UDP socket could not start.");
@@ -85,6 +86,10 @@ using RendezvousHostCoordinator host = new(
host.Poll(); // call each game frame while this coordinator owns polling host.Poll(); // call each game frame while this coordinator owns polling
``` ```
LiteNetLib defaults to one QoS channel. Set `ChannelsCount` before `Start` when
the game protocol uses additional channels; both peers must configure the same
count. Rendezvous does not choose, remap, or reserve a gameplay channel.
Register normal game callbacks on `networkEvents.GameplayEvents`. Rendezvous Register normal game callbacks on `networkEvents.GameplayEvents`. Rendezvous
reserves only its authenticated direct requests and forwards other callbacks. reserves only its authenticated direct requests and forwards other callbacks.
The same socket sends host presence, punches through the mediator, establishes The same socket sends host presence, punches through the mediator, establishes
+112
View File
@@ -0,0 +1,112 @@
# SpaceGame consumer pilot
Tracking: Rendezvous #21 and SpaceGame #3.
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, 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 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` |
| HTTP, UDP, ticket contracts | `1` |
| SpaceGame gameplay protocol | `2` |
At the checkpoint date, the Final Factory Gitea NuGet service was reachable but
both `FinalFactory.Rendezvous.*` `1.0.0` registrations returned HTTP 404. The run
therefore restored locally built candidate packages with the hashes recorded in
[`spacegame.json`](../evidence/consumers/spacegame.json). This proves candidate
compatibility, not immutable registry publication. The release package restore
must be repeated from the public feed.
## Proven local path
The SpaceGame host and client each create one caller-owned `NetManager`, set its
three gameplay QoS channels before `Start`, and give the same manager and
`RendezvousNetListener` to the coordinator. Rendezvous authenticates discovery,
join authorization, host presence, mediation, and connection outcome reporting.
After traversal, SpaceGame performs a separate audience-bound admission exchange
on its own reliable command channel. A trusted game-auth boundary mints the
opaque assertion; the player process never receives the signing key.
The authoritative host rejects expired, replayed, incorrectly signed,
wrong-listing, duplicate-player, over-capacity, identity-mismatched,
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 real-process harnesses observed:
- host publication and lease maintenance;
- browser compatibility filtering and join authorization;
- typed traversal outcome `Connected`;
- successful audience-bound game admission;
- 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.
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
| Path | Evidence | Status |
| --- | --- | --- |
| 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 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 | 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
The pilot found generic integration gaps and keeps their fixes in this
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
- the local credential helper rejects any signing-key file with group or other
permissions, in addition to its ownership, symlink, and hard-link checks.
Documentation contract tests cover these generic requirements.
## Remaining acceptance gates
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; 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.
+99
View File
@@ -0,0 +1,99 @@
# Unscouted consumer pilot
Tracking: Rendezvous #22 and Unscouted #459.
The current checkpoint independently proves that the v1 contracts are not
shaped only around SpaceGame. A real Godot Unscouted host and clients consume
the same Client and Contracts package surface, use one caller-owned LiteNetLib
socket for NAT callbacks and gameplay, perform Unscouted's own keypair
authentication and host admission, exchange gameplay, and exercise a
game-owned fallback. The public package restore and representative external
NAT/CGNAT canary remain required before #22 can close.
## Pinned checkpoint
| Input | Value |
| --- | --- |
| Rendezvous configuration source | `f368fec6eb4344a6042974f58f888cf0f1ac8e8e` |
| Rendezvous package source | `07004cd75fe172aa5dfdb3edda22fc280a4c4477` |
| Unscouted implementation | `1e5886aa7f1e44689b4c75e32693eb7b19fd72d7` |
| Unscouted evidence | `f0574a7de82aadff6495ca5657dfc19cf7c2f67c` |
| Client package | `FinalFactory.Rendezvous.Client` `1.0.0` |
| Contracts package | `FinalFactory.Rendezvous.Contracts` `1.0.0` |
| LiteNetLib | `2.1.4` |
| Godot | `4.7.stable.mono.arch_linux.5b4e0cb0f` |
| Game / environment / region | `unscouted` / `smoke` / `local` |
| Rendezvous and gameplay protocol | `1` |
The exact package hashes are recorded in
[`unscouted.json`](../evidence/consumers/unscouted.json). A clean restore into an
empty package directory using only the consumer's checked-in `NuGet.config`
returns `NU1101` for both packages. The verified local run used those exact
candidate package files from the existing cache. This proves compatibility,
not immutable registry publication.
## Game-neutral service boundary
Rendezvous #22 adds provisioning data, not an Unscouted branch in the server or
SDK. The local production-shaped tenant permits protocol `1`, region `local`,
public managed-dedicated listings, and the three bounded presentation keys
`mode`, `world`, and `mods`. The short-lived credential helper accepts only the
explicitly provisioned `space-game` and `unscouted` scopes and selects a
distinct game-scoped signing-key ID and subject.
The consumer rejects any metadata key outside its three-key presentation
schema and neutralizes control/BBCode characters before display. Rendezvous
never receives Unscouted player keys or resolved identities, colony authority,
simulation or persistence state, fog/interest state, or gameplay packets.
## Proven real Godot path
The normal `NetLaunch` argument path recognizes `--rendezvous-pilot` and opens a
dedicated scene. That scene uses Unscouted's real `LiteNetLibTransport`,
`GameServer`, `GameClient`, `ServerAuthenticator`, and `ClientAuthenticator`.
It is not a copied SDK adapter.
One bounded run against the hardened Compose service started a host plus:
- a protocol-`999` client that found no compatible listing;
- a direct client that received an authorized introduction, completed
same-socket traversal, passed Unscouted keypair admission, and exchanged an
Unscouted gameplay ping/pong; and
- a client pointed at a non-listening mediator that received a typed traversal
failure, applied the fallback decision in Unscouted code, repeated admission,
and exchanged the same gameplay ping/pong through the ordinary game
transport.
The direct client also proved that both a `space-game` join request and a
`production` environment join request return exact `NotFound` results for the
Unscouted listing. The host renewed its lease, admitted two independently
authenticated sessions, completed two gameplay exchanges, and deregistered the
listing on shutdown.
## Verification
- Rendezvous Debug and Release: 299 tests passed in each configuration, zero
failures.
- Unscouted Debug and Release: non-incremental builds passed; 3,310 tests passed
with 15 intentional skips in each configuration.
- Unscouted gdUnit/Godot: 360 tests passed, zero skipped or failed. The harness
fix in Unscouted #461 keeps compilation headless and leaves the open editor's
build tree unchanged.
- The final Godot pilot, ShellCheck, JSON/whitespace checks, formatting gate,
and adversarial branch review passed.
- Export is not applicable because the Unscouted checkout has no
`export_presets.cfg`; both C# configurations and the actual Godot entry point
were exercised.
## Remaining acceptance gates
Do not mark #22 passed until both external gates have direct evidence:
1. publish or expose the exact immutable `1.0.0` packages on the configured
Gitea feed and repeat the empty-cache consumer restore; and
2. run the same Godot host/client path across representative residential,
CGNAT, and IPv6/multi-host networks, recording the topology and typed
direct/fallback outcome.
The loopback run proves the real process, socket, authentication, and gameplay
shape. It does not claim production Internet traversal coverage.
+26 -7
View File
@@ -3,9 +3,25 @@ set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOCAL_KEY="${RENDEZVOUS_SMOKE_LOCAL_KEY:-$ROOT/deploy/compose/secrets/signing-key}" LOCAL_KEY="${RENDEZVOUS_SMOKE_LOCAL_KEY:-$ROOT/deploy/compose/secrets/signing-key}"
GAME_ID="${RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID:-space-game}"
case "$GAME_ID" in
space-game)
KEY_ID="local-smoke-1"
SUBJECT="local-smoke-host"
;;
unscouted)
KEY_ID="local-smoke-unscouted-1"
SUBJECT="local-smoke-unscouted-host"
;;
*)
printf 'RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID must be space-game or unscouted.\n' >&2
exit 2
;;
esac
if (( $# != 0 )); then if (( $# != 0 )); then
printf 'This helper accepts no arguments and mints only the fixed local Compose smoke scope.\n' >&2 printf 'This helper accepts no arguments; select only a provisioned local game through RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID.\n' >&2
exit 2 exit 2
fi fi
@@ -17,7 +33,7 @@ command -v python3 >/dev/null || {
# This is deliberately a local-fixture tool, not a general credential issuer. # This is deliberately a local-fixture tool, not a general credential issuer.
# Python reads the raw key from the protected file; key material never appears in # Python reads the raw key from the protected file; key material never appears in
# a child process argument, environment value, temporary file, or command output. # a child process argument, environment value, temporary file, or command output.
python3 - "$LOCAL_KEY" <<'PY' python3 - "$LOCAL_KEY" "$GAME_ID" "$KEY_ID" "$SUBJECT" <<'PY'
import base64 import base64
import hashlib import hashlib
import hmac import hmac
@@ -29,6 +45,9 @@ import sys
import time import time
key_path = sys.argv[1] key_path = sys.argv[1]
game_id = sys.argv[2]
key_id = sys.argv[3]
subject = sys.argv[4]
try: try:
metadata = os.lstat(key_path) metadata = os.lstat(key_path)
except FileNotFoundError: except FileNotFoundError:
@@ -42,8 +61,8 @@ if stat.S_ISLNK(parent.st_mode) or not stat.S_ISDIR(parent.st_mode):
raise SystemExit(f"Local Compose secret directory must be a non-symlink directory: {parent_path}") raise SystemExit(f"Local Compose secret directory must be a non-symlink directory: {parent_path}")
if parent.st_uid != os.geteuid() or parent.st_mode & 0o077: if parent.st_uid != os.geteuid() or parent.st_mode & 0o077:
raise SystemExit(f"Local Compose secret directory must be owned by this user with mode 0700: {parent_path}") raise SystemExit(f"Local Compose secret directory must be owned by this user with mode 0700: {parent_path}")
if metadata.st_uid != os.geteuid() or metadata.st_mode & 0o022 or metadata.st_nlink != 1: if metadata.st_uid != os.geteuid() or metadata.st_mode & 0o077 or metadata.st_nlink != 1:
raise SystemExit(f"Local Compose smoke key must be owned by this user, single-linked, and not group/world writable: {key_path}") raise SystemExit(f"Local Compose smoke key must be owned by this user, single-linked, and private to its owner: {key_path}")
with open(key_path, "rb") as key_file: with open(key_path, "rb") as key_file:
key = key_file.read(33) key = key_file.read(33)
@@ -55,9 +74,9 @@ payload = {
"version": 1, "version": 1,
"issuer": "final-factory-rendezvous-smoke", "issuer": "final-factory-rendezvous-smoke",
"audience": "rendezvous-service", "audience": "rendezvous-service",
"subject": "local-smoke-host", "subject": subject,
"kind": "dedicatedPublisher", "kind": "dedicatedPublisher",
"gameId": "space-game", "gameId": game_id,
"environmentId": "smoke", "environmentId": "smoke",
"regions": ["local"], "regions": ["local"],
"permissions": [], "permissions": [],
@@ -71,7 +90,7 @@ def base64url(value: bytes) -> str:
return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
encoded = base64url(json.dumps(payload, separators=(",", ":")).encode("utf-8")) encoded = base64url(json.dumps(payload, separators=(",", ":")).encode("utf-8"))
signed = f"rv1.local-smoke-1.{encoded}" signed = f"rv1.{key_id}.{encoded}"
signature = base64url(hmac.new(key, signed.encode("ascii"), hashlib.sha256).digest()) signature = base64url(hmac.new(key, signed.encode("ascii"), hashlib.sha256).digest())
print(f"{signed}.{signature}") print(f"{signed}.{signature}")
PY PY
@@ -67,12 +67,17 @@ factory does not open a socket, and synchronized events must remain enabled:
```csharp ```csharp
RendezvousNetListener networkEvents = new(); RendezvousNetListener networkEvents = new();
NetManager gameplayNetManager = networkEvents.CreateManager(); NetManager gameplayNetManager = networkEvents.CreateManager();
gameplayNetManager.ChannelsCount = 3; // example: configure the game protocol first
if (!gameplayNetManager.Start(0)) if (!gameplayNetManager.Start(0))
{ {
throw new InvalidOperationException("The gameplay UDP socket could not start."); throw new InvalidOperationException("The gameplay UDP socket could not start.");
} }
``` ```
LiteNetLib defaults to one QoS channel. Set `ChannelsCount` before `Start` when
the game protocol uses more than one; both game processes must agree. Rendezvous
does not reserve or reinterpret any gameplay channel.
The host polls join invitations asynchronously; that method only queues a The host polls join invitations asynchronously; that method only queues a
snapshot and never calls the manager. `Poll()` is the sole SDK path that invokes snapshot and never calls the manager. `Poll()` is the sole SDK path that invokes
LiteNetLib and dispatches its synchronized callbacks. Call it once per game LiteNetLib and dispatches its synchronized callbacks. Call it once per game
@@ -238,6 +238,10 @@ public sealed class ProductionProcessTests
"--Rendezvous:Provisioning:SigningKeys:0:NotBefore", now.AddHours(-1).ToString("O"), "--Rendezvous:Provisioning:SigningKeys:0:NotBefore", now.AddHours(-1).ToString("O"),
"--Rendezvous:Provisioning:SigningKeys:0:SignUntil", now.AddHours(1).ToString("O"), "--Rendezvous:Provisioning:SigningKeys:0:SignUntil", now.AddHours(1).ToString("O"),
"--Rendezvous:Provisioning:SigningKeys:0:VerifyUntil", now.AddHours(2).ToString("O"), "--Rendezvous:Provisioning:SigningKeys:0:VerifyUntil", now.AddHours(2).ToString("O"),
"--Rendezvous:Provisioning:SigningKeys:1:SecretReference", $"file:{secretPath}",
"--Rendezvous:Provisioning:SigningKeys:1:NotBefore", now.AddHours(-1).ToString("O"),
"--Rendezvous:Provisioning:SigningKeys:1:SignUntil", now.AddHours(1).ToString("O"),
"--Rendezvous:Provisioning:SigningKeys:1:VerifyUntil", now.AddHours(2).ToString("O"),
"--Rendezvous:Udp:Port", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture), "--Rendezvous:Udp:Port", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
"--Rendezvous:Deployment:PublicUdpPort", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture), "--Rendezvous:Deployment:PublicUdpPort", udpPort.ToString(System.Globalization.CultureInfo.InvariantCulture),
}, },
@@ -40,26 +40,57 @@ public sealed partial class DocumentationContractTests
} }
[Fact] [Fact]
public void LocalCredentialHelperIsFixedScopeAndSmokeDelegatesToIt() public void LocalCredentialHelperWhitelistsProvisionedGameScopesAndSmokeDelegatesToIt()
{ {
string root = FindRepositoryRoot(); string root = FindRepositoryRoot();
string helper = File.ReadAllText(Path.Combine(root, "scripts", "mint-local-publisher-credential.sh")); string helper = File.ReadAllText(Path.Combine(root, "scripts", "mint-local-publisher-credential.sh"));
string smoke = File.ReadAllText(Path.Combine(root, "scripts", "smoke-deployment.sh")); string smoke = File.ReadAllText(Path.Combine(root, "scripts", "smoke-deployment.sh"));
Assert.Contains("if (( $# != 0 ));", helper, StringComparison.Ordinal); Assert.Contains("if (( $# != 0 ));", helper, StringComparison.Ordinal);
Assert.Contains("\"gameId\": \"space-game\"", helper, StringComparison.Ordinal); Assert.Contains("space-game)", helper, StringComparison.Ordinal);
Assert.Contains("unscouted)", helper, StringComparison.Ordinal);
Assert.Contains("KEY_ID=\"local-smoke-1\"", helper, StringComparison.Ordinal);
Assert.Contains("KEY_ID=\"local-smoke-unscouted-1\"", helper, StringComparison.Ordinal);
Assert.Contains("RENDEZVOUS_LOCAL_CREDENTIAL_GAME_ID must be space-game or unscouted", helper, StringComparison.Ordinal);
Assert.Contains("\"gameId\": game_id", helper, StringComparison.Ordinal);
Assert.Contains("\"environmentId\": \"smoke\"", helper, StringComparison.Ordinal); Assert.Contains("\"environmentId\": \"smoke\"", helper, StringComparison.Ordinal);
Assert.Contains("\"regions\": [\"local\"]", helper, StringComparison.Ordinal); Assert.Contains("\"regions\": [\"local\"]", helper, StringComparison.Ordinal);
Assert.Contains("now + 600", helper, StringComparison.Ordinal); Assert.Contains("now + 600", helper, StringComparison.Ordinal);
Assert.Contains("stat.S_ISLNK", helper, StringComparison.Ordinal); Assert.Contains("stat.S_ISLNK", helper, StringComparison.Ordinal);
Assert.Contains("parent.st_mode & 0o077", helper, StringComparison.Ordinal); Assert.Contains("parent.st_mode & 0o077", helper, StringComparison.Ordinal);
Assert.Contains("metadata.st_mode & 0o022", helper, StringComparison.Ordinal); Assert.Contains("metadata.st_mode & 0o077", helper, StringComparison.Ordinal);
Assert.Contains("metadata.st_nlink != 1", helper, StringComparison.Ordinal); Assert.Contains("metadata.st_nlink != 1", helper, StringComparison.Ordinal);
Assert.Contains("mint-local-publisher-credential.sh", smoke, StringComparison.Ordinal); Assert.Contains("mint-local-publisher-credential.sh", smoke, StringComparison.Ordinal);
Assert.DoesNotContain("hexkey:", smoke, StringComparison.Ordinal); Assert.DoesNotContain("hexkey:", smoke, StringComparison.Ordinal);
Assert.DoesNotContain("openssl dgst", smoke, StringComparison.Ordinal); Assert.DoesNotContain("openssl dgst", smoke, StringComparison.Ordinal);
} }
[Fact]
public void UnscoutedComposeTenantIsGameScopedAndMetadataBounded()
{
string root = FindRepositoryRoot();
using JsonDocument settings = JsonDocument.Parse(File.ReadAllText(
Path.Combine(root, "deploy", "compose", "appsettings.Production.json")));
JsonElement provisioning = settings.RootElement.GetProperty("Rendezvous").GetProperty("Provisioning");
JsonElement game = provisioning.GetProperty("Games").EnumerateArray().Single(
static item => item.GetProperty("GameId").GetString() == "unscouted");
JsonElement key = provisioning.GetProperty("SigningKeys").EnumerateArray().Single(
static item => item.GetProperty("KeyId").GetString() == "local-smoke-unscouted-1");
Assert.Equal("smoke", game.GetProperty("EnvironmentId").GetString());
Assert.Equal([1], game.GetProperty("ProtocolVersions").EnumerateArray().Select(static value => value.GetInt32()));
Assert.Equal(["mode", "mods", "world"], game.GetProperty("MetadataValueMaxBytes")
.EnumerateObject().Select(static property => property.Name).Order(StringComparer.Ordinal));
Assert.Equal(["mode", "mods", "world"], game.GetProperty("RequiredMetadataKeys")
.EnumerateArray().Select(static value => value.GetString()).Order(StringComparer.Ordinal));
Assert.Equal(3, game.GetProperty("MetadataMaxKeys").GetInt32());
Assert.Equal("DedicatedEndpointAllowed", game.GetProperty("FallbackPolicy").GetString());
Assert.Equal("unscouted", key.GetProperty("GameId").GetString());
Assert.Equal("smoke", key.GetProperty("EnvironmentId").GetString());
Assert.Equal(["DedicatedPublisher"], key.GetProperty("CredentialKinds")
.EnumerateArray().Select(static value => value.GetString()));
}
[Fact] [Fact]
public void EveryIncidentRunbookHasDetectContainRecoverAndVerifyGates() public void EveryIncidentRunbookHasDetectContainRecoverAndVerifyGates()
{ {
@@ -184,6 +215,79 @@ public sealed partial class DocumentationContractTests
Assert.Equal(httpVersion, udpVersion); Assert.Equal(httpVersion, udpVersion);
Assert.Equal(httpVersion, ticketVersion); Assert.Equal(httpVersion, ticketVersion);
Assert.Contains($"contract version `{httpVersion}`", guide, StringComparison.Ordinal); Assert.Contains($"contract version `{httpVersion}`", guide, StringComparison.Ordinal);
Assert.Contains("gameplayNetwork.ChannelsCount = 3", guide, StringComparison.Ordinal);
Assert.Contains("defaults to one QoS channel", guide, StringComparison.Ordinal);
Assert.Contains("Rendezvous does not choose, remap, or reserve", guide, StringComparison.Ordinal);
}
[Fact]
public void SpaceGamePilotEvidenceSeparatesProvenBehaviorFromOpenGates()
{
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-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.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("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("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);
}
[Fact]
public void UnscoutedPilotEvidenceProvesAnIndependentGameBoundaryAndKeepsExternalGatesOpen()
{
string root = FindRepositoryRoot();
string guide = File.ReadAllText(Path.Combine(root, "docs", "integration", "unscouted-pilot.md"));
using JsonDocument evidence = JsonDocument.Parse(File.ReadAllText(
Path.Combine(root, "docs", "evidence", "consumers", "unscouted.json")));
JsonElement record = evidence.RootElement;
JsonElement run = record.GetProperty("godotRun");
JsonElement negative = record.GetProperty("negativePaths");
Assert.Equal("checkpoint-pass-with-external-gates", record.GetProperty("result").GetString());
Assert.Equal("unscouted", record.GetProperty("configuration").GetProperty("gameId").GetString());
Assert.Equal(["mode", "world", "mods"], record.GetProperty("configuration").GetProperty("metadataKeys")
.EnumerateArray().Select(static value => value.GetString()));
Assert.Equal("none", run.GetProperty("rendezvousGameplayPayloadPath").GetString());
Assert.Equal("unscouted-litenetlib", run.GetProperty("gameplayTransport").GetString());
Assert.True(run.GetProperty("directGameplay").GetBoolean());
Assert.True(run.GetProperty("fallbackGameplay").GetBoolean());
Assert.Equal(2, run.GetProperty("authenticatedSessions").GetInt32());
Assert.Equal("proven-exact-NotFound", negative.GetProperty("wrongGame").GetString());
Assert.Equal("proven-exact-NotFound", negative.GetProperty("wrongEnvironment").GetString());
Assert.Equal(3310, record.GetProperty("verification").GetProperty("consumerDebugTests").GetProperty("passed").GetInt32());
Assert.Equal(360, record.GetProperty("verification").GetProperty("consumerGdUnitTests").GetProperty("passed").GetInt32());
Assert.Contains("public-package-restore", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString()));
Assert.Contains("representative-external-nat", record.GetProperty("openGates").EnumerateArray().Select(static gate => gate.GetString()));
Assert.Contains("Do not mark #22 passed", guide, StringComparison.Ordinal);
Assert.Contains("not an Unscouted branch", guide, StringComparison.Ordinal);
Assert.DoesNotMatch(ReusableCredential(), guide);
} }
[GeneratedRegex(@"rv1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+", RegexOptions.CultureInvariant)] [GeneratedRegex(@"rv1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+", RegexOptions.CultureInvariant)]