diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 0b03194..208639d 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -44,6 +44,10 @@ transport resolves that exact asset session and commits assets as one generation catalog, verified bundles, leases, lookups, render policy, and surface lowering are prepared off to the side, then swapped together. A failed or superseded candidate is disposed without unloading or rewriting the mounted generation. +The mounted provider's trust policy authenticates only that provider. A +provider-qualified asset source in another Verse/runtime uses the configured +cross-target Odin trust for its catalog, subscription, manifests, content, and +bundles; local-development trust does not cross that boundary. Navigation keeps the outgoing presentation quiesced until the candidate route is ready, then commits the provider route before activating the candidate. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 18f6405..a6e5245 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -31,6 +31,7 @@ namespace GameCult.Eve.UnityScene private readonly string _surfaceId; private readonly string _runtimeId; private readonly CultMeshAuthorityTrustPolicy _authorityTrust; + private readonly CultMeshAuthorityTrustPolicy _crossTargetAuthorityTrust; private readonly Dictionary _pendingCommands = new Dictionary(StringComparer.Ordinal); private readonly Dictionary> _receiptSubscriptions = @@ -83,7 +84,8 @@ namespace GameCult.Eve.UnityScene string surfaceId, string runtimeId = "eve-unity", CultMeshBodyPublicationResolver? bodyResolver = null, - CultMeshAuthorityTrustPolicy? authorityTrust = null) + CultMeshAuthorityTrustPolicy? authorityTrust = null, + CultMeshAuthorityTrustPolicy? crossTargetAuthorityTrust = null) { _cachePath = string.IsNullOrWhiteSpace(cachePath) ? throw new ArgumentException("Cache path must be non-empty.", nameof(cachePath)) @@ -107,6 +109,7 @@ namespace GameCult.Eve.UnityScene _bodyResolver = bodyResolver; _authorityTrust = authorityTrust ?? new CultMeshAuthorityTrustPolicy( CultMeshAuthorityTrustMode.AuthenticatedRemote); + _crossTargetAuthorityTrust = crossTargetAuthorityTrust ?? _authorityTrust; CurrentSurfaceDocument = EmptySurfaceDocument(); CurrentAssetManifestDocument = EmptyAssetManifest(); CurrentInputCapability = new EveInputCapabilityDocument(); @@ -493,16 +496,21 @@ namespace GameCult.Eve.UnityScene { if (_meshClient != null) return; - _meshClient = CreateMeshClient(new[] { _rendezvousEndpoint }); + _meshClient = CreateMeshClient(new[] { _rendezvousEndpoint }, _authorityTrust); } - private CultMeshClient CreateMeshClient(IReadOnlyList rendezvousEndpoints) + private CultMeshClient CreateMeshClient( + IReadOnlyList rendezvousEndpoints, + CultMeshAuthorityTrustPolicy authorityTrust) { return new CultMeshClient(new CultMeshClientOptions { RendezvousEndpoints = rendezvousEndpoints, Discovery = EveUnityCultMeshConnectivity.Discovery(), - Sessions = new CultMeshSessionManagerOptions { Trust = _authorityTrust }, + Sessions = new CultMeshSessionManagerOptions + { + Trust = authorityTrust ?? throw new ArgumentNullException(nameof(authorityTrust)) + }, Connectors = EveUnityCultMeshConnectivity.SchemaConnectors(), ContentConnectors = EveUnityCultMeshConnectivity.ContentConnectors(), RealtimeConnectors = new ICultMeshRealtimeTransportConnector[] @@ -1338,9 +1346,13 @@ namespace GameCult.Eve.UnityScene if (string.Equals(source.Identity, CurrentAssetSourceIdentity, StringComparison.Ordinal)) return await AssetMeshClient() .ReadAsync(source.Target, source.ManifestRecordRef, cancellationToken); - using var candidateClient = source.RendezvousEndpoints.Count == 0 - ? null - : CreateMeshClient(source.RendezvousEndpoints); + var crossTarget = IsCrossTarget(source); + var endpoints = AssetRendezvousEndpoints(source, crossTarget); + using var candidateClient = crossTarget || source.RendezvousEndpoints.Count > 0 + ? CreateMeshClient( + endpoints, + AssetAuthorityTrust(source)) + : null; return await (candidateClient ?? _meshClient ?? throw new InvalidOperationException("The CultMesh client is not open.")) .ReadAsync(source.Target, source.ManifestRecordRef, cancellationToken); @@ -1351,11 +1363,17 @@ namespace GameCult.Eve.UnityScene EveAssetCatalogDocument catalog, CancellationToken cancellationToken) { + var crossTarget = IsCrossTarget(source); + var endpoints = AssetRendezvousEndpoints(source, crossTarget); var candidate = new AssetGeneration( source, source.Identity, catalog.Version, - source.RendezvousEndpoints.Count > 0 ? CreateMeshClient(source.RendezvousEndpoints) : null); + crossTarget || source.RendezvousEndpoints.Count > 0 + ? CreateMeshClient( + endpoints, + AssetAuthorityTrust(source)) + : null); try { ConfigureAssetCatalog(candidate, catalog); @@ -1371,6 +1389,24 @@ namespace GameCult.Eve.UnityScene } } + private bool IsCrossTarget(AssetSource source) => + !string.Equals(source.Target.VerseId, _target.VerseId, StringComparison.Ordinal) || + !string.Equals(source.Target.AuthorityRuntimeId, _target.AuthorityRuntimeId, StringComparison.Ordinal); + + private CultMeshAuthorityTrustPolicy AssetAuthorityTrust(AssetSource source) => + IsCrossTarget(source) ? _crossTargetAuthorityTrust : _authorityTrust; + + private IReadOnlyList AssetRendezvousEndpoints(AssetSource source, bool crossTarget) + { + if (source.RendezvousEndpoints.Count > 0) + return source.RendezvousEndpoints; + if (crossTarget) + throw new InvalidOperationException( + $"Cross-target asset source '{source.Target.VerseId}/{source.Target.AuthorityRuntimeId}' " + + "must advertise at least one Odin rendezvous endpoint."); + return new[] { _rendezvousEndpoint }; + } + private async Task AttachAssetCatalogSubscriptionAsync( AssetGeneration candidate, CancellationToken cancellationToken) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs index fdc20d6..0576eed 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -172,6 +172,7 @@ namespace GameCult.Eve.UnityScene target.VerseId, target.AuthorityRuntimeId, _navigationAuthorityTrust, + _navigationAuthorityTrust, CancellationToken.None).ConfigureAwait(false); StagePrepared(prepared); @@ -355,6 +356,7 @@ namespace GameCult.Eve.UnityScene verseFilter, "", _authorityTrust, + _navigationAuthorityTrust, cancellationToken).ConfigureAwait(false); TraceStartup($"discovery and transport preparation {elapsed.Elapsed.TotalMilliseconds:0.###}ms"); AdoptPrepared(prepared); @@ -368,6 +370,7 @@ namespace GameCult.Eve.UnityScene string requiredVerseId, string requiredAuthorityRuntimeId, CultMeshAuthorityTrustPolicy trust, + CultMeshAuthorityTrustPolicy crossTargetTrust, CancellationToken cancellationToken) { var failures = new List(); @@ -395,7 +398,8 @@ namespace GameCult.Eve.UnityScene selection.ProviderId, selection.SurfaceId, runtimeId, - authorityTrust: trust); + authorityTrust: trust, + crossTargetAuthorityTrust: crossTargetTrust); try { await transport.PrepareAsync(cancellationToken).ConfigureAwait(false); diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs index 89906ca..d62393c 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs @@ -526,6 +526,31 @@ namespace GameCult.Eve.UnityScene.Tests UnityEngine.Object.DestroyImmediate(prefab); } + [Test] + public void CrossTargetAssetsUseConfiguredRemoteTrust() + { + var localTrust = new CultMeshAuthorityTrustPolicy(CultMeshAuthorityTrustMode.LocalDevelopment); + var remoteTrust = new CultMeshAuthorityTrustPolicy(CultMeshAuthorityTrustMode.AuthenticatedRemote); + using var transport = new EveUnityCultMeshLiveProviderTransport( + Path.Combine(Path.GetTempPath(), $"eve-asset-trust-{Guid.NewGuid():N}.cc"), + "cultnet+tcp://127.0.0.1:1", + "verse-a", + "authority-a", + "provider-a", + "surface-a", + authorityTrust: localTrust, + crossTargetAuthorityTrust: remoteTrust); + var trustMethod = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( + "AssetAuthorityTrust", + BindingFlags.Instance | BindingFlags.NonPublic)!; + + var localSource = CreateAssetSource("verse-a", "authority-a", "provider-a", "catalog-a"); + var remoteSource = CreateAssetSource("verse-b", "authority-b", "provider-b", "catalog-b"); + + Assert.That(trustMethod.Invoke(transport, new[] { localSource }), Is.SameAs(localTrust)); + Assert.That(trustMethod.Invoke(transport, new[] { remoteSource }), Is.SameAs(remoteTrust)); + } + private static object CreateAssetSource( string verseId, string authorityRuntimeId, diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index bac48fb..53b935d 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.126", + "version": "0.3.127", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": {