You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/09/02 09:59:31 UTC

(maven) branch backport/12998-to-maven-4.0.x created (now 22ebcd49f5)

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a change to branch backport/12998-to-maven-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git


      at 22ebcd49f5 [IMPROVEMENT] Improve standalone ApiRunner to properly apply settings

This branch includes the following new commits:

     new 22ebcd49f5 [IMPROVEMENT] Improve standalone ApiRunner to properly apply settings

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(maven) 01/01: [IMPROVEMENT] Improve standalone ApiRunner to properly apply settings

Posted by gn...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch backport/12998-to-maven-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git

commit 22ebcd49f5baaae6f76838a585d8ab9120d55971
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Wed Sep 2 11:53:15 2026 +0200

    [IMPROVEMENT] Improve standalone ApiRunner to properly apply settings
    
    The standalone ApiRunner is the main entry point for using the Maven 4 API
    outside of a full Maven build (e.g. in tools like mvnup). This improves it
    to be production-ready:
    
    - Remove hardcoded user.home = "target" — read settings from real user home
    - Store effective settings on the session (session.getSettings() returns actual settings)
    - Apply proxy, mirror, and server authentication from settings.xml
    - Configure dependency resolution machinery (DependencyTraverser, DependencyManager, etc.)
    - Support encrypted settings passwords with SecurityMode enum (NONE, IF_AVAILABLE, IF_AVAILABLE_WARN, REQUIRED)
    - Load maven-system.properties and maven-user.properties
    - Detect and set Maven version from classpath pom.properties
    - Honor offline mode from settings
    - Use ApiRunner-provided repositories in mvnup instead of replacing them
---
 .../mvnup/goals/AbstractUpgradeStrategy.java       |  17 +-
 .../impl/resolver/MavenSessionBuilderSupplier.java |  10 +-
 .../apache/maven/impl/standalone/ApiRunner.java    | 425 ++++++++++++++++++---
 .../impl/standalone/ApiRunnerSettingsTest.java     | 289 ++++++++++++++
 4 files changed, 673 insertions(+), 68 deletions(-)

diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java
index 50e5777de2..4c2f2d65ea 100644
--- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java
+++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/AbstractUpgradeStrategy.java
@@ -46,8 +46,6 @@
 import org.apache.maven.api.settings.Settings;
 import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
 import org.apache.maven.impl.standalone.ApiRunner;
-import org.codehaus.plexus.components.secdispatcher.Dispatcher;
-import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher;
 import org.eclipse.aether.spi.connector.transport.TransporterFactory;
 import org.eclipse.aether.spi.connector.transport.http.ChecksumExtractor;
 import org.eclipse.aether.spi.io.PathProcessor;
