From c6dcee6466e5c4ad7e0182f5cf2e3015bfe9c965 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 24 Jul 2026 03:08:12 +0200 Subject: [PATCH 1/3] fix(core): resolve libp2p endpoint runtime constructor after Bedrock deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Libp2pEndpoint loads the isolated Libp2pEndpointRuntime through Libp2pRuntimeLoader.classLoader() and resolves its constructor reflectively by exact signature. PR #57 (trusted Bedrock identity sessions) added BedrockIdentityReadiness and BedrockAdmissionCoordinator to the runtime constructor (now 8/9-arg) but left the wrapper's getDeclaredConstructor(...) lookup at the old 7-arg signature. The lookup therefore matches no constructor and throws NoSuchMethodException, so the wrapper sets runtime=null and logs "Failed to initialize Connect libp2p endpoint runtime". The endpoint never starts and every join fails downstream with "No available Browser Hub". This drift is JDK-independent: it reproduces identically on Java 21 and Java 26 (and jvm-libp2p/kotlin classes load fine on Java 26), so it is not an upstream jvm-libp2p incompatibility despite surfacing when a user upgraded to Java 26. Wire both Bedrock dependencies through Libp2pEndpoint's @Inject constructor (both are already provided in the same injector — the co-located eager singleton WatcherRegister injects them today) and extend the reflective lookup and newInstance to the 9-arg constructor. Add Libp2pEndpointRuntimeInitTest, which constructs the wrapper and asserts the runtime initializes; it fails (runtime==null) with the pre-fix 7-arg lookup and passes after. Document the reflective boundary contract in AGENTS.md. --- AGENTS.md | 20 +++++ CLAUDE.md | 1 + .../connect/tunnel/p2p/Libp2pEndpoint.java | 14 +++- .../p2p/Libp2pEndpointRuntimeInitTest.java | 83 +++++++++++++++++++ 4 files changed, 115 insertions(+), 3 deletions(-) create mode 120000 CLAUDE.md create mode 100644 core/src/test/java/com/minekube/connect/tunnel/p2p/Libp2pEndpointRuntimeInitTest.java diff --git a/AGENTS.md b/AGENTS.md index c4cfef9d..e753367a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,26 @@ curl -I -L --fail https://github.com/minekube/connect-java/releases/download/ runtimeClass = Class.forName( @@ -68,7 +72,9 @@ public Libp2pEndpoint( PlatformUtils.class, ConnectLogger.class, PlatformInjector.class, - SimpleConnectApi.class); + SimpleConnectApi.class, + BedrockIdentityReadiness.class, + BedrockAdmissionCoordinator.class); constructor.setAccessible(true); this.runtime = constructor.newInstance( dataDirectory, @@ -77,7 +83,9 @@ public Libp2pEndpoint( platformUtils, logger, platformInjector, - api); + api, + bedrockIdentityReadiness, + admissionCoordinator); this.startMethod = runtimeClass.getDeclaredMethod("start"); this.startBootstrapMethod = runtimeClass.getDeclaredMethod( "start", List.class, List.class, boolean.class); diff --git a/core/src/test/java/com/minekube/connect/tunnel/p2p/Libp2pEndpointRuntimeInitTest.java b/core/src/test/java/com/minekube/connect/tunnel/p2p/Libp2pEndpointRuntimeInitTest.java new file mode 100644 index 00000000..a1936751 --- /dev/null +++ b/core/src/test/java/com/minekube/connect/tunnel/p2p/Libp2pEndpointRuntimeInitTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2021-2022 Minekube. https://minekube.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author Minekube + * @link https://github.com/minekube/connect-java + */ + +package com.minekube.connect.tunnel.p2p; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; + +import com.minekube.connect.api.logger.ConnectLogger; +import java.lang.reflect.Field; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Guards the reflective boundary between {@link Libp2pEndpoint} (parent class + * loader) and {@code Libp2pEndpointRuntime} (isolated child-first class loader). + * + *

