Skip to content

chore: e2e containerized infra - #9822

Open
mikhail-dcl wants to merge 22 commits into
devfrom
chore/e2e-infra
Open

chore: e2e containerized infra#9822
mikhail-dcl wants to merge 22 commits into
devfrom
chore/e2e-infra

Conversation

@mikhail-dcl

@mikhail-dcl mikhail-dcl commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

What does this PR change?

Client-side support for running the InWorld suite against a local or ephemeral Catalyst fixture (server side: explorer-e2e-infra#15).

TrustedRealms — the startup gate that decides whether a custom realm needs the untrusted-realm consent prompt was a run of inline host comparisons in MainSceneLoader. It moves into one policy with two tiers: exact hosts, and domains whose every subdomain is Decentraland-controlled. The domain tier is what makes the fixtures trusted at all — their hostname is minted per run (f-{id}.e2e-fixtures.decentraland.zone), so it cannot be enumerated as an exact host. Only decentraland.zone gets domain trust, only over https; production stays per-host so no single subdomain or dangling DNS record under it becomes a consent-free realm switch. A match now also short-circuits the lambdas/contracts/servers lookup.

--gateway <url> — routes every supported backend url through the given gateway base instead of gateway.decentraland.{env}, which is the host a fixture run cannot reach:

--gateway https://gateway.localhost
https://places.decentraland.org/api/places  →  https://gateway.localhost/places/api/places

Naming a base also forces routing on, since that is the opt-in the use-gateway feature flag would otherwise carry — a fixture serves no feature-flags backend. GetOriginalUrl reverses against the same base, so signed fetch still recovers the url it signed. With no arg the default path is unchanged byte for byte, and the arg outranks the flag but never the environment (today stays direct). Command line only: it aims a session's whole supported-service traffic at the named host, so it sits in the never-permitted deep-link tier next to gatekeeper-url and comms-adapter.

Test Instructions

metaforge explorer run 9822

Expected result: unchanged behaviour on a normal launch — production realms still route directly and a non-Decentraland --realm still raises the consent prompt.

Steps (the new paths):

  1. --realm https://f-<id>.e2e-fixtures.decentraland.zone → loads with no consent prompt and no peer.decentraland.org lookup.
  2. --gateway https://gateway.localhost → supported services are requested as https://gateway.localhost/{subdomain}/…, with the use-gateway flag off.
  3. decentraland://?realm=http://127.0.0.1:8000&gateway=https://evil.examplegateway is dropped and listed in the denied-params dialog.

Verified: Unity batch EditMode, 107/107 (AppArgsTest, DecentralandUrlsSourceShould, TrustedRealmsShould), no compile errors.

🤖 Generated with Claude Code

The startup gate that decides whether a custom realm needs the untrusted-realm
consent prompt lived as a run of inline host comparisons in MainSceneLoader.
Move it into TrustedRealms, split into two tiers: exact hosts, and domains whose
every subdomain is Decentraland-controlled. The domain tier is what lets the
ephemeral e2e-fixtures.decentraland.zone Catalyst fixtures be trusted at all --
their hostname is minted per run, so it cannot be enumerated as an exact host.

Only decentraland.zone gets domain-level trust. Production stays per-host, so
that neither one production subdomain nor one dangling DNS record under it can
become a consent-free realm switch for every user -- realm is a query param a
decentraland:// deep link may inject, and it sits on DeepLinkAllowlist's
always-permitted tier precisely because this gate exists to catch it.

Domain trust is https-only: a remote realm reached over cleartext can be
answered by a network attacker, so inheriting the domain's trust would hand that
trust to anyone on the path. Loopback opts out and stays scheme-agnostic, since
local scene development and a locally hosted E2E fixture are plain http and have
no meaningful network to attack. Suffix matching is anchored to a label boundary
at the end of the host, which is what rejects evildecentraland.zone and
decentraland.zone.example.com.

TrustedRealmsShould pins the policy, negative cases included: look-alike
registrations, cleartext inside a trusted domain, whole-domain production trust,
and trusted-looking strings in userinfo, query and fragment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mikhail-dcl
mikhail-dcl marked this pull request as ready for review August 20, 2026 15:17
@mikhail-dcl
mikhail-dcl requested review from a team as code owners August 20, 2026 15:17
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🚦 CI Status

Build

Windows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below.

Name Link
Commit 21c322a
Logs https://github.com/decentraland/unity-explorer/actions/runs/33511741473
Download Windows https://github.com/decentraland/unity-explorer/suites/90806599956/artifacts/9803519425
Download Windows S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/chore/e2e-infra/pr-25774-21c322a/Decentraland_windows64.zip
Download Mac https://github.com/decentraland/unity-explorer/suites/90806599956/artifacts/9803442138
Download Mac S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/chore/e2e-infra/pr-25774-21c322a/Decentraland_macos.zip
Built on 2026-09-01T13:52:00Z

Lint

Warnings count reduced: 12191 => 12190

Warnings/errors in files changed by this PR (18)
Assets/DCL/Infrastructure/Utility/Networking/DCLWebSocket.cs:134  AssignNullToNotNullAttribute  Possible 'null' assignment to non-nullable entity
Assets/DCL/Infrastructure/Global/Dynamic/WorldManifestProvider.cs:54  CSharpWarnings::CS0162  Code is unreachable
Assets/DCL/Infrastructure/Global/Dynamic/WorldManifestProvider.cs:78  CSharpWarnings::CS8600  Converting null literal or possible null value into non-nullable type
Assets/DCL/Infrastructure/Global/Dynamic/WorldManifestProvider.cs:119  CSharpWarnings::CS8600  Converting null literal or possible null value into non-nullable type
Assets/DCL/Infrastructure/Global/Dynamic/WorldManifestProvider.cs:79  CSharpWarnings::CS8604  Possible null reference argument for parameter 'dto' in 'ECS.WorldManifest.Create'
Assets/DCL/Infrastructure/Global/Dynamic/WorldManifestProvider.cs:120  CSharpWarnings::CS8604  Possible null reference argument for parameter 'dto' in 'ECS.WorldManifest.Create'
Assets/DCL/Multiplayer/Connections/RoomHubs/RoomHub.cs:30  CSharpWarnings::CS8618  Non-nullable fields 'islandIdentities', 'sceneIdentities' must contain non-null values when exiting constructor. Consider adding the 'required' modifiers or declaring the fields as nullable.
Assets/DCL/Infrastructure/Global/Dynamic/WorldManifestProvider.cs:54  EmptyStatement  Empty statement is redundant
Assets/DCL/Infrastructure/Global/Dynamic/WorldManifestProvider.cs:54  HeuristicUnreachableCode  Code is heuristically unreachable
Assets/DCL/WebRequests/WebRequestUtils.cs:111  InconsistentNaming  Name 'IsDNSLookupError' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IsDnsLookupError'.
Assets/DCL/WebRequests/WebRequestUtils.cs:156  InconsistentNaming  Name 'IsSSLCACertificateError' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IsSslcaCertificateError'.
Assets/DCL/WebRequests/WebRequestUtils.cs:153  InconsistentNaming  Name 'IsUnableToCompleteSSLConnection' does not match rule 'members_should_be_pascal_case'. Suggested name is 'IsUnableToCompleteSslConnection'.
Assets/DCL/Infrastructure/Global/Dynamic/WorldManifestProvider.cs:65  InconsistentNaming  Name 'worldURL' does not match rule 'parameters_should_be_camel_case'. Suggested name is 'worldUrl'.
Assets/DCL/Infrastructure/Global/Tests/EditMode/LocalUntrustedRealmCommsPolicyShould.cs:1  RedundantUsingDirective  Using directive is not required by the code and can be safely removed
Assets/DCL/Multiplayer/Connections/RoomHubs/IRoomHub.cs:4  RedundantUsingDirective  Using directive is not required by the code and can be safely removed
Assets/DCL/Multiplayer/Connections/RoomHubs/NullRoomHub.cs:5  RedundantUsingDirective  Using directive is not required by the code and can be safely removed
Assets/DCL/Multiplayer/Connections/RoomHubs/NullRoomHub.cs:8  RedundantUsingDirective  Using directive is not required by the code and can be safely removed
Assets/DCL/Multiplayer/Connections/RoomHubs/RoomHub.cs:7  RedundantUsingDirective  Using directive is not required by the code and can be safely removed

Tests

All Unity tests passed ✅

TESTS SUITE Result Passed Failed Skipped
EditMode ✅ Passed 25699 0 13
PlayMode ✅ Passed 248 0 37

@decentraland-bot
decentraland-bot self-requested a review August 20, 2026 15:18
@mikhail-dcl
mikhail-dcl marked this pull request as draft August 20, 2026 15:19
@mikhail-dcl mikhail-dcl changed the title chore: extract the trusted-realm host policy into TrustedRealms chore: e2e containerized infra Aug 20, 2026
@mikhail-dcl mikhail-dcl added the force-build Used to trigger a build on draft PR label Aug 20, 2026

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — TrustedRealms extraction

STEP 2 — Root-cause check: PASS

This PR extracts inline trusted-realm host checks from MainSceneLoader.IsTrustedRealmAsync into a dedicated, testable TrustedRealms static class. The extraction solves the right problem: the inline if-chain was untestable in isolation and could not accommodate dynamically minted E2E fixture hostnames (f-{id}.e2e-fixtures.decentraland.zone). The new two-tier model (exact hosts + domain wildcards) addresses both needs cleanly.

STEP 3 — Design & integration: PASS

TrustedRealms is a pure static utility — compile-time-constant arrays and pure functions. No lifecycle, no subscriptions, no state across frames. The MANDATORY OWNER SEARCH does not apply (this is not a system, plugin, manager, or stateful helper).

Existing domain-suffix logic in ChatEnvironmentValidator.HostHasSuffix: Both implement dot-anchored suffix matching, but they serve different purposes (trust gating vs. environment validation for chat teleport) and operate at different levels (pre-parsed Uri.Host vs. raw string with span-based parsing). Consolidation would couple unrelated concerns — no action needed.

Teardown / consumption trace: N/A — no subscriptions, callbacks, connections, or disposable resources.

STEP 4 — Member audit: PASS

Member Consumers Verdict
IsTrusted(Uri) 1 (MainSceneLoader.IsTrustedRealmAsync) Justified extraction for testability at a security boundary — the 78-line test suite covering 20+ attack vectors is the payoff. Not a single-use wrapper.
IsWithinDomain(string, string) 1 (internal to IsTrusted) Private helper encapsulating the boundary-anchored suffix match. Appropriate scope.

STEP 5 — Line-level review: No blocking issues

Behavioral changes (intentional, verified by tests):

  1. Trust broadened for .zone: Three specific *.decentraland.zone hosts → all *.decentraland.zone subdomains. Required for dynamic E2E fixture hostnames. Mitigated by HTTPS-only enforcement on domain-level trust (line 73).

  2. Trust tightened for cleartext .zone: http://sdk-test-scenes.decentraland.zone (and the other two .zone hosts) was trusted before (old code was scheme-agnostic); now untrusted since domain-level trust is HTTPS-only. This is a security improvement. Tests NotTrustCleartextInsideAControlledDomain pin this.

  3. Case sensitivity fixed: Old code used == (case-sensitive); new code uses StringComparison.OrdinalIgnoreCase. Correct since DNS is case-insensitive.

Security analysis:

  • IsWithinDomain suffix match is correctly anchored to a label boundary (dot check at host[host.Length - domain.Length - 1]), preventing look-alikes (evildecentraland.zone). The host.Length > domain.Length + 1 guard prevents a bare-dot prefix from matching.
  • HTTPS-only gate for domain trust prevents MITM on cleartext connections.
  • Tests cover: look-alike domains, URI part spoofing (userinfo, query, fragment), cleartext rejection, FQDN trailing dot, unrelated hosts. Negative cases are comprehensive.
  • No hardcoded secrets, no injection vectors, no input validation gaps.

Code quality:

  • SCREAMING_SNAKE_CASE for TRUSTED_HOSTS / TRUSTED_DOMAINS is consistent with project convention (IDecentralandUrlsSource.ORG_DOMAIN, ZONE_DOMAIN, ALL_DOMAINS).
  • Comprehensive XML documentation on every member, explaining security rationale.
  • Non-nullable Uri realm parameter correctly has no null guard (CLAUDE.md §11: "don't null-check non-null declarations").
  • Test naming follows TrustedRealmsShould.* convention with descriptive method names.

No security issues found. (Security review skill completed.)

STEP 6 — Complexity: SIMPLE

Three meaningful files changed (excluding .meta), under 150 lines of logic. Pure static utility extraction with no ECS, async, plugin, or lifecycle involvement.

STEP 7 — QA: YES

Modifies runtime trust-check behavior — the set of realms that bypass the consent prompt has changed (broadened for HTTPS .zone, tightened for cleartext .zone).

STEP 8 — Warnings: None

STEP 9 — Verdict

REVIEW_RESULT: PASS ✅
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: Pure static utility extraction from MainSceneLoader; no ECS, async, or lifecycle changes
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub

github-actions[bot]
github-actions Bot previously approved these changes Aug 20, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-approved based on Jarvis review — simple fix/chore with no blocking issues. QA approval is still required.

mikhail-dcl and others added 3 commits August 21, 2026 11:00
Halve the comment volume on the trusted-realm host policy and state the rules
as constraints rather than narration: what may join TRUSTED_HOSTS and
TRUSTED_DOMAINS, why domain trust is https-only, and what the label-boundary
match rejects.

Drop the claim that malformed-realm handling stays with the caller -- the
caller does a bare new Uri(realm), which throws.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Gateway routing is gated on the use-gateway feature flag, which a fixture run
cannot set: an offline fixture serves no feature-flags backend, so the one code
path the gateway mock exists to exercise was unreachable from a test launch.

--gateway overrides the flag in both directions (--gateway / --gateway true force
it on, --gateway false forces it off) and leaves the decision to the flag when
absent. It overrides the flag, never the environment: today has no gateway and
stays unrouted however the arg is set. No --debug is required, mirroring --pulse,
because the arg only picks between Decentraland's own gateway and its own service
hosts.

The override reaches GatewayUrlsSource as a bool? rather than a resolved bool
because the source reads the flag lazily -- feature flags land after it is
constructed, so the value to fall back to does not exist at parse time.
ResolveFeatureFlagOverride is that tri-state read; ResolveFeatureFlagArg now
delegates to it, so both spellings share one definition of the semantics.

Deep links can never set it. It stays out of both permitted sets, which is
deny-by-default, and is named in the never-permitted tier next to pulse; the
DeepLinkParamDescriptions entry makes a denial render as what it does rather than
as "not recognized". A deep link that reroutes every backend url for a session is
exactly the class of param that tier exists for.

Verified in Unity batch EditMode: 75/75 pass with the six new cases among them,
no compile errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

STEP 1 — Context & Scope

Files read: CLAUDE.md, all 13 changed files, plus surrounding context: IDecentralandUrlsSource.cs (domain constants), DeepLinkAllowlist.cs (full — deep link security model), GatewayUrlsSource.cs (full — gateway routing), MainSceneLoader.cs (construction site lines 255–290 + IsTrustedRealmAsync lines 837–862), FeaturesRegistry.cs (all 30+ ResolveFeatureFlagArg callers), IRealmNavigator.cs (TEST_SCENES_URL). Repo cloned and checked out to PR head for rg searches.

Subsystem docs: App arguments, deep link security model (SEC-004/005/019/020/052), gateway URL routing, trusted realm consent prompt.

STEP 2 — Root-cause check

Problem: E2E fixture runs need (a) trusted realm status for dynamically-minted hostnames under decentraland.zone and (b) gateway routing without a feature-flags backend.

Does the diff fix the cause? Yes. Both features address genuine infrastructure gaps:

  • TrustedRealms extracts previously-untestable inline host checks into a policy class with a domain-tier that covers dynamically-minted fixture hostnames — the only way to trust them without hardcoding each one.
  • --gateway adds a CLI override for a feature flag that e2e fixtures cannot serve, using the existing ResolveFeatureFlagArg pattern decomposed into a three-valued ResolveFeatureFlagOverride.

PASS — no symptom masking.

STEP 3 — Design & Integration

TrustedRealms — new static utility class. Stateless (no fields, no lifecycle, no persistent collections). Pure function: Uri → bool. Not a system, plugin, manager, or controller. The extraction from MainSceneLoader is justified: it makes the trust policy unit-testable (the inline code was previously untestable) and consolidates the policy in one place with a documented two-tier model.

Owner search: IsTrustedRealmAsync in MainSceneLoader (line 848) is the sole consumer. The trusted-host checks were inline in that method. No other class creates or destroys trust state — there is no lifecycle owner to conflict with. Files searched: MainSceneLoader.cs, RealmController.cs, ChatEnvironmentValidator.cs, IRealmNavigator.cs. The static utility is the correct home.

ResolveFeatureFlagOverride — decomposes the existing ResolveFeatureFlagArg into a nullable override (the three-valued part) + fallback application. ResolveFeatureFlagArg delegates to it with ?? fallback, preserving exact behavioral identity for all 30+ existing callers in FeaturesRegistry.cs. No duplication — the new method returns bool? which models the three states (set-true, set-false, not-set) that bool cannot express. Needed because GatewayUrlsSource.enabled evaluates the flag lazily at access time, so it cannot resolve a fallback at construction.

GatewayUrlsSource.cliUseGateway — constructor-injected bool? with default null. Backward-compatible: all existing callers verified (MainSceneLoader line 277, DecentralandUrlsSourceShould tests, CreateForTest factory). The enabled property correctly short-circuits: envSupported && (cliUseGateway ?? flag) — the environment check always runs first, so today is never routed even with cliUseGateway: true. Tested by KeepTodayOffTheGatewayWhenTheArgForcesItOn.

Teardown trace: No subscriptions, event hookups, connections, or disposable resources introduced. All new code is pure computation.

PASS — no design issues.

STEP 4 — Member audit

Member Consumers Verdict
TrustedRealms.IsTrusted(Uri) MainSceneLoader.IsTrustedRealmAsync (1 prod), TrustedRealmsShould (tests) Static utility extracted from inline code; testability gain justifies the class. Not single-use indirection — the class adds a domain-tier the inline code lacked.
TrustedRealms.IsWithinDomain(string, string) IsTrusted (1, private) Private helper, correct scope.
ResolveFeatureFlagOverride MainSceneLoader (1 prod), ResolveFeatureFlagArg (1 delegation), AppArgsTests (2 tests) Legitimate new API: the nullable return models three states that bool cannot express.
GatewayUrlsSource.cliUseGateway enabled property (1, private) Private field, clean.
AppArgsFlags.GATEWAY MainSceneLoader, AppArgsTests, DeepLinkParamDescriptions, deep link test Proper constant following existing pattern.

PASS — no single-use-merge, absent≠false, or redundant-guard issues.

STEP 5 — Line-level review

Pass A (blocking issues): None found.

  • No CLAUDE.md violations: naming PascalCase ✓, no LINQ ✓, allocation-free utility (foreach over small string[]) ✓, no persistent state ✓
  • No bugs: IsWithinDomain boundary math verified for all edge cases — host[host.Length - domain.Length - 1] == '.' correctly rejects suffix-lookalikes (evildecentraland.zone → char at boundary is l, not .), host.Length > domain.Length + 1 prevents out-of-bounds, EndsWith anchors to the right
  • No security vulnerabilities: Uri.Host is used (immune to userinfo/fragment/query spoofing), HTTPS-only for domain tier, deny-by-default deep link model
  • No performance issues: foreach over small arrays, string.Equals/EndsWith with OrdinalIgnoreCase
  • No missing error handling, no resource leaks, no detached async, no nullability violations

Pass B (design smells): None found.

  • Naming correct — TrustedRealms names the responsibility, IsWithinDomain names the check
  • No magic values — domains reference IDecentralandUrlsSource.ORG_DOMAIN/ZONE_DOMAIN constants
  • Comments explain security rationale with SEC ticket references without narrating caller behavior
  • No YAGNI: both features serve the stated E2E fixture use case

Security review:

  • ✅ Domain trust: IsWithinDomain dot-boundary check rejects evildecentraland.zone, decentraland.zone.example.com. Tests pin all negative cases including trailing-dot and multi-label-depth hosts.
  • ✅ HTTPS-only for domain tier: cleartext downgrade attack prevented; exact hosts (loopback) opt out since loopback has no meaningful network to attack.
  • ✅ Deep link denial: gateway not in PERMITTED_KEYS or WHITELISTED_REALM_PERMITTED_KEYS → dropped by deny-by-default. Test DeepLinkDropsExecAndInfraParamsEvenForLoopbackRealm verifies. Description in DeepLinkParamDescriptions for consent dialog.
  • requireDebug: false: appropriate — the arg picks between Decentraland's own gateway and its own service hosts, no debug-mode capability unlock.
  • IsGatewayTransformable constrains transformation to single-label-subdomain HTTPS URLs under .decentraland.{tld} with no port or userinfo — CLI flag cannot redirect traffic to attacker infrastructure.
  • ✅ No secrets, no injection vectors, no auth bypass.

Parallel sub-agent review results:

  • Security agent: No issues. Verified domain boundary checks, URI spoofing resistance, HTTPS enforcement, deep link denial, requireDebug appropriateness.
  • Architecture agent: No issues. TrustedRealms extraction justified, ResolveFeatureFlagOverride is proper decomposition, GatewayUrlsSource change backward-compatible, no CLAUDE.md violations.
  • Code quality agent: No issues in PR scope. Naming conventions followed throughout, allocation-free paths, excellent test coverage with security-regression pinning, idiomatic NUnit [TestCase] attributes.

STEP 6 — Complexity

COMPLEX — modifies security-sensitive trust policy (realm consent-prompt bypass), public API surface (ResolveFeatureFlagOverride), and gateway URL routing logic across 13 files.

STEP 7 — QA assessment

QA_REQUIRED: YES — changes affect runtime behavior: which realms show the consent prompt (user-visible) and how gateway routing resolves (network connectivity).

STEP 8 — Non-blocking warnings

None. Main.unity is not modified.


REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Modifies the trusted-realm security policy (consent-prompt bypass) and gateway URL routing, both runtime-critical and security-sensitive.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by mikhail-dcl via GitHub

@github-actions
github-actions Bot dismissed their stale review August 21, 2026 08:34

Dismissed: latest Jarvis review no longer auto-approves this PR.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Jarvis reviewed this PR and found no blocking issues, but assessed it as complex — human DEV review is still required before merging.

…e flag

The first cut read --gateway as a boolean over the use-gateway flag, mirroring
--pulse. That controls whether the transform runs, not where it points: the
gateway host is built from --dclenv and stayed gateway.decentraland.{org,zone},
which is the one host a fixture run cannot reach. As a counterpart to the
fixture's gateway mock it was useless -- it aimed harder at production.

--gateway now takes the base to route through, and naming one forces routing on,
since that is the opt-in the flag would otherwise carry:

  --gateway https://gateway.localhost
  https://places.decentraland.org/api/places -> https://gateway.localhost/places/api/places

GetOriginalUrl reverses against the same base, so signed fetch keeps recovering
the un-gatewayed url it signed. The default path is untouched byte for byte: with
no arg the host is still derived from the url's own domain, so no production
routing moves. The arg outranks the flag, never the environment -- today has no
gateway and stays direct.

That makes it a host override rather than a toggle, which is why it sits in the
never-permitted deep-link tier next to gatekeeper-url and comms-adapter rather
than beside the dev-mode flags: a link that could set it would route a session's
whole supported-service traffic through a server of its choosing.

ResolveFeatureFlagOverride is reverted with the boolean it existed for.

Verified in Unity batch EditMode: 107/107 (AppArgsTest, DecentralandUrlsSourceShould,
TrustedRealmsShould), no compile errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@decentraland-bot

This comment has been minimized.

mikhail-dcl and others added 2 commits August 25, 2026 11:15
--base-domain (#9826) landed in dev and overlaps every file --gateway touches.
Five conflicts, resolved as follows.

GatewayUrlsSource: dev moved the gateway host off the hardcoded env domain onto
gateway.{BaseDomain}, so --gateway now overrides that resolved prefix rather than
a decentraland-shaped one. gatewayPrefix became the single origin both paths
build from, which removes the second branch the transform carried and drops the
domain-derivation the default path used to do -- dev's IsGatewayTransformable
already restricts transformable hosts to this deployment's own base domain, so
nothing else can reach it. The shape follows #9845, which implements the same
override on top of the same dev change.

The value is now validated and normalized (NormalizeGatewayPrefix): an absolute
http(s) url with a host and no query or fragment, reduced to a prefix ending in
'/'. Anything else ends the launch instead of being coerced, because a mistyped
gateway silently routes every supported service somewhere unintended.

MainSceneLoader: kept TrustedRealms.IsTrusted over dev's re-inlined host list,
and kept dev's --base-domain arm alongside it. --gateway is now captured while
the deep link is still deferred and reported through
WarnIfCommandLineOnlyArgCameFromTheDeepLink, matching --base-domain and
--eth-network: reading it later meant accepting it in the denied-params dialog
would have applied it, which is not what "command line only" means.

AppArgsFlags, DeepLinkAllowlist and docs/app-arguments.md: both sides kept, with
gateway named in the same never-permitted tier as the other infrastructure-
pointing params.

Verified in Unity batch EditMode: 203/203 across AppArgsTest,
DecentralandUrlsSourceShould, TrustedRealmsShould, ChatEnvironmentValidatorShould
and RealmLaunchSettingsShould -- dev's base-domain cases included -- no compile
errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Throwing ArgumentException from GatewayUrlsSource's constructor made a mistyped
flag surface as a crash in bootstrap, while the flag next to it reports what is
wrong and ends the launch. Same class of mistake, so same treatment.

Validation moves to the capture site as CaptureGatewayArg, mirroring
CaptureEthNetworkArg: it reports which value was rejected and what was expected,
and returns false so the caller exits. The rule keeps one home --
GatewayUrlsSource.TryNormalizeGatewayPrefix -- and the constructor now takes the
normalized prefix the launch path already validated.

Verified in Unity batch EditMode: 205/205, no compile errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@decentraland-bot

This comment has been minimized.

mikhail-dcl and others added 2 commits August 27, 2026 14:24
An e2e fixture serves its global comms adapter over cleartext
(`fixed-adapter:signed-login:http://127.0.0.1:8080/...`), which the
Archipelago fork had no tier for: the launch died on "Cannot determine the
protocol from the about url".

`--accept-untrusted-realm` widens the fixed-room tier to a cleartext adapter
whose host is loopback. Remote http stays rejected, https/wss are untouched,
and the flag is absent from the deep-link allowlist because it lowers a
transport guarantee — never a link's call to make.

Loopback detection becomes one implementation. `Utility.Networking.LoopbackUrls`
reads the host out of the authority as a span — no `Uri` parse, no `Substring`
— so it costs nothing on the per-request paths that used to duplicate it, and
it rejects the lookalikes a prefix match let through (`127.0.0.1.example.com`,
`127.0.0.1@example.com`). It replaces `WebRequestUtils.IsLocalhost`,
`McpHttpServer`'s origin check (which also never matched an IPv6 loopback
origin, since `Uri.Host` brackets it) and the two loopback entries in
`TrustedRealms`. `DeepLinkAllowlist` deliberately keeps `Uri.IsLoopback`: its
semantics are wider and pinned by its own cases.

`RefinedAdapterAddresses` now strips the handshake pre-info in front of an http
adapter too, as it already did for https and wss. Without that the fork picks
`FixedConnectiveRoom` and the room signs a fetch of
"fixed-adapter:signed-login:http://..." verbatim. The three near-identical
strip methods collapse into one pass cutting at the earliest scheme, so a
scheme inside the url's own query no longer truncates it.

Verified: Unity 6000.4.0f1 batch EditMode, 251/251 — LoopbackUrlsShould,
ArchipelagoProtocolSelectionShould, RefinedAdapterAddressesShould, AppArgsTest,
TrustedRealmsShould, McpHttpServerShould, DecentralandUrlsSourceShould.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two conflicts, both the same shape: dev's abgen pipeline (#9835) and this
branch's --gateway each appended a trailing optional parameter to
GatewayUrlsSource. Kept both, with the base-class parameter in the position
the base constructor declares it and the derived-only one last:

  GatewayUrlsSource(..., customBaseDomain, abgenPipelineForced, cliGatewayPrefix)

MainSceneLoader passes both at the call site. No other call site is positional
past customBaseDomain, so the order breaks nothing.

The two features compose without further work: an abgen url resolves as
FeatureFlagsDependent, which GatewayUrlsSource.RawUrl already declines to
gateway-rewrite.
@decentraland-bot

This comment has been minimized.

pentreathm and others added 3 commits August 27, 2026 19:32
--gateway aims every supported service at one origin, and adding WorldServer
to that set moved world realms onto it too. ChatEnvironmentValidator only
accepted hosts under BaseDomain, so a local e2e fixture — whose gateway is
loopback by design — failed every /goto <world> with "you cannot teleport to
realms outside decentraland.org", rejecting the realms the client was itself
routing.

Expose the origin as IDecentralandUrlsSource.GatewayOrigin, null unless
routing is actually on, and accept it ahead of the domain check. Naming a
gateway is command line only, so this adds no link-reachable surface, and the
origin keeps its trailing '/' — the authority boundary that stops
http://127.0.0.1:8080.attacker.com/ from matching http://127.0.0.1:8080/.
Today still refuses every realm: its guard stays ahead of both checks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pentreathm pentreathm added force-build Used to trigger a build on draft PR and removed force-build Used to trigger a build on draft PR labels Aug 28, 2026
@decentraland-bot

This comment has been minimized.

@pentreathm pentreathm added force-build Used to trigger a build on draft PR and removed force-build Used to trigger a build on draft PR labels Aug 28, 2026
@decentraland-bot

This comment has been minimized.

@decentraland-bot

This comment has been minimized.

@decentraland-bot

This comment has been minimized.

@decentraland-bot

This comment has been minimized.

mikhail-dcl and others added 2 commits August 31, 2026 18:25
The three local rooms are started together and reported as one boolean, so
both the health check failure and the 30s startup timeout above it read the
same whichever room is stuck. IRoomHub now reports each started room's state,
connect attempt and connection loop health, and both failure paths carry it:
a room still handshaking shows as Starting/None, a refused one as Error, and
a scene-banned one as ForbiddenAccess.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dev removed the Today environment (#9885), which is what the gateway's
environment gate and the chat validator's first check existed for, so the
resolutions drop both and keep the --gateway origin on top of dev's shape:

- GatewayUrlsSource: dev's non-nullable prefix trio, built unconditionally,
  with the cli origin still overriding gateway.{BaseDomain}; enabled is now
  "an origin was named or the flag says so", no env gate. The null guard in
  TransformToGateway goes with it - the field can no longer be null.
- ChatEnvironmentValidator: the gateway-origin check stays, the Today
  rejection and the environment ctor argument go.
- Tests: the two Today cases drop, the gateway cases lose the environment
  argument, and the gatewayOrigin overload of the urls-source helper stays.
- RequestEnvelope: keep Utility.Networking for the local certificate handler,
  take dev's removal of the unused UnityEngine.Pool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@decentraland-bot

This comment has been minimized.

mikhail-dcl and others added 2 commits September 1, 2026 11:30
WorldCommsAdapter was missing from GatewayUrlsSource, so a session started with
--gateway signed its scene-room handshake for the fixture and sent it to the
public worlds-content-server, which answered 404. Its siblings WorldServer and
WorldContentServer were already routed; this leaves an e2e run reaching the
internet for the one request that decides whether a World's comms come up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@decentraland-bot

This comment has been minimized.

@mikhail-dcl
mikhail-dcl marked this pull request as ready for review September 1, 2026 13:09
@claude

claude Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @mikhail-dcl's task in 3m 53s —— View job


Dependency & supply-chain security review

  • Identify all dependency changes in the diff
  • Look up registry/provenance metadata — blocked, see note below
  • Trace what the bumped dependency is used for in this PR
  • Assess provenance, capability, shipping impact, transitive risk
  • Workflow / prompt review — not applicable (no .github/workflows/** or .github/prompts/** changes, no new binaries)

STEP 1 — Dependency changes

Exactly one, in Explorer/Packages/manifest.json:13 and mirrored in packages-lock.json:93,101:

Field Value
name com.decentraland.livekit-sdk
change 222d67ccc289337dadb5e8d96ee5b633dfb905b6a4bd750e2262b9eeeee75dc83858017bbd3679ef
source https://github.com/decentraland/client-sdk-unity.git (git, Decentraland-owned fork of livekit/client-sdk-unity)
type source + native LiveKit FFI (rust livekit-ffi) — inference from the LiveKit.Internal.FFIClients namespace used at the call site
scope runtime, ships to end users — it is the comms transport for voice/chat/island rooms
pinning 40-char commit SHA, immutable ✅

No other manifest entries changed. No .asmdef changes, no new binaries, no editor/install hooks, no scoped-registry changes. Transitive deps of the package are byte-identical in the lockfile (com.cysharp.unitask, com.nickkhalow.richtypes, io.livekit.unity) — no new transitive surface from this PR.

STEP 1.5 — Registry metadata: could not be gathered

I have no WebFetch permission and gh api against a repo other than decentraland/unity-explorer is not in my allowlist, so I could not open github.com/decentraland/client-sdk-unity, diff 222d67c...a4bd750, or read the fork's commit list, authorship, or signature status. Everything below about the SDK's contents is [Inference] or [UNKNOWN], never inspected fact. To let me verify this properly on a future run, add WebFetch and Bash(gh api repos/decentraland/*) to --allowedTools.

STEP 2 — Assessment

A. Provenance[PR] Decentraland-owned org, SHA-pinned, same repo as the existing pin. Bumping a first-party fork by SHA is the right shape. [UNKNOWN] the actual commit range, its authors, and whether the fork picked up upstream commits alongside the Decentraland change.

B. Capability — this is the part that needs a human 🟡

The bump is not cosmetic. Commit 2bb038a9b "fix: pin loopback ICE SDK build" exists to supply a new public mutable static in the SDK that this PR writes to from two places:

  • Explorer/Assets/DCL/Infrastructure/Global/Dynamic/MainSceneLoader.cs:260FFIBridgeExtensions.UseTransportAllForLoopbackUrls = applicationParametersParser.HasFlag(AppArgsFlags.ACCEPT_UNTRUSTED_REALM);
  • Explorer/Assets/DCL/Infrastructure/Global/Dynamic/RealmController.cs:186-189 — recomputed per realm via LocalUntrustedRealmCommsPolicy.ShouldUseTransportAll

[Inference] the name maps to LiveKit's IceTransportType: the client moves from relay-only to allowing direct host/srflx candidates, which changes what local network addresses are offered to peers. [UNKNOWN] what it actually does, and — more importantly — whether the SDK-side guard the client-side comments rely on exists at all:

LocalUntrustedRealmCommsPolicy.cs:14"The LiveKit SDK applies the final URL-level loopback check as well, so entering a remote world later cannot inherit it."
MainSceneLoader.cs:257-259"The SDK still applies its own loopback URL check, so this opt-in cannot change transport behavior for a remote realm."

Both comments assert a security property that lives entirely inside the unreviewable dependency. The client-side layers are sound on their own (--accept-untrusted-realm is CLI-only, dropped by the deny-by-default deep-link allowlist with a regression test at AppArgsTests.cs:71, and RealmController re-narrows to a loopback CatalystBaseUrl after every realm change). But note that MainSceneLoader.cs:260 sets the unnarrowed value at startup, and the first RealmController bootstrap is what narrows it — so between those two points the SDK-side check is the only thing standing between the flag and a non-loopback room. Someone with access to the fork should confirm that check exists in a4bd750.

C. Shipping impact[PR] runtime, all desktop platforms. The relaxed path is gated behind a CLI-only flag, so default retail behaviour is unchanged.

D/E. Maintenance / transitive[UNKNOWN] by the same access limitation. No new transitive deps. Pre-existing (not introduced here, but worth a separate ticket): io.livekit.unity and com.nickkhalow.richtypes are floating git URLs with no ref, so they resolve to whatever the default branch holds at resolve time.

STEP 3 — Risk classification

Dependency / file Version Source Type Scope Risk Confidence
com.decentraland.livekit-sdk a4bd750 (was 222d67c) git, decentraland fork source + native FFI runtime, ships MEDIUM LOW (diff not inspectable in this environment)

MEDIUM, not LOW — first-party and SHA-pinned, but it carries a transport-policy change whose safety argument is stated in this PR's comments and verifiable only in the dependency. Not HIGH — no unpinned ref, no unknown publisher, no dynamic loading, and the capability is proportionate to the stated e2e-fixture purpose.

Recommendation: before merge, have a reviewer with repo access confirm on decentraland/client-sdk-unity@a4bd750 that (a) the diff from 222d67c is limited to UseTransportAllForLoopbackUrls and does not pull unrelated upstream commits, and (b) the SDK-side loopback URL check the client comments depend on actually exists.


Non-dependency findings from the same diff

1. docs/app-arguments.md:166 contradicts the code — --accept-untrusted-realm does more than documented. The doc says:

"It changes nothing else: TLS validation, the untrusted-realm consent prompt and the https/wss paths are all untouched."

That was true when written (fb1a2d16d), but two later commits made it false:

  • 7b7d7127e added LocalCertificateValidation, which disables TLS certificate validation for loopback https/wss when the flag is set — wired into RequestEnvelope.cs:102, DCLWebSocket.cs:101, and DeepLinkWorldWhitelistProvider.cs:38. So TLS validation is touched, and the https/wss paths are touched.
  • 2bb038a9b/e970cdd83/c3a811ac2 added the ICE transport change above, which the doc does not mention at all.

The bypass itself looks correctly scoped (loopback-only, opt-in, default off), so this is a documentation accuracy problem on a security-relevant flag rather than a code defect — but for a flag whose whole job is lowering guarantees, the doc understating its blast radius is worth fixing before merge. Fix this →

2. --accept-untrusted-realm has no DeepLinkParamDescriptions entry. gateway was added at DeepLinkParamDescriptions.cs:32, but ACCEPT_UNTRUSTED_REALM was not, so a crafted decentraland:// link carrying it shows the generic "Not recognized by this version - its effect is unknown." in the consent dialog. The class doc says entries are needed for "params a DeepLinkAllowlist denial can realistically surface" — and AppArgsTests.cs:65 constructs exactly such a link. The key is still correctly dropped, so this is a user-messaging gap, not a bypass. Fix this →

Observations (no action needed):

  • TryNormalizeGatewayPrefix (GatewayUrlsSource.cs:136) accepts http:// for any host, not just loopback — --gateway http://x.example would send all supported-service traffic, signed fetches included, over cleartext. Consistent with the existing --gatekeeper-url / --comms-adapter tier (CLI-only, deny-listed for deep links, operator's own call), so I'm flagging it only so the asymmetry against loopback-restricted --accept-untrusted-realm is a conscious choice.
  • The double-transform guard in TransformToGateway narrowed from "subdomain equals gateway" to "url starts with the full prefix". A bare https://gateway.{BaseDomain} with no trailing slash would now be re-transformed. I checked DecentralandUrl and SUPPORTED_SUBDOMAINS_OF_NON_CLIENT_ORIGIN (only profile-images) — no such URL exists, so it is unreachable today; noting it since the guard is defence-in-depth.
  • LoopbackUrls, TrustedRealms, and the RefinedAdapterAddresses earliest-scheme fix all handle the look-alike cases correctly (127.0.0.1.example.com, 127.0.0.1@example.com, evildecentraland.zone, decentraland.zone.example.com), and the tests pin them. The McpHttpServer.IsAllowed consolidation onto LoopbackUrls.IsLoopbackWebUrl also tightens the old check, which accepted a bare unbracketed ::1.

DEPENDENCY_REVIEW: NEEDS_ATTENTION

Not a block: the pin is immutable, first-party, and proportionate. It needs a human because the security argument for the ICE change rests on behaviour inside client-sdk-unity@a4bd750, which I had no access to inspect, and because the flag's own documentation currently understates what it disables.
· branch chore/e2e-infra

@decentraland-bot

Copy link
Copy Markdown
Contributor

PR #9822, run #33516051543

Overall: ✅ no significant changes

Builds: Windows change, Windows baseline, macOS change, macOS baseline

How to read this table
  • Each build is measured 3 times, interleaved with the other build (change, baseline, change, baseline, ...) in the same session, so both see the same world content and machine state. The values are the median, and (min–max) is the lowest and highest of those runs.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a difference that passed every check: the runs are fully separated (every run of one build faster than every run of the other), and the median difference is at least 3% and at least 0.5 ms.
  • ⚪ within noise — the builds' runs overlap, or the difference is tiny; it cannot be told apart from random variation. Treat it as no change.
  • — informational — the 0.1% worst metrics average only the few worst frames of a run, so a single OS hiccup swings them by a lot; they are shown for context and never earn a verdict.
  • ⚠️ no verdict — the two builds' sessions were not comparable (very different sample counts, or too few usable runs), so no conclusion is drawn from them.
  • Exceptions per run — the average number of exceptions in a run's log, not counting teardown ones logged while the app quits. Flagged only on a difference of at least 2 per run and 1.5× the other build; exception kinds the baseline never threw are called out under the table. The Exception breakdown groups all of them by the explorer's report category and exception type (as totals across the runs).
  • A run that logged unusually many exceptions (at least 10 and 5× the median of its build's runs — e.g. a service was down during it) is excluded from all numbers and called out under the table.
  • The Overall line at the top only reacts to a metric that moved on two or more machines, or by 10% or more on one — a single modest 🟢/🔴 cell can still be a statistical fluke.

Intel Core i5

Metric Baseline Change Δ Result
Samples 2380 (×3) 2387 (×3)
CPU average 37.5 ms (36.3–37.8) 37.5 ms (36.8–37.7) 0.0 ms ⚪ within noise
CPU 1% worst 337.7 ms (302.4–339.0) 318.6 ms (293.7–345.1) -19.1 ms ⚪ within noise
CPU 0.1% worst 360.9 ms (310.5–404.2) 329.2 ms (310.7–418.6) -31.7 ms — informational
GPU average 23.3 ms (22.9–23.8) 22.9 ms (22.8–23.3) -0.4 ms ⚪ within noise
GPU 1% worst 327.7 ms (308.7–340.2) 321.6 ms (297.7–350.1) -6.2 ms ⚪ within noise
GPU 0.1% worst 368.5 ms (318.0–403.6) 331.8 ms (307.9–423.2) -36.8 ms — informational
Exceptions per run 0 0 0 ⚪ no significant change

Apple M1

Metric Baseline Change Δ Result
Samples 3134 (×3) 3147 (×3)
CPU average 28.5 ms (28.4–28.9) 28.4 ms (26.8–29.7) -0.0 ms ⚪ within noise
CPU 1% worst 239.5 ms (238.4–240.9) 234.4 ms (228.4–239.2) -5.2 ms ⚪ within noise
CPU 0.1% worst 245.9 ms (240.9–246.5) 244.9 ms (242.3–245.0) -1.1 ms — informational
GPU average 20.1 ms (20.0–20.1) 20.0 ms (18.3–22.5) -0.1 ms ⚪ within noise
GPU 1% worst 57.7 ms (56.0–60.7) 58.3 ms (54.1–58.4) 0.6 ms ⚪ within noise
GPU 0.1% worst 62.6 ms (62.6–65.5) 61.6 ms (56.9–63.4) -1.0 ms — informational
Exceptions per run 0 0 0 ⚪ no significant change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

force-build Used to trigger a build on draft PR new-dependency

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants