Authenticate cross-Verse asset sources

This commit is contained in:
Meta Vens 2026-08-19 08:09:59 +02:00
commit 12637367e1
5 changed files with 79 additions and 10 deletions

View file

@ -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.

View file

@ -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<string, EveSurfaceCommandRequest> _pendingCommands =
new Dictionary<string, EveSurfaceCommandRequest>(StringComparer.Ordinal);
private readonly Dictionary<string, Task<ReceiptSubscription>> _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<string> rendezvousEndpoints)
private CultMeshClient CreateMeshClient(
IReadOnlyList<string> 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<EveAssetCatalogDocument>(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<EveAssetCatalogDocument>(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<string> 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)

View file

@ -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<string>();
@ -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);

View file

@ -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,

View file

@ -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": {