The endpoint wrapper resolves the runtime constructor reflectively via + * {@code getDeclaredConstructor(...)}. If the wrapper's parameter list drifts + * from the runtime's actual constructor — as it did when PR #57 added the + * trusted-Bedrock-identity dependencies to the runtime but not to the wrapper — + * the lookup throws {@link NoSuchMethodException}, the wrapper silently sets its + * runtime to {@code null}, logs "Failed to initialize Connect libp2p endpoint + * runtime", and every player join later fails with "No available Browser Hub". + * + *

This drift is not tied to any JDK version: the reflective lookup either + * matches an existing constructor or it does not, identically on Java 11, 21 and + * 26. Constructing the wrapper and asserting the runtime is non-null keeps the + * two constructor signatures in lock-step. + */ +class Libp2pEndpointRuntimeInitTest { + + @Test + void initializesRuntimeAcrossIsolatedLoaderBoundary(@TempDir Path dataDirectory) throws Exception { + ConnectLogger logger = mock(ConnectLogger.class); + + // Only the dependency *types* drive the reflective constructor lookup, so + // null values for the isolated runtime's collaborators are sufficient to + // exercise resolution + construction across the class-loader boundary. + Libp2pEndpoint endpoint = new Libp2pEndpoint( + dataDirectory, + null, // ConnectConfig + "connect-token", + null, // PlatformUtils + logger, + null, // PlatformInjector + null, // SimpleConnectApi + null, // BedrockIdentityReadiness + null); // BedrockAdmissionCoordinator + + Field runtimeField = Libp2pEndpoint.class.getDeclaredField("runtime"); + runtimeField.setAccessible(true); + Object runtime = runtimeField.get(endpoint); + + assertNotNull(runtime, + "Libp2pEndpoint must resolve and construct the isolated Libp2pEndpointRuntime; " + + "a null runtime means the reflective constructor lookup failed " + + "(NoSuchMethodException) and the libp2p endpoint will never start."); + } +} From 3c788a418c14615b4d65427e69f3538920cd2959 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 24 Jul 2026 10:24:03 +0200 Subject: [PATCH 2/3] no-mistakes(review): Strengthen libp2p coordinator wiring regression test --- .../p2p/Libp2pEndpointRuntimeInitTest.java | 53 ++++++++++++------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/core/src/test/java/com/minekube/connect/tunnel/p2p/Libp2pEndpointRuntimeInitTest.java b/core/src/test/java/com/minekube/connect/tunnel/p2p/Libp2pEndpointRuntimeInitTest.java index a1936751..64c8f310 100644 --- a/core/src/test/java/com/minekube/connect/tunnel/p2p/Libp2pEndpointRuntimeInitTest.java +++ b/core/src/test/java/com/minekube/connect/tunnel/p2p/Libp2pEndpointRuntimeInitTest.java @@ -26,9 +26,12 @@ package com.minekube.connect.tunnel.p2p; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.mockito.Mockito.mock; import com.minekube.connect.api.logger.ConnectLogger; +import com.minekube.connect.bedrock.BedrockAdmissionCoordinator; +import com.minekube.connect.bedrock.VerifiedBedrockIdentityRegistry; import java.lang.reflect.Field; import java.nio.file.Path; import org.junit.jupiter.api.Test; @@ -56,28 +59,38 @@ class Libp2pEndpointRuntimeInitTest { @Test void initializesRuntimeAcrossIsolatedLoaderBoundary(@TempDir Path dataDirectory) throws Exception { ConnectLogger logger = mock(ConnectLogger.class); + BedrockAdmissionCoordinator admissionCoordinator = new BedrockAdmissionCoordinator( + new VerifiedBedrockIdentityRegistry()); - // Only the dependency *types* drive the reflective constructor lookup, so - // null values for the isolated runtime's collaborators are sufficient to - // exercise resolution + construction across the class-loader boundary. - Libp2pEndpoint endpoint = new Libp2pEndpoint( - dataDirectory, - null, // ConnectConfig - "connect-token", - null, // PlatformUtils - logger, - null, // PlatformInjector - null, // SimpleConnectApi - null, // BedrockIdentityReadiness - null); // BedrockAdmissionCoordinator + try { + Libp2pEndpoint endpoint = new Libp2pEndpoint( + dataDirectory, + null, // ConnectConfig + "connect-token", + null, // PlatformUtils + logger, + null, // PlatformInjector + null, // SimpleConnectApi + null, // BedrockIdentityReadiness + admissionCoordinator); - Field runtimeField = Libp2pEndpoint.class.getDeclaredField("runtime"); - runtimeField.setAccessible(true); - Object runtime = runtimeField.get(endpoint); + Field runtimeField = Libp2pEndpoint.class.getDeclaredField("runtime"); + runtimeField.setAccessible(true); + Object runtime = runtimeField.get(endpoint); - assertNotNull(runtime, - "Libp2pEndpoint must resolve and construct the isolated Libp2pEndpointRuntime; " - + "a null runtime means the reflective constructor lookup failed " - + "(NoSuchMethodException) and the libp2p endpoint will never start."); + assertNotNull(runtime, + "Libp2pEndpoint must resolve and construct the isolated Libp2pEndpointRuntime; " + + "a null runtime means the reflective constructor lookup failed " + + "(NoSuchMethodException) and the libp2p endpoint will never start."); + + Field admissionCoordinatorField = runtime.getClass().getDeclaredField("admissionCoordinator"); + admissionCoordinatorField.setAccessible(true); + Object runtimeAdmissionCoordinator = admissionCoordinatorField.get(runtime); + + assertNotNull(runtimeAdmissionCoordinator); + assertSame(admissionCoordinator, runtimeAdmissionCoordinator); + } finally { + admissionCoordinator.close(); + } } } From 002c8db3ebcbb1e7c21ca486451f54f1772ef2b6 Mon Sep 17 00:00:00 2001 From: Robin Date: Fri, 24 Jul 2026 10:38:44 +0200 Subject: [PATCH 3/3] no-mistakes(document): Consolidated boundary guidance and linted regression test --- AGENTS.md | 14 ++++++------- .../p2p/Libp2pEndpointRuntimeInitTest.java | 21 +++++++------------ 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e753367a..d90e16df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,19 +51,17 @@ curl -I -L --fail https://github.com/minekube/connect-java/releases/download/The endpoint wrapper resolves the runtime constructor reflectively via - * {@code getDeclaredConstructor(...)}. If the wrapper's parameter list drifts - * from the runtime's actual constructor — as it did when PR #57 added the - * trusted-Bedrock-identity dependencies to the runtime but not to the wrapper — - * the lookup throws {@link NoSuchMethodException}, the wrapper silently sets its - * runtime to {@code null}, logs "Failed to initialize Connect libp2p endpoint - * runtime", and every player join later fails with "No available Browser Hub". - * - *

This drift is not tied to any JDK version: the reflective lookup either - * matches an existing constructor or it does not, identically on Java 11, 21 and - * 26. Constructing the wrapper and asserting the runtime is non-null keeps the - * two constructor signatures in lock-step. + *

The wrapper's constructor parameter list must match the runtime constructor + * exactly. Supplying a real admission coordinator also verifies that the + * injected dependency crosses the boundary unchanged. */ class Libp2pEndpointRuntimeInitTest { @Test - void initializesRuntimeAcrossIsolatedLoaderBoundary(@TempDir Path dataDirectory) throws Exception { + void initializesRuntimeAcrossIsolatedLoaderBoundary( + @TempDir Path dataDirectory) throws Exception { ConnectLogger logger = mock(ConnectLogger.class); BedrockAdmissionCoordinator admissionCoordinator = new BedrockAdmissionCoordinator( new VerifiedBedrockIdentityRegistry()); @@ -83,7 +75,8 @@ void initializesRuntimeAcrossIsolatedLoaderBoundary(@TempDir Path dataDirectory) + "a null runtime means the reflective constructor lookup failed " + "(NoSuchMethodException) and the libp2p endpoint will never start."); - Field admissionCoordinatorField = runtime.getClass().getDeclaredField("admissionCoordinator"); + Field admissionCoordinatorField = runtime.getClass() + .getDeclaredField("admissionCoordinator"); admissionCoordinatorField.setAccessible(true); Object runtimeAdmissionCoordinator = admissionCoordinatorField.get(runtime);