@@ -257,18 +255,9 @@ protected static String remoteResolutionUnsupportedReason(UpgradeContext context
     }
 
     private Session createMaven4Session(UpgradeContext context) {
-        // Reuse the operator's real user home (settings.xml) and local repository so remote
-        // resolution follows the configured repository posture instead of the standalone
-        // test defaults. When settings were never loaded (unit tests, embedding), keep the
-        // isolated test user home.
-        boolean settingsLoaded = context.effectiveSettings != null;
-        Session session = ApiRunner.createSession(
-                injector -> {
-                    injector.bindInstance(Dispatcher.class, new LegacyDispatcher());
-                    injector.bindImplicit(TransporterFactoryConfig.class);
-                },
-                context.localRepositoryPath,
-                !settingsLoaded);
+        Session session = ApiRunner.createSession(injector -> {
+            injector.bindImplicit(TransporterFactoryConfig.class);
+        });
 
         if (remoteResolutionUnsupportedReason(context) != null) {
             // No remote repositories at all, rather than resolving from hardcoded public
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java
index 3145953644..3f97b754e9 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/resolver/MavenSessionBuilderSupplier.java
@@ -78,7 +78,7 @@ public MavenSessionBuilderSupplier(RepositorySystem repositorySystem, boolean ma
                         : Maven4ScopeManagerConfiguration.INSTANCE);
     }
 
-    protected DependencyTraverser getDependencyTraverser() {
+    public DependencyTraverser getDependencyTraverser() {
         return new FatArtifactTraverser();
     }
 
@@ -97,7 +97,7 @@ public DependencyManager getDependencyManager(boolean transitive) {
         return new ClassicDependencyManager(getScopeManager());
     }
 
-    protected DependencySelector getDependencySelector() {
+    public DependencySelector getDependencySelector() {
         return new AndDependencySelector(
                 ScopeDependencySelector.legacy(
                         null, Arrays.asList(DependencyScope.TEST.id(), DependencyScope.PROVIDED.id())),
@@ -105,7 +105,7 @@ protected DependencySelector getDependencySelector() {
                 new ExclusionDependencySelector());
     }
 
-    protected DependencyGraphTransformer getDependencyGraphTransformer() {
+    public DependencyGraphTransformer getDependencyGraphTransformer() {
         return new ChainedDependencyGraphTransformer(
                 new ConflictResolver(
                         new ConfigurableVersionSelector(), new ManagedScopeSelector(getScopeManager()),
@@ -122,13 +122,13 @@ protected DependencyGraphTransformer getDependencyGraphTransformer() {
      * <p>
      * Important: this "static" list of types should be in-sync with core provided types.
      */
-    protected ArtifactTypeRegistry getArtifactTypeRegistry() {
+    public ArtifactTypeRegistry getArtifactTypeRegistry() {
         DefaultArtifactTypeRegistry stereotypes = new DefaultArtifactTypeRegistry();
         new DefaultTypeProvider().types().forEach(stereotypes::add);
         return stereotypes;
     }
 
-    protected ArtifactDescriptorPolicy getArtifactDescriptorPolicy() {
+    public ArtifactDescriptorPolicy getArtifactDescriptorPolicy() {
         return new SimpleArtifactDescriptorPolicy(true, true);
     }
 
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java
index 3421eadf1b..b64dafa869 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/standalone/ApiRunner.java
@@ -18,6 +18,9 @@
  */
 package org.apache.maven.impl.standalone;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.time.Instant;
@@ -29,11 +32,14 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
 import org.apache.maven.api.Artifact;
+import org.apache.maven.api.Constants;
 import org.apache.maven.api.Lifecycle;
 import org.apache.maven.api.MonotonicClock;
 import org.apache.maven.api.Packaging;
@@ -45,6 +51,7 @@
 import org.apache.maven.api.Version;
 import org.apache.maven.api.annotations.Nonnull;
 import org.apache.maven.api.annotations.Nullable;
+import org.apache.maven.api.di.Named;
 import org.apache.maven.api.di.Provides;
 import org.apache.maven.api.di.SessionScoped;
 import org.apache.maven.api.model.PluginContainer;
@@ -57,32 +64,69 @@
 import org.apache.maven.api.services.RepositoryFactory;
 import org.apache.maven.api.services.SettingsBuilder;
 import org.apache.maven.api.services.TypeRegistry;
+import org.apache.maven.api.services.VersionParser;
+import org.apache.maven.api.settings.Mirror;
+import org.apache.maven.api.settings.Proxy;
+import org.apache.maven.api.settings.Server;
 import org.apache.maven.api.settings.Settings;
 import org.apache.maven.api.spi.TypeProvider;
 import org.apache.maven.api.toolchain.ToolchainModel;
 import org.apache.maven.di.Injector;
 import org.apache.maven.di.Key;
 import org.apache.maven.di.impl.DIException;
+import org.apache.maven.di.impl.InjectorImpl;
 import org.apache.maven.impl.AbstractSession;
 import org.apache.maven.impl.InternalSession;
 import org.apache.maven.impl.di.SessionScope;
+import org.apache.maven.impl.model.DefaultInterpolator;
+import org.apache.maven.impl.resolver.MavenSessionBuilderSupplier;
 import org.apache.maven.impl.resolver.scopes.Maven4ScopeManagerConfiguration;
+import org.codehaus.plexus.components.secdispatcher.Cipher;
+import org.codehaus.plexus.components.secdispatcher.Dispatcher;
+import org.codehaus.plexus.components.secdispatcher.MasterSource;
+import org.codehaus.plexus.components.secdispatcher.internal.cipher.AESGCMNoPadding;
+import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher;
+import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.MasterDispatcher;
+import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterSource;
+import org.codehaus.plexus.components.secdispatcher.internal.sources.GpgAgentMasterSource;
+import org.codehaus.plexus.components.secdispatcher.internal.sources.PinEntryMasterSource;
+import org.codehaus.plexus.components.secdispatcher.internal.sources.SystemPropertyMasterSource;
 import org.eclipse.aether.DefaultRepositorySystemSession;
 import org.eclipse.aether.RepositorySystem;
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.internal.impl.scope.ScopeManagerImpl;
 import org.eclipse.aether.repository.LocalRepository;
 import org.eclipse.aether.repository.LocalRepositoryManager;
+import org.eclipse.aether.util.repository.AuthenticationBuilder;
+import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
+import org.eclipse.aether.util.repository.DefaultMirrorSelector;
+import org.eclipse.aether.util.repository.DefaultProxySelector;
 
 /**
  * Provides functionality for running Maven API in a standalone mode.
  * <p>
  * This class serves as the main entry point for executing Maven operations outside
  * of the standard Maven build environment. It provides methods for creating and
- * managing Maven sessions in a simplified context, primarily for testing and
- * specialized execution scenarios.
+ * managing Maven sessions in a simplified context, suitable for tools, IDE integrations,
+ * and specialized execution scenarios.
  * </p>
  *
+ * <p>The standalone session reads and applies the user's {@code settings.xml} including:</p>
+ * <ul>
+ *   <li>Local repository location</li>
+ *   <li>Server authentication (username, password, private key)</li>
+ *   <li>Proxy configuration</li>
+ *   <li>Mirror configuration</li>
+ *   <li>Repository definitions from active profiles</li>
+ *   <li>Offline mode</li>
+ * </ul>
+ *
+ * <p>It also loads {@code maven-system.properties} from the Maven configuration directory
+ * ({@code ${maven.home}/conf} or as configured via {@code maven.installation.conf}/{@code maven.conf})
+ * and from the user's Maven home ({@code ~/.m2/}), merging both into the session's system properties.
+ * User properties are loaded from {@code ~/.m2/maven-user.properties} and exposed via
+ * {@link Session#getUserProperties()}.</p>
+ *
  * <p>Example usage:</p>
  * <pre>
  * Session session = ApiRunner.createSession();
@@ -99,6 +143,27 @@
  */
 public class ApiRunner {
 
+    /**
+     * Controls how the standalone session handles settings encryption/decryption.
+     *
+     * <p>Maven settings ({@code settings.xml}) may contain encrypted server passwords.
+     * Decryption requires the {@code plexus-sec-dispatcher} library on the classpath.
+     * This enum lets callers control the behavior when dispatchers are (or are not) available.</p>
+     */
+    public enum SecurityMode {
+        /** Do not attempt to configure security dispatchers. Encrypted passwords are passed through as-is. */
+        NONE,
+        /** Try to configure security dispatchers; silently skip if the required classes are not on the classpath. */
+        IF_AVAILABLE,
+        /**
+         * Try to configure security dispatchers; warn to {@code System.err} if the required classes
+         * are not on the classpath. This is the default.
+         */
+        IF_AVAILABLE_WARN,
+        /** Security dispatchers are required; throw {@link MavenException} if they cannot be configured. */
+        REQUIRED
+    }
+
     /**
      * Creates a new Maven session with default configuration.
      *
@@ -126,29 +191,27 @@ public static Session createSession(Consumer<Injector> injectorConsumer) {
      * @return a new {@link Session} instance
      */
     public static Session createSession(Consumer<Injector> injectorConsumer, Path localRepo) {
-        return createSession(injectorConsumer, localRepo, true);
+        return createSession(injectorConsumer, localRepo, SecurityMode.IF_AVAILABLE_WARN);
     }
 
     /**
-     * Creates a new Maven session with custom injector configuration, local repository path
-     * and control over {@code user.home} isolation.
+     * Creates a new Maven session with custom injector configuration, local repository path,
+     * and security mode.
      *
      * @param injectorConsumer consumer function to customize the injector
      * @param localRepo path to the local repository
-     * @param isolateUserHome when {@code true}, the session remaps {@code user.home} to the
-     *        {@code target} directory so the invoking user's {@code settings.xml} and local
-     *        repository cannot interfere with tests; callers that want the operator's real
-     *        configuration honored (such as the mvnup tool) must pass {@code false}
+     * @param securityMode controls how encrypted passwords in settings are handled
      * @return a new {@link Session} instance
      */
-    public static Session createSession(Consumer<Injector> injectorConsumer, Path localRepo, boolean isolateUserHome) {
+    public static Session createSession(
+            Consumer<Injector> injectorConsumer, Path localRepo, SecurityMode securityMode) {
         Injector injector = Injector.create();
         injector.bindInstance(Injector.class, injector);
         injector.bindImplicit(ApiRunner.class);
         injector.bindImplicit(RepositorySystemSupplier.class);
         injector.bindInstance(LocalRepoProvider.class, () -> localRepo);
-        injector.bindInstance(UserHomeIsolation.class, () -> isolateUserHome);
         injector.discover(ApiRunner.class.getClassLoader());
+        configureSecurityDispatchers(injector, securityMode != null ? securityMode : SecurityMode.IF_AVAILABLE_WARN);
         if (injectorConsumer != null) {
             injectorConsumer.accept(injector);
         }
@@ -161,14 +224,45 @@ public static Session createSession(Consumer<Injector> injectorConsumer, Path lo
     }
 
     /**
-     * Controls whether the standalone session isolates {@code user.home} from the invoking
-     * user's environment.
+     * Attempts to bind security dispatcher classes for settings password decryption.
+     * The dispatchers (legacy and master) are loaded from the {@code plexus-sec-dispatcher}
+     * library. If the library is not on the classpath, the behavior depends on the
+     * {@link SecurityMode}.
+     *
+     * <p>If dispatchers are already bound (e.g., via {@code discover()} in tests or by a
+     * custom {@code injectorConsumer}), this method is a no-op to avoid duplicate bindings.</p>
      */
-    interface UserHomeIsolation {
-        /**
-         * @return {@code true} to remap {@code user.home} away from the real user home
-         */
-        boolean isolated();
+    private static void configureSecurityDispatchers(Injector injector, SecurityMode mode) {
+        if (mode == SecurityMode.NONE) {
+            return;
+        }
+        try {
+            Class.forName("org.codehaus.plexus.components.secdispatcher.Dispatcher");
+            // Skip if dispatchers are already bound (e.g., from discover() scanning test classes)
+            if (!hasExistingDispatchers(injector)) {
+                injector.bindImplicit(SecDispatcherBindings.class);
+            }
+        } catch (ClassNotFoundException | NoClassDefFoundError e) {
+            switch (mode) {
+                case REQUIRED:
+                    throw new MavenException(
+                            "Security dispatchers required but plexus-sec-dispatcher is not on the classpath", e);
+                case IF_AVAILABLE_WARN:
+                    System.err.println("WARNING: plexus-sec-dispatcher not available on classpath; "
+                            + "encrypted passwords in settings.xml will not be decrypted");
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+
+    private static boolean hasExistingDispatchers(Injector injector) {
+        if (injector instanceof InjectorImpl impl) {
+            Set<?> bindings = impl.getAllBindings(Dispatcher.class);
+            return bindings != null && !bindings.isEmpty();
+        }
+        return false;
     }
 
     /**
@@ -190,6 +284,9 @@ static class DefaultSession extends AbstractSession {
 
         private final Map<String, String> systemProperties;
         private final Instant startTime = MonotonicClock.now();
+        private Settings settings;
+        private Version mavenVersion;
+        private Map<String, String> userProperties = Map.of();
 
         DefaultSession(RepositorySystemSession session, RepositorySystem repositorySystem, Lookup lookup) {
             this(session, repositorySystem, Collections.emptyList(), null, lookup);
@@ -209,12 +306,29 @@ protected DefaultSession(
 
         @Override
         protected Session newSession(RepositorySystemSession session, List<RemoteRepository> repositories) {
-            return new DefaultSession(session, repositorySystem, repositories, null, lookup);
+            DefaultSession newSession = new DefaultSession(session, repositorySystem, repositories, null, lookup);
+            newSession.settings = this.settings;
+            newSession.mavenVersion = this.mavenVersion;
+            newSession.userProperties = this.userProperties;
+            return newSession;
+        }
+
+        void setSettings(Settings settings) {
+            this.settings = settings;
+        }
+
+        void setMavenVersion(Version mavenVersion) {
+            this.mavenVersion = mavenVersion;
+        }
+
+        void setUserProperties(Map<String, String> userProperties) {
+            this.userProperties = userProperties != null ? userProperties : Map.of();
         }
 
         @Override
+        @Nonnull
         public Settings getSettings() {
-            return Settings.newInstance();
+            return settings != null ? settings : Settings.newInstance();
         }
 
         @Override
@@ -225,7 +339,7 @@ public Collection<ToolchainModel> getToolchains() {
 
         @Override
         public Map<String, String> getUserProperties() {
-            return Map.of();
+            return userProperties;
         }
 
         @Override
@@ -245,7 +359,7 @@ public Map<String, String> getEffectiveProperties(Project project) {
 
         @Override
         public Version getMavenVersion() {
-            return null;
+            return mavenVersion;
         }
 
         @Override
@@ -393,33 +507,50 @@ public List<String> computePhases(Lifecycle lifecycle) {
 
     @Provides
     @SuppressWarnings("unused")
-    static Session newSession(
-            RepositorySystem system,
-            Lookup lookup,
-            @Nullable LocalRepoProvider localRepoProvider,
-            @Nullable UserHomeIsolation userHomeIsolation) {
+    static Session newSession(RepositorySystem system, Lookup lookup, @Nullable LocalRepoProvider localRepoProvider) {
         Map<String, String> properties = new HashMap<>();
         // Env variables prefixed with "env."
         System.getenv().forEach((k, v) -> properties.put("env." + k, v));
         // Java System properties
         System.getProperties().forEach((k, v) -> properties.put(k.toString(), v.toString()));
 
-        // Test isolation shim: do not let the invoking user's settings interfere with unit
-        // tests. Callers that want the operator's real settings.xml and local repository
-        // honored must create the session with isolateUserHome=false (see createSession).
-        if (userHomeIsolation == null || userHomeIsolation.isolated()) {
-            properties.put("user.home", "target");
-        }
-
         Path userHome = Paths.get(properties.get("user.home"));
         Path mavenUserHome = userHome.resolve(".m2");
         Path mavenSystemHome = properties.containsKey("maven.home")
                 ? Paths.get(properties.get("maven.home"))
                 : properties.containsKey("env.MAVEN_HOME") ? Paths.get(properties.get("env.MAVEN_HOME")) : null;
 
+        // Load maven-system.properties: installation-level first (sets paths like maven.user.conf),
+        // then user-level (~/.m2/) for custom overrides.  The installation-level file is a bootstrapper
+        // that in the CLI includes user and project-level files via ${includes}; since we don't support
+        // that directive, we load the user-level file explicitly.
+        Path mavenConf = resolveMavenConf(properties);
+        if (mavenConf != null) {
+            Map<String, String> systemFileProps =
+                    loadMavenProperties(mavenConf.resolve("maven-system.properties"), properties);
+            properties.putAll(systemFileProps);
+        }
+        properties.putAll(loadMavenProperties(mavenUserHome.resolve("maven-system.properties"), properties));
+
+        // Load maven-user.properties from the user-level location (~/.m2/) only.
+        // The installation-level maven-user.properties (in ${maven.conf}/) contains Maven-internal
+        // configuration (cache config, conflict resolver) that is not appropriate for standalone use;
+        // it is a CLI bootstrapper that includes user and project-level files via ${includes}.
+        Map<String, String> userProperties = new HashMap<>();
+        userProperties.putAll(loadMavenProperties(mavenUserHome.resolve("maven-user.properties"), properties));
+
+        // Configure the resolver session with dependency resolution machinery
+        MavenSessionBuilderSupplier sessionBuilderSupplier = new MavenSessionBuilderSupplier(system, false);
         DefaultRepositorySystemSession rsession = new DefaultRepositorySystemSession(h -> false);
         rsession.setScopeManager(new ScopeManagerImpl(Maven4ScopeManagerConfiguration.INSTANCE));
+        rsession.setDependencyTraverser(sessionBuilderSupplier.getDependencyTraverser());
+        rsession.setDependencyManager(sessionBuilderSupplier.getDependencyManager(true));
+        rsession.setDependencySelector(sessionBuilderSupplier.getDependencySelector());
+        rsession.setDependencyGraphTransformer(sessionBuilderSupplier.getDependencyGraphTransformer());
+        rsession.setArtifactTypeRegistry(sessionBuilderSupplier.getArtifactTypeRegistry());
+        rsession.setArtifactDescriptorPolicy(sessionBuilderSupplier.getArtifactDescriptorPolicy());
         rsession.setSystemProperties(properties);
+        rsession.setUserProperties(userProperties);
         rsession.setConfigProperties(properties);
 
         DefaultSession session = new DefaultSession(
@@ -437,6 +568,13 @@ static Session newSession(
                         mavenUserHome.resolve("settings.xml"))
                 .getEffectiveSettings();
 
+        // Store the effective settings and user properties on the session
+        session.setSettings(settings);
+        session.setUserProperties(userProperties);
+
+        // Set the Maven version
+        session.setMavenVersion(detectMavenVersion(lookup));
+
         // local repository
         String localRepository = settings.getLocalRepository() != null
                         && !settings.getLocalRepository().isEmpty()
@@ -446,15 +584,56 @@ static Session newSession(
                         : mavenUserHome.resolve("repository").toString();
         LocalRepositoryManager llm = system.newLocalRepositoryManager(rsession, new LocalRepository(localRepository));
         rsession.setLocalRepositoryManager(llm);
-        // active proxies
-        // TODO
-        // active profiles
-
-        Profile profile = session.getService(SettingsBuilder.class)
-                .convert(org.apache.maven.api.settings.Profile.newBuilder()
-                        .repositories(settings.getRepositories())
-                        .pluginRepositories(settings.getPluginRepositories())
-                        .build());
+
+        // Apply offline mode from settings
+        if (settings.isOffline()) {
+            rsession.setOffline(true);
+        }
+
+        // Apply proxy configuration from settings
+        DefaultProxySelector proxySelector = new DefaultProxySelector();
+        for (Proxy proxy : settings.getProxies()) {
+            if (proxy.isActive()) {
+                AuthenticationBuilder authBuilder = new AuthenticationBuilder();
+                authBuilder.addUsername(proxy.getUsername()).addPassword(proxy.getPassword());
+                proxySelector.add(
+                        new org.eclipse.aether.repository.Proxy(
+                                proxy.getProtocol(), proxy.getHost(), proxy.getPort(), authBuilder.build()),
+                        proxy.getNonProxyHosts());
+            }
+        }
+        rsession.setProxySelector(proxySelector);
+
+        // Apply mirror configuration from settings
+        DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector();
+        for (Mirror mirror : settings.getMirrors()) {
+            mirrorSelector.add(
+                    mirror.getId(),
+                    mirror.getUrl(),
+                    mirror.getLayout(),
+                    false,
+                    mirror.isBlocked(),
+                    mirror.getMirrorOf(),
+                    mirror.getMirrorOfLayouts());
+        }
+        rsession.setMirrorSelector(mirrorSelector);
+
+        // Apply server authentication from settings
+        DefaultAuthenticationSelector authSelector = new DefaultAuthenticationSelector();
+        for (Server server : settings.getServers()) {
+            AuthenticationBuilder authBuilder = new AuthenticationBuilder();
+            authBuilder.addUsername(server.getUsername()).addPassword(server.getPassword());
+            authBuilder.addPrivateKey(server.getPrivateKey(), server.getPassphrase());
+            authSelector.add(server.getId(), authBuilder.build());
+        }
+        rsession.setAuthenticationSelector(authSelector);
+
+        // Build repositories from active profiles in settings
+        SettingsBuilder settingsBuilder = session.getService(SettingsBuilder.class);
+        Profile profile = settingsBuilder.convert(org.apache.maven.api.settings.Profile.newBuilder()
+                .repositories(settings.getRepositories())
+                .pluginRepositories(settings.getPluginRepositories())
+                .build());
         RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class);
         List<RemoteRepository> repositories = profile.getRepositories().stream()
                 .map(repositoryFactory::createRemote)
@@ -462,16 +641,164 @@ static Session newSession(
         InternalSession s = (InternalSession) session.withRemoteRepositories(repositories);
         InternalSession.associate(rsession, s);
         return s;
+    }
 
-        // List<RemoteRepository> repositories = repositoryFactory.createRemote();
+    /**
+     * Resolves the Maven configuration directory following the same lookup order as the Maven CLI:
+     * {@code maven.installation.conf} → {@code maven.conf} → {@code ${maven.home}/conf} → {@code ${MAVEN_HOME}/conf}.
+     *
+     * @param properties the system properties (env + Java system properties)
+     * @return the maven conf directory path, or {@code null} if it cannot be determined
+     */
+    private static Path resolveMavenConf(Map<String, String> properties) {
+        String installConf = properties.get(Constants.MAVEN_INSTALLATION_CONF);
+        if (installConf != null) {
+            return Paths.get(installConf);
+        }
+        String mavenConf = properties.get("maven.conf");
+        if (mavenConf != null) {
+            return Paths.get(mavenConf);
+        }
+        String mavenHome = properties.get(Constants.MAVEN_HOME);
+        if (mavenHome != null) {
+            return Paths.get(mavenHome).resolve("conf");
+        }
+        String envMavenHome = properties.get("env.MAVEN_HOME");
+        if (envMavenHome != null) {
+            return Paths.get(envMavenHome).resolve("conf");
+        }
+        return null;
+    }
+
+    /**
+     * Loads properties from a file and interpolates {@code ${...}} references against the given
+     * fallback properties. If the file does not exist or cannot be read, an empty map is returned.
+     *
+     * @param path the properties file to load
+     * @param fallback fallback values for interpolation (typically system properties)
+     * @return the loaded and interpolated properties
+     */
+    private static Map<String, String> loadMavenProperties(Path path, Map<String, String> fallback) {
+        if (path == null || !Files.exists(path)) {
+            return new HashMap<>();
+        }
+        Properties fileProps = new Properties();
+        try (InputStream is = Files.newInputStream(path)) {
+            fileProps.load(is);
+        } catch (IOException e) {
+            // Silently ignore: properties files are optional configuration and the standalone
+            // API has no logger.  Failing to read a file (permissions, concurrent deletion)
+            // should not prevent session creation — callers get the same behavior as if the
+            // file did not exist.
+            return new HashMap<>();
+        }
+        Map<String, String> result = new HashMap<>();
+        fileProps.forEach((k, v) -> result.put(k.toString(), v.toString()));
+        // Interpolate ${...} references against the loaded properties + fallback
+        for (Map.Entry<String, String> entry : result.entrySet()) {
+            String value = entry.getValue();
+            if (value != null && value.contains("${")) {
+                entry.setValue(
+                        DefaultInterpolator.substVars(value, entry.getKey(), null, result, fallback::get, null, false));
+            }
+        }
+        return result;
+    }
 
-        //        session.getService(SettingsBuilder.class).convert()
+    /**
+     * Detects the Maven version by reading the pom.properties resource from the classpath.
+     * Falls back to reading from maven-impl's own pom.properties if maven-core is not available.
+     * If no version can be determined, returns {@code 0.0.0} as a sentinel value so that
+     * {@link Session#getMavenVersion()} is never null.
+     *
+     * @param lookup the lookup service
+     * @return the detected Maven version, never {@code null}
+     */
+    @Nonnull
+    private static Version detectMavenVersion(Lookup lookup) {
+        String version = loadVersionFromProperties("META-INF/maven/org.apache.maven/maven-core/pom.properties");
+        if (version == null) {
+            version = loadVersionFromProperties("META-INF/maven/org.apache.maven/maven-impl/pom.properties");
+        }
+        if (version == null) {
+            version = "0.0.0";
+        }
+        try {
+            return lookup.lookup(VersionParser.class).parseVersion(version);
+        } catch (Exception e) {
+            // Should not happen with "0.0.0", but be safe
+            return lookup.lookup(VersionParser.class).parseVersion("0.0.0");
+        }
+    }
 
-        //        settings.getDelegate().getRepositories().stream()
-        //                        .map(r -> SettingsUtilsV4.)
-        //        defaultSession.getService(RepositoryFactory.class).createRemote()
-        //        return defaultSession;
+    private static String loadVersionFromProperties(String resource) {
+        try (InputStream is = ApiRunner.class.getResourceAsStream("/" + resource)) {
+            if (is != null) {
+                Properties props = new Properties();
+                props.load(is);
+                String version = props.getProperty("version", "").trim();
+                if (!version.isEmpty() && !version.startsWith("${")) {
+                    return version;
+                }
+            }
+        } catch (IOException e) {
+            // ignore
+        }
+        return null;
     }
 
     record DumbPackaging(String id, Type type, Map<String, PluginContainer> plugins) implements Packaging {}
+
+    /**
+     * Provides the security dispatcher bindings needed for decrypting encrypted passwords
+     * in settings.xml. This class is only loaded when {@code plexus-sec-dispatcher} is confirmed
+     * to be on the classpath (see {@link #configureSecurityDispatchers}).
+     *
+     * <p>Supports both legacy ({@code {...}}) and Maven 4 master-key-based encrypted passwords.</p>
+     */
+    @SuppressWarnings("unused")
+    static class SecDispatcherBindings {
+
+        @Provides
+        @Named(LegacyDispatcher.NAME)
+        static Dispatcher legacyDispatcher() {
+            return new LegacyDispatcher();
+        }
+
+        @Provides
+        @Named(MasterDispatcher.NAME)
+        static Dispatcher masterDispatcher(Map<String, Cipher> ciphers, Map<String, MasterSource> sources) {
+            return new MasterDispatcher(ciphers, sources);
+        }
+
+        @Provides
+        @Named(AESGCMNoPadding.CIPHER_ALG)
+        static Cipher aesCipher() {
+            return new AESGCMNoPadding();
+        }
+
+        @Provides
+        @Named(EnvMasterSource.NAME)
+        static MasterSource envSource() {
+            return new EnvMasterSource();
+        }
+
+        @Provides
+        @Named(GpgAgentMasterSource.NAME)
+        static MasterSource gpgAgentSource() {
+            return new GpgAgentMasterSource();
+        }
+
+        @Provides
+        @Named(PinEntryMasterSource.NAME)
+        static MasterSource pinEntrySource() {
+            return new PinEntryMasterSource();
+        }
+
+        @Provides
+        @Named(SystemPropertyMasterSource.NAME)
+        static MasterSource systemPropertySource() {
+            return new SystemPropertyMasterSource();
+        }
+    }
 }
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/ApiRunnerSettingsTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/ApiRunnerSettingsTest.java
new file mode 100644
index 0000000000..4e4bf9f9aa
--- /dev/null
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/standalone/ApiRunnerSettingsTest.java
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.impl.standalone;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.maven.api.Session;
+import org.apache.maven.api.settings.Server;
+import org.apache.maven.api.settings.Settings;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link ApiRunner} settings handling and {@link ApiRunner.SecurityMode}.
+ */
+class ApiRunnerSettingsTest {
+
+    @TempDir
+    Path tempDir;
+
+    @Test
+    void testSecurityModeNone() {
+        Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+        assertNotNull(session);
+        assertNotNull(session.getSettings());
+    }
+
+    @Test
+    void testSecurityModeIfAvailable() {
+        // plexus-sec-dispatcher is on the test classpath, so dispatchers should be bound
+        Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.IF_AVAILABLE);
+        assertNotNull(session);
+        assertNotNull(session.getSettings());
+    }
+
+    @Test
+    void testSecurityModeIfAvailableWarn() {
+        // Default mode — same as IF_AVAILABLE when sec-dispatcher is on classpath
+        Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.IF_AVAILABLE_WARN);
+        assertNotNull(session);
+        assertNotNull(session.getSettings());
+    }
+
+    @Test
+    void testSecurityModeRequired() {
+        // plexus-sec-dispatcher is on the test classpath, so this should succeed
+        Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.REQUIRED);
+        assertNotNull(session);
+        assertNotNull(session.getSettings());
+    }
+
+    @Test
+    void testDefaultSecurityMode() {
+        // Default createSession() uses IF_AVAILABLE_WARN
+        Session session = ApiRunner.createSession();
+        assertNotNull(session);
+        assertNotNull(session.getSettings());
+    }
+
+    @Test
+    void testMavenVersionIsNeverNull() {
+        Session session = ApiRunner.createSession();
+        assertNotNull(session.getMavenVersion(), "getMavenVersion() should never return null");
+    }
+
+    @Test
+    void testSettingsServersApplied() throws IOException {
+        Path m2Dir = tempDir.resolve(".m2");
+        Files.createDirectories(m2Dir);
+        Files.writeString(m2Dir.resolve("settings.xml"), """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <settings xmlns="http://maven.apache.org/SETTINGS/1.2.0">
+                  <servers>
+                    <server>
+                      <id>my-repo</id>
+                      <username>myuser</username>
+                      <password>mypassword</password>
+                    </server>
+                  </servers>
+                </settings>
+                """);
+
+        String oldHome = System.getProperty("user.home");
+        try {
+            System.setProperty("user.home", tempDir.toString());
+            Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+            Settings settings = session.getSettings();
+            assertNotNull(settings);
+            assertEquals(1, settings.getServers().size());
+            Server server = settings.getServers().get(0);
+            assertEquals("my-repo", server.getId());
+            assertEquals("myuser", server.getUsername());
+            assertEquals("mypassword", server.getPassword());
+        } finally {
+            System.setProperty("user.home", oldHome);
+        }
+    }
+
+    @Test
+    void testSettingsMirrorsApplied() throws IOException {
+        Path m2Dir = tempDir.resolve(".m2");
+        Files.createDirectories(m2Dir);
+        Files.writeString(m2Dir.resolve("settings.xml"), """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <settings xmlns="http://maven.apache.org/SETTINGS/1.2.0">
+                  <mirrors>
+                    <mirror>
+                      <id>my-mirror</id>
+                      <url>https://mirror.example.com/maven2</url>
+                      <mirrorOf>central</mirrorOf>
+                    </mirror>
+                  </mirrors>
+                </settings>
+                """);
+
+        String oldHome = System.getProperty("user.home");
+        try {
+            System.setProperty("user.home", tempDir.toString());
+            Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+            Settings settings = session.getSettings();
+            assertNotNull(settings);
+            assertEquals(1, settings.getMirrors().size());
+            assertEquals("my-mirror", settings.getMirrors().get(0).getId());
+            assertEquals(
+                    "https://mirror.example.com/maven2",
+                    settings.getMirrors().get(0).getUrl());
+            assertEquals("central", settings.getMirrors().get(0).getMirrorOf());
+        } finally {
+            System.setProperty("user.home", oldHome);
+        }
+    }
+
+    @Test
+    void testSettingsRepositories() throws IOException {
+        Path m2Dir = tempDir.resolve(".m2");
+        Files.createDirectories(m2Dir);
+        // Maven 4 top-level repositories in settings
+        Files.writeString(m2Dir.resolve("settings.xml"), """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <settings xmlns="http://maven.apache.org/SETTINGS/2.0.0">
+                  <repositories>
+                    <repository>
+                      <id>custom-repo</id>
+                      <url>https://repo.example.com/maven2</url>
+                    </repository>
+                  </repositories>
+                </settings>
+                """);
+
+        String oldHome = System.getProperty("user.home");
+        try {
+            System.setProperty("user.home", tempDir.toString());
+            Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+            Settings settings = session.getSettings();
+            assertNotNull(settings);
+            assertTrue(
+                    settings.getRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())),
+                    "Effective settings should include top-level repository");
+            // Session should also expose them as remote repositories
+            assertTrue(
+                    session.getRemoteRepositories().stream().anyMatch(r -> "custom-repo".equals(r.getId())),
+                    "Session should include repository from settings");
+        } finally {
+            System.setProperty("user.home", oldHome);
+        }
+    }
+
+    @Test
+    void testLegacyEncryptedPasswordDecrypted() throws IOException {
+        Path m2Dir = tempDir.resolve(".m2");
+        Files.createDirectories(m2Dir);
+        // Legacy master password from the existing IT test resources (mng-8379)
+        // Master password "testtest" encrypted with the default settings.security key
+        Files.writeString(m2Dir.resolve("settings-security.xml"), """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <settingsSecurity>
+                  <master>{1wQaa6S/o8MH7FnaTNL53XmhT5O0SEGXQi3gC49o6OY=}</master>
+                </settingsSecurity>
+                """);
+        // Server password "testtest" encrypted with the master password above
+        Files.writeString(m2Dir.resolve("settings.xml"), """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0">
+                  <servers>
+                    <server>
+                      <id>testserver</id>
+                      <username>testuser</username>
+                      <password>{BteqUEnqHecHM7MZfnj9FwLcYbdInWxou1C929Txa0A=}</password>
+                    </server>
+                  </servers>
+                </settings>
+                """);
+
+        String oldHome = System.getProperty("user.home");
+        try {
+            System.setProperty("user.home", tempDir.toString());
+            // With dispatchers active, the legacy password should be decrypted
+            Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.IF_AVAILABLE);
+            Settings settings = session.getSettings();
+            assertNotNull(settings);
+            assertEquals(1, settings.getServers().size());
+            Server server = settings.getServers().get(0);
+            assertEquals("testserver", server.getId());
+            assertEquals("testuser", server.getUsername());
+            assertEquals("testtest", server.getPassword());
+        } finally {
+            System.setProperty("user.home", oldHome);
+        }
+    }
+
+    @Test
+    void testPlaintextPasswordUnchangedWithSecurityModeNone() throws IOException {
+        // SecurityMode.NONE prevents ApiRunner from binding its own SecDispatcherBindings.
+        // Note: in a test environment, discover() may still find a test-scoped SecDispatcherProvider;
+        // this test verifies that plaintext passwords pass through correctly in all modes.
+        Path m2Dir = tempDir.resolve(".m2");
+        Files.createDirectories(m2Dir);
+        Files.writeString(m2Dir.resolve("settings.xml"), """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <settings>
+                  <servers>
+                    <server>
+                      <id>testserver</id>
+                      <username>testuser</username>
+                      <password>plaintext-password</password>
+                    </server>
+                  </servers>
+                </settings>
+                """);
+
+        String oldHome = System.getProperty("user.home");
+        try {
+            System.setProperty("user.home", tempDir.toString());
+            Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+            Settings settings = session.getSettings();
+            assertNotNull(settings);
+            assertEquals(1, settings.getServers().size());
+            Server server = settings.getServers().get(0);
+            assertEquals("plaintext-password", server.getPassword());
+        } finally {
+            System.setProperty("user.home", oldHome);
+        }
+    }
+
+    @Test
+    void testSettingsOfflineMode() throws IOException {
+        Path m2Dir = tempDir.resolve(".m2");
+        Files.createDirectories(m2Dir);
+        Files.writeString(m2Dir.resolve("settings.xml"), """
+                <?xml version="1.0" encoding="UTF-8"?>
+                <settings xmlns="http://maven.apache.org/SETTINGS/1.2.0">
+                  <offline>true</offline>
+                </settings>
+                """);
+
+        String oldHome = System.getProperty("user.home");
+        try {
+            System.setProperty("user.home", tempDir.toString());
+            Session session = ApiRunner.createSession(null, null, ApiRunner.SecurityMode.NONE);
+            Settings settings = session.getSettings();
+            assertNotNull(settings);
+            assertTrue(settings.isOffline(), "Settings should have offline mode enabled");
+        } finally {
+            System.setProperty("user.home", oldHome);
+        }
+    }
+}