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

majin1102 pushed a commit to branch emr/hms-namespace-explorer
in repository https://gitbox.apache.org/repos/asf/amoro.git

commit 269587f9017e51b2dd71ff4c1c0abfb3613fed87
Author: majin.nathan <[email protected]>
AuthorDate: Mon Aug 10 16:57:01 2026 +0800

    feat: support HMS namespaces and Lance tables
---
 .../apache/amoro/server/AmoroManagementConf.java   |   8 +
 .../apache/amoro/server/AmoroServiceContainer.java |  17 +-
 .../amoro/server/catalog/CatalogBuilder.java       |   3 +-
 .../amoro/server/catalog/CatalogManager.java       |   5 +
 .../server/catalog/DefaultCatalogManager.java      |  23 +-
 .../amoro/server/catalog/ExternalCatalog.java      |  11 +
 .../amoro/server/las/LasCatalogSynchronizer.java   |  19 +-
 .../amoro/server/las/LasIntegrationConfig.java     |  13 +-
 .../amoro/server/las/LasIntegrationContext.java    |  50 +++-
 .../TestAmoroServiceContainerEnvironment.java      |  40 +++
 .../TestDefaultCatalogManagerNamespaces.java       |  20 ++
 .../server/las/TestLasCatalogSynchronizer.java     |  28 +-
 .../server/las/TestLasIntegrationContext.java      |  30 ++
 .../org/apache/amoro/CommonUnifiedCatalog.java     |  60 ++--
 amoro-format-lance/pom.xml                         |  35 ++-
 .../amoro/formats/lance/LanceCatalogFactory.java   |   4 +
 .../amoro/formats/lance/LanceHms3Catalog.java      | 302 +++++++++++++++++++++
 .../formats/lance/LanceStorageOptionsProvider.java | 205 ++++++++++++++
 .../formats/lance/TestLanceCatalogFactory.java     |  67 +++++
 .../amoro/formats/lance/TestLanceHms3Catalog.java  | 144 ++++++++++
 .../lance/TestLanceHms3CatalogIntegration.java     |  53 ++++
 .../lance/TestLanceStorageOptionsProvider.java     |  91 +++++++
 .../amoro/formats/paimon/PaimonCatalogFactory.java |  28 ++
 .../formats/paimon/TestPaimonCatalogFactory.java   |  74 +++++
 amoro-web/mock/modules/catalogs.js                 |  37 ++-
 amoro-web/mock/modules/common.js                   |   6 +-
 amoro-web/mock/modules/table.js                    |  35 ++-
 amoro-web/src/components/Sidebar.vue               |   4 +
 amoro-web/src/views/catalogs/index.vue             |  17 +-
 amoro-web/src/views/hive-details/index.vue         |  11 +-
 .../src/views/tables/components/TableExplorer.vue  | 174 +++++++++---
 amoro-web/src/views/tables/index.vue               |  84 +++---
 dist/src/main/amoro-bin/bin/load-config.sh         |  10 +
 dist/src/main/amoro-bin/conf/config.yaml           |  11 +-
 pom.xml                                            |  12 +
 35 files changed, 1572 insertions(+), 159 deletions(-)

diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
index c122cee2b..90338dd5d 100644
--- a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroManagementConf.java
@@ -119,6 +119,14 @@ public class AmoroManagementConf {
           .defaultValue(true)
           .withDescription("Whether catalogs are grouped by an optional 
namespace.");
 
+  public static final ConfigOption<Boolean> 
CATALOG_NAMESPACE_ALLOWLIST_ENABLED =
+      ConfigOptions.key("catalog.namespace-allowlist-enabled")
+          .booleanType()
+          .defaultValue(true)
+          .withDescription(
+              "Whether namespace discovery and HMS catalog synchronization are 
restricted to the"
+                  + " namespace allowlist.");
+
   public static final ConfigOption<Integer> TABLE_MANIFEST_IO_THREAD_COUNT =
       ConfigOptions.key("table-manifest-io.thread-count")
           .intType()
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java
index b9eb95f2d..c84a1d77d 100644
--- a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java
+++ b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java
@@ -575,6 +575,21 @@ public class AmoroServiceContainer {
         .build();
   }
 
+  @VisibleForTesting
+  static String redactEnvironmentValue(String key, String value) {
+    String normalizedKey = key.toUpperCase(Locale.ROOT);
+    if (normalizedKey.endsWith("_AK")
+        || normalizedKey.endsWith("_SK")
+        || normalizedKey.contains("ACCESS_KEY")
+        || normalizedKey.contains("SECRET")
+        || normalizedKey.contains("TOKEN")
+        || normalizedKey.contains("PASSWORD")
+        || normalizedKey.contains("CREDENTIAL")) {
+      return "<redacted>";
+    }
+    return value;
+  }
+
   private class ConfigurationHelper {
 
     private JsonNode yamlConfig;
@@ -614,7 +629,7 @@ public class AmoroServiceContainer {
     private Map<String, Object> initEnvConfig() {
       LOG.info("initializing system env configuration...");
       Map<String, String> envs = System.getenv();
-      envs.forEach((k, v) -> LOG.info("export {}={}", k, v));
+      envs.forEach((k, v) -> LOG.info("export {}={}", k, 
redactEnvironmentValue(k, v)));
       String prefix = AmoroManagementConf.SYSTEM_CONFIG.toUpperCase();
       return ConfigHelpers.convertConfigurationKeys(prefix, System.getenv());
     }
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogBuilder.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogBuilder.java
index af39ba533..6eedd1e8f 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogBuilder.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogBuilder.java
@@ -68,7 +68,8 @@ public class CatalogBuilder {
               TableFormat.MIXED_ICEBERG,
               TableFormat.MIXED_HIVE,
               TableFormat.PAIMON,
-              TableFormat.HUDI),
+              TableFormat.HUDI,
+              TableFormat.LANCE),
           CATALOG_TYPE_AMS,
           Sets.newHashSet(TableFormat.ICEBERG, TableFormat.MIXED_ICEBERG));
 
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogManager.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogManager.java
index 5a1c05c19..5163d28f9 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogManager.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/CatalogManager.java
@@ -39,6 +39,11 @@ public interface CatalogManager {
     return false;
   }
 
+  /** Returns whether namespace discovery is restricted to an explicit 
allowlist. */
+  default boolean namespaceAllowlistEnabled() {
+    return true;
+  }
+
   /** Lists namespaces, or the community-compatible default namespace when 
unsupported. */
   default List<String> listNamespaces() {
     return Collections.singletonList("default");
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/DefaultCatalogManager.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/DefaultCatalogManager.java
index c815afc8a..6bb1b3f4e 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/DefaultCatalogManager.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/DefaultCatalogManager.java
@@ -52,6 +52,7 @@ import java.time.Duration;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
@@ -113,11 +114,27 @@ public class DefaultCatalogManager extends PersistentBase 
implements CatalogMana
     return 
serverConfiguration.getBoolean(AmoroManagementConf.CATALOG_NAMESPACE_ENABLED);
   }
 
+  @Override
+  public boolean namespaceAllowlistEnabled() {
+    return 
serverConfiguration.getBoolean(AmoroManagementConf.CATALOG_NAMESPACE_ALLOWLIST_ENABLED);
+  }
+
   @Override
   public List<String> listNamespaces() {
     if (!supportNamespace()) {
       return Collections.singletonList("default");
     }
+    if (!namespaceAllowlistEnabled()) {
+      return listCatalogMetas().stream()
+          .map(CatalogMeta::getCatalogProperties)
+          .filter(Objects::nonNull)
+          .map(properties -> properties.get(CatalogMetaProperties.NAMESPACE))
+          .filter(Objects::nonNull)
+          .filter(namespace -> !namespace.trim().isEmpty())
+          .distinct()
+          .sorted()
+          .collect(Collectors.toList());
+    }
     return getAs(NamespaceAllowlistMapper.class, 
NamespaceAllowlistMapper::listNamespaces);
   }
 
@@ -129,7 +146,7 @@ public class DefaultCatalogManager extends PersistentBase 
implements CatalogMana
     if (!supportNamespace()) {
       return "default".equals(namespace) ? listCatalogMetas() : 
Collections.emptyList();
     }
-    if (!listNamespaces().contains(namespace)) {
+    if (namespaceAllowlistEnabled() && !listNamespaces().contains(namespace)) {
       return Collections.emptyList();
     }
     return listCatalogMetas().stream()
@@ -144,6 +161,8 @@ public class DefaultCatalogManager extends PersistentBase 
implements CatalogMana
   @Override
   public void addNamespace(String namespace) {
     Preconditions.checkState(supportNamespace(), "Catalog namespaces are not 
enabled");
+    Preconditions.checkState(
+        namespaceAllowlistEnabled(), "Catalog namespace allowlist is not 
enabled");
     try {
       doAs(
           NamespaceAllowlistMapper.class,
@@ -163,6 +182,8 @@ public class DefaultCatalogManager extends PersistentBase 
implements CatalogMana
   @Override
   public void removeNamespace(String namespace) {
     Preconditions.checkState(supportNamespace(), "Catalog namespaces are not 
enabled");
+    Preconditions.checkState(
+        namespaceAllowlistEnabled(), "Catalog namespace allowlist is not 
enabled");
     doAs(NamespaceAllowlistMapper.class, mapper -> 
mapper.deleteNamespace(namespace));
   }
 
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/ExternalCatalog.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/ExternalCatalog.java
index 8148f64a3..5b17c42df 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/catalog/ExternalCatalog.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/catalog/ExternalCatalog.java
@@ -129,6 +129,17 @@ public class ExternalCatalog extends ServerCatalog {
     return doAs(() -> unifiedCatalog.loadTable(database, tableName));
   }
 
+  @Override
+  public void dispose() {
+    if (unifiedCatalog instanceof AutoCloseable) {
+      try {
+        ((AutoCloseable) unifiedCatalog).close();
+      } catch (Exception e) {
+        throw new IllegalStateException("Failed to dispose external catalog " 
+ name(), e);
+      }
+    }
+  }
+
   private void updateDatabaseFilter(CatalogMeta metadata) {
     String databaseFilter =
         
metadata.getCatalogProperties().get(CatalogMetaProperties.KEY_DATABASE_FILTER);
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/las/LasCatalogSynchronizer.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/las/LasCatalogSynchronizer.java
index e52a1f5b4..060742cf7 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/las/LasCatalogSynchronizer.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/las/LasCatalogSynchronizer.java
@@ -46,7 +46,7 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
-/** Periodically projects allowlisted HMS3 catalogs into AMS CatalogMeta 
records. */
+/** Periodically projects HMS3 catalogs into AMS CatalogMeta records. */
 public final class LasCatalogSynchronizer implements AutoCloseable {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(LasCatalogSynchronizer.class);
@@ -106,7 +106,9 @@ public final class LasCatalogSynchronizer implements 
AutoCloseable {
     }
 
     Set<String> rawCatalogNames = new HashSet<>(discoveredCatalogs);
-    Set<String> allowlistedNamespaces = new 
HashSet<>(catalogManager.listNamespaces());
+    boolean allowlistEnabled = catalogManager.namespaceAllowlistEnabled();
+    Set<String> allowlistedNamespaces =
+        allowlistEnabled ? new HashSet<>(catalogManager.listNamespaces()) : 
Collections.emptySet();
     Map<String, CatalogMeta> existingCatalogs =
         catalogManager.listCatalogMetas().stream()
             .collect(Collectors.toMap(CatalogMeta::getCatalogName, catalog -> 
catalog));
@@ -116,7 +118,8 @@ public final class LasCatalogSynchronizer implements 
AutoCloseable {
     int removed = 0;
     for (String physicalCatalogName : rawCatalogNames) {
       Optional<String> namespace = parseNamespace(physicalCatalogName);
-      if (!namespace.isPresent() || 
!allowlistedNamespaces.contains(namespace.get())) {
+      if (!namespace.isPresent()
+          || (allowlistEnabled && 
!allowlistedNamespaces.contains(namespace.get()))) {
         continue;
       }
 
@@ -146,8 +149,10 @@ public final class LasCatalogSynchronizer implements 
AutoCloseable {
     }
 
     LOG.info(
-        "LAS HMS catalog synchronization completed: discovered={}, 
allowlistedNamespaces={}, created={}, updated={}, removed={}",
+        "LAS HMS catalog synchronization completed: discovered={}, 
allowlistEnabled={},"
+            + " allowlistedNamespaces={}, created={}, updated={}, removed={}",
         rawCatalogNames.size(),
+        allowlistEnabled,
         allowlistedNamespaces.size(),
         created,
         updated,
@@ -182,7 +187,11 @@ public final class LasCatalogSynchronizer implements 
AutoCloseable {
     Map<String, String> catalogProperties = new HashMap<>();
     catalogProperties.put(
         CatalogMetaProperties.TABLE_FORMATS,
-        TableFormat.ICEBERG.name() + "," + TableFormat.PAIMON.name());
+        TableFormat.ICEBERG.name()
+            + ","
+            + TableFormat.PAIMON.name()
+            + ","
+            + TableFormat.LANCE.name());
     catalogProperties.put(CatalogMetaProperties.NAMESPACE, namespace);
     catalogProperties.put(CATALOG_SOURCE, CATALOG_SOURCE_LAS_HMS);
     catalogProperties.put(
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/las/LasIntegrationConfig.java 
b/amoro-ams/src/main/java/org/apache/amoro/server/las/LasIntegrationConfig.java
index 65bd5cc6a..4e373fb24 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/las/LasIntegrationConfig.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/las/LasIntegrationConfig.java
@@ -28,6 +28,9 @@ public final class LasIntegrationConfig {
 
   private static final String PREFIX = "las.integration.";
 
+  public static final String LAS_SERVICE_ACCESS_KEY_ENV = "LAS_SERVICE_AK";
+  public static final String LAS_SERVICE_SECRET_KEY_ENV = "LAS_SERVICE_SK";
+
   public static final ConfigOption<Boolean> ENABLED =
       ConfigOptions.key(PREFIX + "enabled")
           .booleanType()
@@ -62,13 +65,19 @@ public final class LasIntegrationConfig {
       ConfigOptions.key(PREFIX + "iam.bootstrap-access-key")
           .stringType()
           .noDefaultValue()
-          .withDescription("Access key of the AMS workload identity used to 
call AssumeRole.");
+          .withDescription(
+              "Optional access key override for the AMS workload identity. LAS 
deployments use "
+                  + LAS_SERVICE_ACCESS_KEY_ENV
+                  + ".");
 
   public static final ConfigOption<String> IAM_BOOTSTRAP_SECRET_KEY =
       ConfigOptions.key(PREFIX + "iam.bootstrap-secret-key")
           .stringType()
           .noDefaultValue()
-          .withDescription("Secret key of the AMS workload identity used to 
call AssumeRole.");
+          .withDescription(
+              "Optional secret key override for the AMS workload identity. LAS 
deployments use "
+                  + LAS_SERVICE_SECRET_KEY_ENV
+                  + ".");
 
   public static final ConfigOption<String> IAM_BOOTSTRAP_SESSION_TOKEN =
       ConfigOptions.key(PREFIX + "iam.bootstrap-session-token")
diff --git 
a/amoro-ams/src/main/java/org/apache/amoro/server/las/LasIntegrationContext.java
 
b/amoro-ams/src/main/java/org/apache/amoro/server/las/LasIntegrationContext.java
index 7c558747b..ecbd99233 100644
--- 
a/amoro-ams/src/main/java/org/apache/amoro/server/las/LasIntegrationContext.java
+++ 
b/amoro-ams/src/main/java/org/apache/amoro/server/las/LasIntegrationContext.java
@@ -34,6 +34,8 @@ import java.time.Duration;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 
 /** Validated network configuration and client factories shared by future LAS 
OpenAPI handlers. */
@@ -50,15 +52,30 @@ public final class LasIntegrationContext {
   private final String emrServerlessService;
   private final Duration connectTimeout;
   private final Duration readTimeout;
+  private final String bootstrapAccessKey;
+  private final String bootstrapSecretKey;
+  private final String bootstrapSessionToken;
 
-  private LasIntegrationContext(Configurations configurations) {
+  private LasIntegrationContext(
+      Configurations configurations, Map<String, String> environmentVariables) 
{
     this.configurations = configurations;
+    Objects.requireNonNull(environmentVariables, "environmentVariables");
     this.enabled = configurations.getBoolean(LasIntegrationConfig.ENABLED);
     this.region = configurations.getString(LasIntegrationConfig.REGION);
     this.emrServerlessService =
         configurations.getString(LasIntegrationConfig.EMR_SERVERLESS_SERVICE);
     this.connectTimeout = 
configurations.get(LasIntegrationConfig.CONNECT_TIMEOUT);
     this.readTimeout = configurations.get(LasIntegrationConfig.READ_TIMEOUT);
+    this.bootstrapAccessKey =
+        firstNonBlank(
+            
configurations.getString(LasIntegrationConfig.IAM_BOOTSTRAP_ACCESS_KEY),
+            
environmentVariables.get(LasIntegrationConfig.LAS_SERVICE_ACCESS_KEY_ENV));
+    this.bootstrapSecretKey =
+        firstNonBlank(
+            
configurations.getString(LasIntegrationConfig.IAM_BOOTSTRAP_SECRET_KEY),
+            
environmentVariables.get(LasIntegrationConfig.LAS_SERVICE_SECRET_KEY_ENV));
+    this.bootstrapSessionToken =
+        
configurations.getString(LasIntegrationConfig.IAM_BOOTSTRAP_SESSION_TOKEN);
 
     if (!enabled) {
       this.hmsUri = null;
@@ -76,8 +93,8 @@ public final class LasIntegrationContext {
         requiredUri(configurations, 
LasIntegrationConfig.EMR_SERVERLESS_ENDPOINT, HTTP_SCHEMES);
     requiredString(configurations, LasIntegrationConfig.REGION);
     requiredString(configurations, 
LasIntegrationConfig.EMR_SERVERLESS_SERVICE);
-    requiredString(configurations, 
LasIntegrationConfig.IAM_BOOTSTRAP_ACCESS_KEY);
-    requiredString(configurations, 
LasIntegrationConfig.IAM_BOOTSTRAP_SECRET_KEY);
+    requiredCredential(LasIntegrationConfig.LAS_SERVICE_ACCESS_KEY_ENV, 
bootstrapAccessKey);
+    requiredCredential(LasIntegrationConfig.LAS_SERVICE_SECRET_KEY_ENV, 
bootstrapSecretKey);
     requiredString(configurations, LasIntegrationConfig.IAM_ROLE_SESSION_NAME);
     requiredString(configurations, LasIntegrationConfig.IAM_DATA_ROLE_NAME);
     positiveDuration(configurations, LasIntegrationConfig.CONNECT_TIMEOUT);
@@ -98,7 +115,12 @@ public final class LasIntegrationContext {
   }
 
   public static LasIntegrationContext initialize(Configurations 
configurations) {
-    return new LasIntegrationContext(configurations);
+    return new LasIntegrationContext(configurations, System.getenv());
+  }
+
+  static LasIntegrationContext initialize(
+      Configurations configurations, Map<String, String> environmentVariables) 
{
+    return new LasIntegrationContext(configurations, environmentVariables);
   }
 
   public boolean enabled() {
@@ -143,13 +165,9 @@ public final class LasIntegrationContext {
 
   public Credential bootstrapCredential() {
     ensureEnabled();
-    String accessKey = 
configurations.getString(LasIntegrationConfig.IAM_BOOTSTRAP_ACCESS_KEY);
-    String secretKey = 
configurations.getString(LasIntegrationConfig.IAM_BOOTSTRAP_SECRET_KEY);
-    String sessionToken =
-        
configurations.getString(LasIntegrationConfig.IAM_BOOTSTRAP_SESSION_TOKEN);
-    return StringUtils.isBlank(sessionToken)
-        ? new Credential(accessKey, secretKey)
-        : new Credential(accessKey, secretKey, sessionToken);
+    return StringUtils.isBlank(bootstrapSessionToken)
+        ? new Credential(bootstrapAccessKey, bootstrapSecretKey)
+        : new Credential(bootstrapAccessKey, bootstrapSecretKey, 
bootstrapSessionToken);
   }
 
   public URI hmsUri() {
@@ -263,6 +281,16 @@ public final class LasIntegrationContext {
     return value;
   }
 
+  private static String firstNonBlank(String preferred, String fallback) {
+    return StringUtils.isNotBlank(preferred) ? preferred : fallback;
+  }
+
+  private static void requiredCredential(String environmentVariable, String 
value) {
+    if (StringUtils.isBlank(value)) {
+      throw new IllegalArgumentException(environmentVariable + " must be 
configured");
+    }
+  }
+
   private static Duration positiveDuration(
       Configurations configurations, ConfigOption<Duration> option) {
     Duration value = configurations.get(option);
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/TestAmoroServiceContainerEnvironment.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/TestAmoroServiceContainerEnvironment.java
new file mode 100644
index 000000000..6442b87d1
--- /dev/null
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/TestAmoroServiceContainerEnvironment.java
@@ -0,0 +1,40 @@
+/*
+ * 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.amoro.server;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestAmoroServiceContainerEnvironment {
+
+  @Test
+  public void testSensitiveEnvironmentValuesAreRedacted() {
+    Assertions.assertEquals(
+        "<redacted>", 
AmoroServiceContainer.redactEnvironmentValue("LAS_SERVICE_AK", "ak"));
+    Assertions.assertEquals(
+        "<redacted>", 
AmoroServiceContainer.redactEnvironmentValue("LAS_SERVICE_SK", "sk"));
+    Assertions.assertEquals(
+        "<redacted>", 
AmoroServiceContainer.redactEnvironmentValue("ASSUME_ROLE_ACCESS_KEY", "ak"));
+    Assertions.assertEquals(
+        "<redacted>", 
AmoroServiceContainer.redactEnvironmentValue("SESSION_TOKEN", "token"));
+    Assertions.assertEquals(
+        "thrift://hms:9083",
+        AmoroServiceContainer.redactEnvironmentValue("HMS_URI", 
"thrift://hms:9083"));
+  }
+}
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/catalog/TestDefaultCatalogManagerNamespaces.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/catalog/TestDefaultCatalogManagerNamespaces.java
index a8b91fc05..1e57f7368 100644
--- 
a/amoro-ams/src/test/java/org/apache/amoro/server/catalog/TestDefaultCatalogManagerNamespaces.java
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/catalog/TestDefaultCatalogManagerNamespaces.java
@@ -107,6 +107,26 @@ public class TestDefaultCatalogManagerNamespaces {
     Assertions.assertTrue(manager.listCatalogMetas("tenant-a").isEmpty());
   }
 
+  @Test
+  public void testDisabledAllowlistDiscoversNamespacesFromCatalogs() {
+    Configurations configurations = new Configurations();
+    configurations.setBoolean(AmoroManagementConf.CATALOG_NAMESPACE_ENABLED, 
true);
+    
configurations.setBoolean(AmoroManagementConf.CATALOG_NAMESPACE_ALLOWLIST_ENABLED,
 false);
+    DefaultCatalogManager manager = new DefaultCatalogManager(configurations);
+
+    DB.insertCatalog(catalog("tenant-b@las", "tenant-b"));
+    DB.insertCatalog(catalog("tenant-a@las", "tenant-a"));
+
+    Assertions.assertTrue(manager.supportNamespace());
+    Assertions.assertFalse(manager.namespaceAllowlistEnabled());
+    Assertions.assertEquals(Arrays.asList("tenant-a", "tenant-b"), 
manager.listNamespaces());
+    Assertions.assertEquals(
+        Collections.singletonList("tenant-a@las"),
+        catalogNames(manager.listCatalogMetas("tenant-a")));
+    Assertions.assertTrue(manager.listCatalogMetas("unknown").isEmpty());
+    Assertions.assertThrows(IllegalStateException.class, () -> 
manager.addNamespace("tenant-c"));
+  }
+
   private static CatalogMeta catalog(String catalogName, String namespace) {
     HashMap<String, String> properties = new HashMap<>();
     properties.put(CatalogMetaProperties.NAMESPACE, namespace);
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/las/TestLasCatalogSynchronizer.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/las/TestLasCatalogSynchronizer.java
index 24af35cc7..dfcfddd32 100644
--- 
a/amoro-ams/src/test/java/org/apache/amoro/server/las/TestLasCatalogSynchronizer.java
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/las/TestLasCatalogSynchronizer.java
@@ -45,9 +45,11 @@ import java.util.Arrays;
 import java.util.Base64;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 public class TestLasCatalogSynchronizer {
 
@@ -72,6 +74,7 @@ public class TestLasCatalogSynchronizer {
         .thenReturn(
             Arrays.asList("tenant-a@las", "tenant-a@manual", "tenant-b@keep", 
"invalid-catalog"));
     CatalogManager catalogManager = mock(CatalogManager.class);
+    when(catalogManager.namespaceAllowlistEnabled()).thenReturn(true);
     
when(catalogManager.listNamespaces()).thenReturn(Collections.singletonList("tenant-a"));
     CatalogMeta manual = catalog("tenant-a@manual", "tenant-a", false);
     CatalogMeta removedFromAllowlist = catalog("tenant-b@keep", "tenant-b", 
true);
@@ -131,6 +134,7 @@ public class TestLasCatalogSynchronizer {
     HMSClient hmsSdk = mock(HMSClient.class);
     
when(hmsSdk.getCatalogs()).thenReturn(Collections.singletonList("tenant-a@las"));
     CatalogManager catalogManager = mock(CatalogManager.class);
+    when(catalogManager.namespaceAllowlistEnabled()).thenReturn(true);
     
when(catalogManager.listNamespaces()).thenReturn(Collections.singletonList("tenant-a"));
     CatalogMeta existing = catalog("tenant-a@las", "tenant-a", true);
     existing.getCatalogProperties().put("preserved", "value");
@@ -143,7 +147,7 @@ public class TestLasCatalogSynchronizer {
     verify(catalogManager).updateCatalog(updated.capture());
     Assertions.assertEquals("value", 
updated.getValue().getCatalogProperties().get("preserved"));
     Assertions.assertEquals(
-        "ICEBERG,PAIMON",
+        "ICEBERG,PAIMON,LANCE",
         
updated.getValue().getCatalogProperties().get(CatalogMetaProperties.TABLE_FORMATS));
     when(catalogManager.listCatalogMetas())
         .thenReturn(Collections.singletonList(updated.getValue()));
@@ -164,6 +168,28 @@ public class TestLasCatalogSynchronizer {
     verifyNoInteractions(catalogManager);
   }
 
+  @Test
+  public void testDisabledAllowlistSynchronizesAllValidCatalogs() throws 
Exception {
+    HMSClient hmsSdk = mock(HMSClient.class);
+    when(hmsSdk.getCatalogs())
+        .thenReturn(Arrays.asList("tenant-a@las", "tenant-b@hive", 
"invalid-catalog"));
+    CatalogManager catalogManager = mock(CatalogManager.class);
+    when(catalogManager.namespaceAllowlistEnabled()).thenReturn(false);
+    
when(catalogManager.listCatalogMetas()).thenReturn(Collections.emptyList());
+
+    LasCatalogSynchronizer synchronizer = synchronizer(hmsSdk, catalogManager);
+    synchronizer.syncOnce();
+
+    ArgumentCaptor<CatalogMeta> created = 
ArgumentCaptor.forClass(CatalogMeta.class);
+    verify(catalogManager, times(2)).createCatalog(created.capture());
+    Assertions.assertEquals(
+        new HashSet<>(Arrays.asList("tenant-a@las", "tenant-b@hive")),
+        created.getAllValues().stream()
+            .map(CatalogMeta::getCatalogName)
+            .collect(Collectors.toSet()));
+    verify(catalogManager, never()).listNamespaces();
+  }
+
   @Test
   public void testInvalidHmsSnapshotDoesNotMutateCatalogState() throws 
Exception {
     HMSClient hmsSdk = mock(HMSClient.class);
diff --git 
a/amoro-ams/src/test/java/org/apache/amoro/server/las/TestLasIntegrationContext.java
 
b/amoro-ams/src/test/java/org/apache/amoro/server/las/TestLasIntegrationContext.java
index fdeebaf5e..7e8530b87 100644
--- 
a/amoro-ams/src/test/java/org/apache/amoro/server/las/TestLasIntegrationContext.java
+++ 
b/amoro-ams/src/test/java/org/apache/amoro/server/las/TestLasIntegrationContext.java
@@ -66,6 +66,36 @@ public class TestLasIntegrationContext {
     
Assertions.assertTrue(exception.getMessage().contains(LasIntegrationConfig.HMS_URI.key()));
   }
 
+  @Test
+  public void testUsesExistingLasServiceCredentialEnvironmentNames() {
+    Configurations configurations = validConfigurations();
+    configurations.removeConfig(LasIntegrationConfig.IAM_BOOTSTRAP_ACCESS_KEY);
+    configurations.removeConfig(LasIntegrationConfig.IAM_BOOTSTRAP_SECRET_KEY);
+    Map<String, String> environment = new HashMap<>();
+    environment.put(LasIntegrationConfig.LAS_SERVICE_ACCESS_KEY_ENV, 
"las-service-ak");
+    environment.put(LasIntegrationConfig.LAS_SERVICE_SECRET_KEY_ENV, 
"las-service-sk");
+
+    LasIntegrationContext context = 
LasIntegrationContext.initialize(configurations, environment);
+    bytedance.olap.iam.Credential credential = context.bootstrapCredential();
+
+    Assertions.assertEquals("las-service-ak", credential.getAccessKeyId());
+    Assertions.assertEquals("las-service-sk", credential.getSecretAccessKey());
+  }
+
+  @Test
+  public void testExplicitCredentialConfigurationOverridesEnvironment() {
+    Map<String, String> environment = new HashMap<>();
+    environment.put(LasIntegrationConfig.LAS_SERVICE_ACCESS_KEY_ENV, 
"las-service-ak");
+    environment.put(LasIntegrationConfig.LAS_SERVICE_SECRET_KEY_ENV, 
"las-service-sk");
+
+    LasIntegrationContext context =
+        LasIntegrationContext.initialize(validConfigurations(), environment);
+    bytedance.olap.iam.Credential credential = context.bootstrapCredential();
+
+    Assertions.assertEquals("bootstrap-ak", credential.getAccessKeyId());
+    Assertions.assertEquals("bootstrap-sk", credential.getSecretAccessKey());
+  }
+
   @Test
   public void testCrossVpcConfigurationIsAtomic() {
     Configurations configurations = validConfigurations();
diff --git 
a/amoro-common/src/main/java/org/apache/amoro/CommonUnifiedCatalog.java 
b/amoro-common/src/main/java/org/apache/amoro/CommonUnifiedCatalog.java
index 281911c9a..f15a214d1 100644
--- a/amoro-common/src/main/java/org/apache/amoro/CommonUnifiedCatalog.java
+++ b/amoro-common/src/main/java/org/apache/amoro/CommonUnifiedCatalog.java
@@ -33,7 +33,7 @@ import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-public class CommonUnifiedCatalog implements UnifiedCatalog {
+public class CommonUnifiedCatalog implements UnifiedCatalog, AutoCloseable {
 
   private final String catalogName;
   private final String metaStoreType;
@@ -116,12 +116,12 @@ public class CommonUnifiedCatalog implements 
UnifiedCatalog {
   @Override
   public AmoroTable<?> loadTable(String database, String table) {
     return formatCatalogAsOrder(
+            TableFormat.LANCE,
             TableFormat.MIXED_HIVE,
             TableFormat.MIXED_ICEBERG,
             TableFormat.ICEBERG,
             TableFormat.PAIMON,
-            TableFormat.HUDI,
-            TableFormat.LANCE)
+            TableFormat.HUDI)
         .map(
             formatCatalog -> {
               try {
@@ -144,12 +144,12 @@ public class CommonUnifiedCatalog implements 
UnifiedCatalog {
   public List<TableIDWithFormat> listTables(String database) {
     TableFormat[] formats =
         new TableFormat[] {
+          TableFormat.LANCE,
           TableFormat.MIXED_HIVE,
           TableFormat.MIXED_ICEBERG,
           TableFormat.ICEBERG,
           TableFormat.PAIMON,
-          TableFormat.HUDI,
-          TableFormat.LANCE
+          TableFormat.HUDI
         };
 
     Map<String, TableFormat> tableNameToFormat = Maps.newHashMap();
@@ -206,19 +206,47 @@ public class CommonUnifiedCatalog implements 
UnifiedCatalog {
     ServiceLoader<FormatCatalogFactory> loader = 
ServiceLoader.load(FormatCatalogFactory.class);
     String normalizedMetastoreType = 
CatalogUtil.normalizeMetastoreType(metaStoreType);
     Set<TableFormat> formats = 
CatalogUtil.tableFormats(normalizedMetastoreType, catalogProperties);
-    Map<TableFormat, FormatCatalog> formatCatalogs = Maps.newConcurrentMap();
-    for (FormatCatalogFactory factory : loader) {
-      if (formats.contains(factory.format())) {
-        Map<String, String> formatCatalogProperties =
-            factory.convertCatalogProperties(
-                name(), normalizedMetastoreType, this.catalogProperties);
-        FormatCatalog catalog =
-            factory.create(
-                name(), normalizedMetastoreType, formatCatalogProperties, 
tableMetaStore);
-        formatCatalogs.put(factory.format(), catalog);
+    Map<TableFormat, FormatCatalog> newFormatCatalogs = 
Maps.newConcurrentMap();
+    try {
+      for (FormatCatalogFactory factory : loader) {
+        if (formats.contains(factory.format())) {
+          Map<String, String> formatCatalogProperties =
+              factory.convertCatalogProperties(
+                  name(), normalizedMetastoreType, this.catalogProperties);
+          FormatCatalog catalog =
+              factory.create(
+                  name(), normalizedMetastoreType, formatCatalogProperties, 
tableMetaStore);
+          newFormatCatalogs.put(factory.format(), catalog);
+        }
       }
+    } catch (RuntimeException e) {
+      closeFormatCatalogs(newFormatCatalogs);
+      throw e;
     }
-    this.formatCatalogs = formatCatalogs;
+    Map<TableFormat, FormatCatalog> oldFormatCatalogs = this.formatCatalogs;
+    this.formatCatalogs = newFormatCatalogs;
+    closeFormatCatalogs(oldFormatCatalogs);
+  }
+
+  @Override
+  public void close() {
+    Map<TableFormat, FormatCatalog> catalogs = this.formatCatalogs;
+    this.formatCatalogs = Maps.newHashMap();
+    closeFormatCatalogs(catalogs);
+  }
+
+  private static void closeFormatCatalogs(Map<TableFormat, FormatCatalog> 
catalogs) {
+    catalogs.values().stream()
+        .filter(AutoCloseable.class::isInstance)
+        .map(AutoCloseable.class::cast)
+        .forEach(
+            catalog -> {
+              try {
+                catalog.close();
+              } catch (Exception e) {
+                throw new IllegalStateException("Failed to close format 
catalog", e);
+              }
+            });
   }
 
   /** get format catalogs as given format order */
diff --git a/amoro-format-lance/pom.xml b/amoro-format-lance/pom.xml
index 4d546422c..5955279d4 100755
--- a/amoro-format-lance/pom.xml
+++ b/amoro-format-lance/pom.xml
@@ -42,12 +42,19 @@
             <groupId>org.apache.arrow</groupId>
             <artifactId>arrow-vector</artifactId>
         </dependency>
+        <!-- Keep Arrow's Netty buffer and common classes on one version. 
Without this direct
+             dependency Hadoop's older transitive netty-common wins and 
RootAllocator fails. -->
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-common</artifactId>
+            <version>${netty.version}</version>
+        </dependency>
 
         <!-- Lance format Java API -->
         <dependency>
             <groupId>org.lance</groupId>
             <artifactId>lance-core</artifactId>
-            <version>7.0.0</version>
+            <version>7.0.0-ve-6</version>
             <exclusions>
                 <exclusion>
                     <groupId>org.lance</groupId>
@@ -71,6 +78,32 @@
             <artifactId>lance-namespace-apache-client</artifactId>
             <version>0.8.0</version>
         </dependency>
+        <dependency>
+            <groupId>org.lance</groupId>
+            <artifactId>lance-namespace-hive3</artifactId>
+            <version>0.4.1</version>
+            <scope>runtime</scope>
+            <exclusions>
+                <!-- Use the versions already managed by Amoro. Hive3-only 
calls are isolated
+                     behind reflection so the hadoop2 profile remains 
compilable. -->
+                <exclusion>
+                    <groupId>org.lance</groupId>
+                    <artifactId>lance-namespace-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.lance</groupId>
+                    <artifactId>lance-namespace-apache-client</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.hive</groupId>
+                    <artifactId>hive-standalone-metastore</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.hive</groupId>
+                    <artifactId>hive-exec</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
         <dependency>
             <groupId>org.apache.iceberg</groupId>
             <artifactId>iceberg-aws</artifactId>
diff --git 
a/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceCatalogFactory.java
 
b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceCatalogFactory.java
index a78fa1ae3..aeca11224 100755
--- 
a/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceCatalogFactory.java
+++ 
b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceCatalogFactory.java
@@ -21,6 +21,7 @@ package org.apache.amoro.formats.lance;
 import org.apache.amoro.FormatCatalog;
 import org.apache.amoro.FormatCatalogFactory;
 import org.apache.amoro.TableFormat;
+import org.apache.amoro.properties.CatalogMetaProperties;
 import org.apache.amoro.table.TableMetaStore;
 
 import java.util.HashMap;
@@ -38,6 +39,9 @@ public class LanceCatalogFactory implements 
FormatCatalogFactory {
       String metastoreType,
       Map<String, String> properties,
       TableMetaStore metaStore) {
+    if 
(CatalogMetaProperties.CATALOG_TYPE_HIVE.equalsIgnoreCase(metastoreType)) {
+      return new LanceHms3Catalog(catalogName, properties, metaStore);
+    }
     return new LanceDirectoryV1Catalog(catalogName, properties);
   }
 
diff --git 
a/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceHms3Catalog.java
 
b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceHms3Catalog.java
new file mode 100644
index 000000000..e7a7fdedc
--- /dev/null
+++ 
b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceHms3Catalog.java
@@ -0,0 +1,302 @@
+/*
+ * 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.amoro.formats.lance;
+
+import org.apache.amoro.AmoroTable;
+import org.apache.amoro.FormatCatalog;
+import org.apache.amoro.NoSuchTableException;
+import org.apache.amoro.table.TableIdentifier;
+import org.apache.amoro.table.TableMetaStore;
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.lance.Dataset;
+import org.lance.namespace.LanceNamespace;
+import org.lance.namespace.errors.NamespaceNotFoundException;
+import org.lance.namespace.errors.TableNotFoundException;
+import org.lance.namespace.model.DescribeTableResponse;
+import org.lance.namespace.model.ListNamespacesRequest;
+import org.lance.namespace.model.ListNamespacesResponse;
+import org.lance.namespace.model.ListTablesRequest;
+import org.lance.namespace.model.ListTablesResponse;
+import org.lance.namespace.model.NamespaceExistsRequest;
+import org.lance.namespace.model.TableExistsRequest;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeSet;
+
+/** Read-only Lance catalog backed by a Hive 3 metastore. */
+public class LanceHms3Catalog implements FormatCatalog, AutoCloseable {
+
+  private static final String HIVE3_NAMESPACE_CLASS = 
"org.lance.namespace.hive3.Hive3Namespace";
+  private static final int PAGE_SIZE = 1_000;
+
+  private final String catalogName;
+  private final TableMetaStore metaStore;
+  private final BufferAllocator allocator;
+  private final LanceNamespace delegateNamespace;
+  private final LanceNamespace datasetNamespace;
+
+  public LanceHms3Catalog(
+      String catalogName, Map<String, String> properties, TableMetaStore 
metaStore) {
+    BufferAllocator newAllocator = new RootAllocator(Long.MAX_VALUE);
+    LanceNamespace newNamespace;
+    try {
+      newNamespace =
+          createHive3Namespace(
+              properties == null ? Collections.emptyMap() : properties, 
metaStore, newAllocator);
+    } catch (RuntimeException | Error e) {
+      newAllocator.close();
+      throw e;
+    }
+
+    this.catalogName = catalogName;
+    this.metaStore = metaStore;
+    this.allocator = newAllocator;
+    this.delegateNamespace = newNamespace;
+    this.datasetNamespace =
+        withStorageOptions(
+            newNamespace, new 
LanceStorageOptionsProvider(metaStore.getConfiguration()));
+  }
+
+  LanceHms3Catalog(
+      String catalogName,
+      TableMetaStore metaStore,
+      BufferAllocator allocator,
+      LanceNamespace delegateNamespace,
+      LanceStorageOptionsProvider storageOptionsProvider) {
+    this.catalogName = catalogName;
+    this.metaStore = metaStore;
+    this.allocator = allocator;
+    this.delegateNamespace = delegateNamespace;
+    this.datasetNamespace = withStorageOptions(delegateNamespace, 
storageOptionsProvider);
+  }
+
+  @Override
+  public List<String> listDatabases() {
+    TreeSet<String> databases = new TreeSet<>();
+    String pageToken = null;
+    do {
+      ListNamespacesRequest request =
+          new ListNamespacesRequest()
+              .id(Collections.singletonList(catalogName))
+              .limit(PAGE_SIZE)
+              .pageToken(pageToken);
+      ListNamespacesResponse response =
+          metaStore.doAs(() -> delegateNamespace.listNamespaces(request));
+      if (response == null) {
+        break;
+      }
+      if (response.getNamespaces() != null) {
+        databases.addAll(response.getNamespaces());
+      }
+      pageToken = nextPageToken(pageToken, response.getPageToken());
+    } while (pageToken != null);
+    return new ArrayList<>(databases);
+  }
+
+  @Override
+  public boolean databaseExists(String database) {
+    try {
+      NamespaceExistsRequest request = new 
NamespaceExistsRequest().id(identifier(database));
+      metaStore.doAs(
+          () -> {
+            delegateNamespace.namespaceExists(request);
+            return null;
+          });
+      return true;
+    } catch (NamespaceNotFoundException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public boolean tableExists(String database, String table) {
+    try {
+      TableExistsRequest request = new 
TableExistsRequest().id(identifier(database, table));
+      metaStore.doAs(
+          () -> {
+            delegateNamespace.tableExists(request);
+            return null;
+          });
+      return true;
+    } catch (NamespaceNotFoundException | TableNotFoundException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public void createDatabase(String database) {
+    throw readOnly();
+  }
+
+  @Override
+  public void dropDatabase(String database) {
+    throw readOnly();
+  }
+
+  @Override
+  public AmoroTable<?> loadTable(String database, String table) {
+    if (!tableExists(database, table)) {
+      throw new NoSuchTableException(
+          "Lance table " + catalogName + "." + database + "." + table + " does 
not exist");
+    }
+
+    List<String> tableId = identifier(database, table);
+    try {
+      Dataset dataset =
+          metaStore.doAs(
+              () ->
+                  Dataset.open()
+                      .allocator(allocator)
+                      .namespaceClient(datasetNamespace)
+                      .tableId(tableId)
+                      .build());
+      return new LanceTable(
+          TableIdentifier.of(catalogName, database, table), dataset, 
Collections.emptyMap());
+    } catch (RuntimeException e) {
+      throw new IllegalStateException("Failed to open Lance table " + 
String.join(".", tableId), e);
+    }
+  }
+
+  @Override
+  public boolean dropTable(String database, String table, boolean purge) {
+    throw readOnly();
+  }
+
+  @Override
+  public List<String> listTables(String database) {
+    if (!databaseExists(database)) {
+      return Collections.emptyList();
+    }
+
+    TreeSet<String> tables = new TreeSet<>();
+    String pageToken = null;
+    do {
+      ListTablesRequest request =
+          new ListTablesRequest()
+              .id(identifier(database))
+              // HMS is the metadata source of truth. Do not make table 
discovery depend on an
+              // object-store probe, which runs before temporary TOS 
credentials are vended.
+              .includeDeclared(true)
+              .limit(PAGE_SIZE)
+              .pageToken(pageToken);
+      ListTablesResponse response = metaStore.doAs(() -> 
delegateNamespace.listTables(request));
+      if (response == null) {
+        break;
+      }
+      if (response.getTables() != null) {
+        tables.addAll(response.getTables());
+      }
+      pageToken = nextPageToken(pageToken, response.getPageToken());
+    } while (pageToken != null);
+    return new ArrayList<>(tables);
+  }
+
+  @Override
+  public void close() {
+    try {
+      if (delegateNamespace instanceof AutoCloseable) {
+        ((AutoCloseable) delegateNamespace).close();
+      }
+    } catch (Exception e) {
+      throw new IllegalStateException("Failed to close Lance HMS3 namespace", 
e);
+    } finally {
+      allocator.close();
+    }
+  }
+
+  private static LanceNamespace createHive3Namespace(
+      Map<String, String> properties, TableMetaStore metaStore, 
BufferAllocator allocator) {
+    try {
+      Class<?> namespaceClass = Class.forName(HIVE3_NAMESPACE_CLASS);
+      Object namespaceObject = 
namespaceClass.getDeclaredConstructor().newInstance();
+      if (!(namespaceObject instanceof LanceNamespace)) {
+        throw new IllegalStateException(
+            HIVE3_NAMESPACE_CLASS + " does not implement the configured 
LanceNamespace API");
+      }
+      namespaceClass
+          .getMethod("setHadoopConf", Configuration.class)
+          .invoke(namespaceObject, metaStore.getConfiguration());
+      LanceNamespace namespace = (LanceNamespace) namespaceObject;
+      namespace.initialize(new HashMap<>(properties), allocator);
+      return namespace;
+    } catch (ClassNotFoundException e) {
+      throw new IllegalStateException(
+          "Lance HMS3 support requires the lance-namespace-hive3 runtime", e);
+    } catch (InvocationTargetException e) {
+      throw new IllegalStateException("Failed to initialize Lance HMS3 
namespace", e.getCause());
+    } catch (ReflectiveOperationException | LinkageError e) {
+      throw new IllegalStateException(
+          "Lance HMS3 namespace is unavailable with the current Hive runtime", 
e);
+    }
+  }
+
+  private static LanceNamespace withStorageOptions(
+      LanceNamespace delegate, LanceStorageOptionsProvider 
storageOptionsProvider) {
+    return (LanceNamespace)
+        Proxy.newProxyInstance(
+            LanceNamespace.class.getClassLoader(),
+            new Class<?>[] {LanceNamespace.class},
+            (proxy, method, args) -> {
+              try {
+                Object result = method.invoke(delegate, args);
+                if (result instanceof DescribeTableResponse) {
+                  DescribeTableResponse response = (DescribeTableResponse) 
result;
+                  String location = response.getLocation();
+                  Map<String, String> storageOptions = new HashMap<>();
+                  if (response.getStorageOptions() != null) {
+                    storageOptions.putAll(response.getStorageOptions());
+                  }
+                  
storageOptions.putAll(storageOptionsProvider.storageOptions(location));
+                  
response.setLocation(storageOptionsProvider.datasetLocation(location));
+                  response.setStorageOptions(storageOptions);
+                }
+                return result;
+              } catch (InvocationTargetException e) {
+                throw e.getCause();
+              }
+            });
+  }
+
+  private List<String> identifier(String... parts) {
+    List<String> identifier = new ArrayList<>(parts.length + 1);
+    identifier.add(catalogName);
+    Collections.addAll(identifier, parts);
+    return identifier;
+  }
+
+  private static String nextPageToken(String previous, String next) {
+    if (StringUtils.isBlank(next) || next.equals(previous)) {
+      return null;
+    }
+    return next;
+  }
+
+  private static UnsupportedOperationException readOnly() {
+    return new UnsupportedOperationException("Lance HMS3 catalog is 
read-only");
+  }
+}
diff --git 
a/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceStorageOptionsProvider.java
 
b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceStorageOptionsProvider.java
new file mode 100644
index 000000000..ceebaf3ba
--- /dev/null
+++ 
b/amoro-format-lance/src/main/java/org/apache/amoro/formats/lance/LanceStorageOptionsProvider.java
@@ -0,0 +1,205 @@
+/*
+ * 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.amoro.formats.lance;
+
+import org.apache.hadoop.conf.Configuration;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/** Converts Hadoop TOS configuration and credentials into Lance storage 
options. */
+class LanceStorageOptionsProvider {
+
+  static final String TOS_ENDPOINT = "fs.tos.endpoint";
+  static final String TOS_REGION = "fs.tos.region";
+  static final String VOLC_REGION = "fs.volc.openapi.region";
+  static final String TOS_ACCESS_KEY = "fs.tos.access-key-id";
+  static final String TOS_SECRET_KEY = "fs.tos.secret-access-key";
+  static final String TOS_SESSION_TOKEN = "fs.tos.session-token";
+  static final String TOS_CREDENTIAL_PROVIDER = "fs.tos.credentials.provider";
+  static final String TOS_CREDENTIAL_TTL = 
"fs.tos.credential.sts.token.time-to-live";
+
+  private static final long DEFAULT_CREDENTIAL_TTL_SECONDS = 
Duration.ofHours(1).getSeconds();
+  private static final long MAX_ADVERTISED_LIFETIME_MILLIS = 
Duration.ofMinutes(15).toMillis();
+
+  private final Configuration configuration;
+  private final CredentialLoader credentialLoader;
+
+  LanceStorageOptionsProvider(Configuration configuration) {
+    this(configuration, new ProtonCredentialLoader(configuration));
+  }
+
+  LanceStorageOptionsProvider(Configuration configuration, CredentialLoader 
credentialLoader) {
+    this.configuration = configuration;
+    this.credentialLoader = credentialLoader;
+  }
+
+  Map<String, String> storageOptions(String location) {
+    if (location == null || 
!"tos".equalsIgnoreCase(URI.create(location).getScheme())) {
+      return Collections.emptyMap();
+    }
+
+    Map<String, String> options = new HashMap<>();
+    putIfNotBlank(options, "endpoint", configuration.getTrimmed(TOS_ENDPOINT));
+    putIfNotBlank(
+        options,
+        "region",
+        firstNonBlank(configuration.getTrimmed(TOS_REGION), 
configuration.getTrimmed(VOLC_REGION)));
+
+    TemporaryCredential credential = credential(location);
+    if (credential != null) {
+      putIfNotBlank(options, "access_key_id", credential.accessKeyId);
+      putIfNotBlank(options, "secret_access_key", credential.secretAccessKey);
+      putIfNotBlank(options, "security_token", credential.sessionToken);
+      putRefreshOptions(options);
+    }
+    return options;
+  }
+
+  String datasetLocation(String location) {
+    // The ByteDance Lance runtime has a native tos:// object-store 
implementation backed by
+    // Proton. Keeping the HMS location intact selects that implementation; 
rewriting it to s3://
+    // would bypass Proton and route requests through the generic S3 client.
+    return location;
+  }
+
+  private TemporaryCredential credential(String location) {
+    if (configuration.getTrimmed(TOS_CREDENTIAL_PROVIDER) != null) {
+      return credentialLoader.load(bucket(location));
+    }
+
+    String accessKey = configuration.getTrimmed(TOS_ACCESS_KEY);
+    String secretKey = configuration.getTrimmed(TOS_SECRET_KEY);
+    if (accessKey == null || secretKey == null) {
+      return null;
+    }
+    return new TemporaryCredential(
+        accessKey, secretKey, configuration.getTrimmed(TOS_SESSION_TOKEN));
+  }
+
+  private void putRefreshOptions(Map<String, String> options) {
+    long configuredTtlSeconds =
+        configuration.getLong(TOS_CREDENTIAL_TTL, 
DEFAULT_CREDENTIAL_TTL_SECONDS);
+    long advertisedLifetime =
+        Math.max(
+            1_000L,
+            Math.min(
+                Duration.ofSeconds(configuredTtlSeconds).toMillis() / 2,
+                MAX_ADVERTISED_LIFETIME_MILLIS));
+    long refreshOffset = Math.max(1_000L, Math.min(60_000L, advertisedLifetime 
/ 5));
+    options.put(
+        "expires_at_millis", String.valueOf(System.currentTimeMillis() + 
advertisedLifetime));
+    options.put("refresh_offset_millis", String.valueOf(refreshOffset));
+  }
+
+  private static String bucket(String location) {
+    URI uri = URI.create(location);
+    return uri.getHost() == null ? uri.getAuthority() : uri.getHost();
+  }
+
+  private static String firstNonBlank(String first, String second) {
+    return first == null || first.isEmpty() ? second : first;
+  }
+
+  private static void putIfNotBlank(Map<String, String> options, String key, 
String value) {
+    if (value != null && !value.isEmpty()) {
+      options.put(key, value);
+    }
+  }
+
+  interface CredentialLoader {
+    TemporaryCredential load(String bucket);
+  }
+
+  static class TemporaryCredential {
+    private final String accessKeyId;
+    private final String secretAccessKey;
+    private final String sessionToken;
+
+    TemporaryCredential(String accessKeyId, String secretAccessKey, String 
sessionToken) {
+      this.accessKeyId = accessKeyId;
+      this.secretAccessKey = secretAccessKey;
+      this.sessionToken = sessionToken;
+    }
+  }
+
+  /** Uses Proton through reflection so the community Lance module has no 
Proton dependency. */
+  private static class ProtonCredentialLoader implements CredentialLoader {
+    private static final String CONF_CLASS = "io.proton.common.conf.Conf";
+    private static final String PROVIDER_FACTORY_CLASS =
+        "io.proton.common.object.auth.ProviderFactory";
+
+    private final Configuration configuration;
+    private final ConcurrentMap<String, Object> providers = new 
ConcurrentHashMap<>();
+
+    private ProtonCredentialLoader(Configuration configuration) {
+      this.configuration = configuration;
+    }
+
+    @Override
+    public TemporaryCredential load(String bucket) {
+      try {
+        Object provider = providers.computeIfAbsent(bucket, 
this::createProvider);
+        Object credential = 
provider.getClass().getMethod("expirableCredential").invoke(provider);
+        return new TemporaryCredential(
+            invokeString(credential, "accessKeyId"),
+            invokeString(credential, "accessKeySecret"),
+            invokeString(credential, "sessionToken"));
+      } catch (InvocationTargetException e) {
+        throw credentialFailure(bucket, e.getCause());
+      } catch (ReflectiveOperationException | RuntimeException e) {
+        throw credentialFailure(bucket, e);
+      }
+    }
+
+    private Object createProvider(String bucket) {
+      try {
+        Class<?> confClass = Class.forName(CONF_CLASS);
+        Object protonConf =
+            confClass.getMethod("copyOf", Iterable.class).invoke(null, 
configuration);
+        Class<?> factoryClass = Class.forName(PROVIDER_FACTORY_CLASS);
+        Method createProvider =
+            factoryClass.getMethod("createProvider", confClass, String.class, 
String.class);
+        return createProvider.invoke(null, protonConf, bucket, "tos");
+      } catch (InvocationTargetException e) {
+        throw credentialFailure(bucket, e.getCause());
+      } catch (ReflectiveOperationException e) {
+        throw credentialFailure(bucket, e);
+      }
+    }
+
+    private static String invokeString(Object target, String method)
+        throws ReflectiveOperationException {
+      Object value = target.getClass().getMethod(method).invoke(target);
+      return value == null ? null : value.toString();
+    }
+
+    private static IllegalStateException credentialFailure(String bucket, 
Throwable cause) {
+      return new IllegalStateException(
+          "Failed to obtain temporary TOS credentials for bucket " + bucket, 
cause);
+    }
+  }
+}
diff --git 
a/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceCatalogFactory.java
 
b/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceCatalogFactory.java
new file mode 100644
index 000000000..32ccb87c3
--- /dev/null
+++ 
b/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceCatalogFactory.java
@@ -0,0 +1,67 @@
+/*
+ * 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.amoro.formats.lance;
+
+import org.apache.amoro.FormatCatalog;
+import org.apache.amoro.properties.CatalogMetaProperties;
+import org.apache.amoro.table.TableMetaStore;
+import org.apache.hadoop.conf.Configuration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class TestLanceCatalogFactory {
+
+  @Test
+  public void testHiveMetastoreUsesHms3Catalog() throws Exception {
+    Configuration configuration = new Configuration(false);
+    configuration.set("hive.metastore.uris", "thrift://127.0.0.1:1");
+    TableMetaStore metaStore = 
TableMetaStore.builder().withConfiguration(configuration).build();
+
+    FormatCatalog catalog =
+        new LanceCatalogFactory()
+            .create(
+                "tenant@catalog",
+                CatalogMetaProperties.CATALOG_TYPE_HIVE,
+                Collections.emptyMap(),
+                metaStore);
+
+    Assertions.assertInstanceOf(LanceHms3Catalog.class, catalog);
+    ((AutoCloseable) catalog).close();
+  }
+
+  @Test
+  public void testFilesystemMetastoreKeepsDirectoryCatalog() {
+    Map<String, String> properties = new HashMap<>();
+    properties.put(CatalogMetaProperties.KEY_WAREHOUSE, 
"file:/tmp/lance-catalog");
+
+    FormatCatalog catalog =
+        new LanceCatalogFactory()
+            .create(
+                "filesystem-catalog",
+                CatalogMetaProperties.CATALOG_TYPE_FILESYSTEM,
+                properties,
+                TableMetaStore.EMPTY);
+
+    Assertions.assertInstanceOf(LanceDirectoryV1Catalog.class, catalog);
+  }
+}
diff --git 
a/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceHms3Catalog.java
 
b/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceHms3Catalog.java
new file mode 100644
index 000000000..6af85b496
--- /dev/null
+++ 
b/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceHms3Catalog.java
@@ -0,0 +1,144 @@
+/*
+ * 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.amoro.formats.lance;
+
+import org.apache.amoro.table.TableMetaStore;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.hadoop.conf.Configuration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.lance.namespace.LanceNamespace;
+import org.lance.namespace.errors.NamespaceNotFoundException;
+import org.lance.namespace.errors.TableNotFoundException;
+import org.lance.namespace.model.ListNamespacesRequest;
+import org.lance.namespace.model.ListNamespacesResponse;
+import org.lance.namespace.model.ListTablesRequest;
+import org.lance.namespace.model.ListTablesResponse;
+import org.lance.namespace.model.NamespaceExistsRequest;
+import org.lance.namespace.model.TableExistsRequest;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+public class TestLanceHms3Catalog {
+
+  @Test
+  public void testPhysicalCatalogScopesDatabaseAndTableRequests() {
+    TestingNamespace namespace = new TestingNamespace();
+    TableMetaStore metaStore = metaStore();
+    try (LanceHms3Catalog catalog =
+        new LanceHms3Catalog(
+            "tenant@catalog",
+            metaStore,
+            new RootAllocator(Long.MAX_VALUE),
+            namespace,
+            new LanceStorageOptionsProvider(metaStore.getConfiguration()))) {
+      Assertions.assertEquals(Arrays.asList("db_a", "db_b"), 
catalog.listDatabases());
+      Assertions.assertEquals(Arrays.asList("table_a", "table_b"), 
catalog.listTables("db_a"));
+      Assertions.assertTrue(catalog.databaseExists("db_a"));
+      Assertions.assertFalse(catalog.databaseExists("missing"));
+      Assertions.assertTrue(catalog.tableExists("db_a", "table_a"));
+      Assertions.assertFalse(catalog.tableExists("db_a", "missing"));
+
+      Assertions.assertEquals(Collections.singletonList("tenant@catalog"), 
namespace.databaseId);
+      Assertions.assertEquals(Arrays.asList("tenant@catalog", "db_a"), 
namespace.tableListId);
+      Assertions.assertTrue(namespace.includeDeclared);
+      Assertions.assertEquals(
+          Arrays.asList("tenant@catalog", "db_a", "missing"), 
namespace.tableExistsId);
+    }
+    Assertions.assertTrue(namespace.closed);
+  }
+
+  @Test
+  public void testMutationsAreRejected() {
+    TestingNamespace namespace = new TestingNamespace();
+    TableMetaStore metaStore = metaStore();
+    try (LanceHms3Catalog catalog =
+        new LanceHms3Catalog(
+            "tenant@catalog",
+            metaStore,
+            new RootAllocator(Long.MAX_VALUE),
+            namespace,
+            new LanceStorageOptionsProvider(metaStore.getConfiguration()))) {
+      Assertions.assertThrows(
+          UnsupportedOperationException.class, () -> 
catalog.createDatabase("db"));
+      Assertions.assertThrows(
+          UnsupportedOperationException.class, () -> 
catalog.dropDatabase("db"));
+      Assertions.assertThrows(
+          UnsupportedOperationException.class, () -> catalog.dropTable("db", 
"table", false));
+    }
+  }
+
+  private static TableMetaStore metaStore() {
+    return TableMetaStore.builder().withConfiguration(new 
Configuration(false)).build();
+  }
+
+  private static class TestingNamespace implements LanceNamespace, 
AutoCloseable {
+    private List<String> databaseId;
+    private List<String> tableListId;
+    private List<String> tableExistsId;
+    private boolean includeDeclared;
+    private boolean closed;
+
+    @Override
+    public void initialize(
+        Map<String, String> properties, 
org.apache.arrow.memory.BufferAllocator allocator) {}
+
+    @Override
+    public String namespaceId() {
+      return "test";
+    }
+
+    @Override
+    public ListNamespacesResponse listNamespaces(ListNamespacesRequest 
request) {
+      databaseId = request.getId();
+      return new ListNamespacesResponse().namespaces(new 
HashSet<>(Arrays.asList("db_b", "db_a")));
+    }
+
+    @Override
+    public void namespaceExists(NamespaceExistsRequest request) {
+      if (request.getId().contains("missing")) {
+        throw new NamespaceNotFoundException("missing");
+      }
+    }
+
+    @Override
+    public ListTablesResponse listTables(ListTablesRequest request) {
+      tableListId = request.getId();
+      includeDeclared = Boolean.TRUE.equals(request.getIncludeDeclared());
+      return new ListTablesResponse().tables(new 
HashSet<>(Arrays.asList("table_b", "table_a")));
+    }
+
+    @Override
+    public void tableExists(TableExistsRequest request) {
+      tableExistsId = request.getId();
+      if (request.getId().contains("missing")) {
+        throw new TableNotFoundException("missing");
+      }
+    }
+
+    @Override
+    public void close() {
+      closed = true;
+    }
+  }
+}
diff --git 
a/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceHms3CatalogIntegration.java
 
b/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceHms3CatalogIntegration.java
new file mode 100644
index 000000000..e7af47f84
--- /dev/null
+++ 
b/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceHms3CatalogIntegration.java
@@ -0,0 +1,53 @@
+/*
+ * 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.amoro.formats.lance;
+
+import org.apache.amoro.table.TableMetaStore;
+import org.apache.hadoop.conf.Configuration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+/** Opt-in smoke test for a real Hive 3 metastore. */
+public class TestLanceHms3CatalogIntegration {
+
+  @Test
+  public void testRealHmsMetadata() {
+    String hmsUri = System.getProperty("lance.hms.uri");
+    String catalogName = System.getProperty("lance.hms.catalog");
+    String database = System.getProperty("lance.hms.database");
+    String table = System.getProperty("lance.hms.table");
+    Assumptions.assumeTrue(
+        hmsUri != null && catalogName != null && database != null && table != 
null,
+        "Real HMS coordinates were not provided");
+
+    Configuration configuration = new Configuration(false);
+    configuration.set("hive.metastore.uris", hmsUri);
+    TableMetaStore metaStore = 
TableMetaStore.builder().withConfiguration(configuration).build();
+
+    try (LanceHms3Catalog catalog =
+        new LanceHms3Catalog(catalogName, Collections.emptyMap(), metaStore)) {
+      Assertions.assertTrue(catalog.listDatabases().contains(database));
+      Assertions.assertTrue(catalog.tableExists(database, table));
+      Assertions.assertTrue(catalog.listTables(database).contains(table));
+    }
+  }
+}
diff --git 
a/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceStorageOptionsProvider.java
 
b/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceStorageOptionsProvider.java
new file mode 100644
index 000000000..a06557d81
--- /dev/null
+++ 
b/amoro-format-lance/src/test/java/org/apache/amoro/formats/lance/TestLanceStorageOptionsProvider.java
@@ -0,0 +1,91 @@
+/*
+ * 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.amoro.formats.lance;
+
+import org.apache.hadoop.conf.Configuration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class TestLanceStorageOptionsProvider {
+
+  @Test
+  public void testTemporaryCredentialsAreMappedForTos() {
+    Configuration configuration = new Configuration(false);
+    configuration.set(LanceStorageOptionsProvider.TOS_ENDPOINT, 
"https://tos.example.com";);
+    configuration.set(LanceStorageOptionsProvider.VOLC_REGION, "cn-test");
+    configuration.set(LanceStorageOptionsProvider.TOS_CREDENTIAL_PROVIDER, 
"test-provider");
+    configuration.setLong(LanceStorageOptionsProvider.TOS_CREDENTIAL_TTL, 
3_600L);
+    AtomicReference<String> requestedBucket = new AtomicReference<>();
+
+    LanceStorageOptionsProvider provider =
+        new LanceStorageOptionsProvider(
+            configuration,
+            bucket -> {
+              requestedBucket.set(bucket);
+              return new LanceStorageOptionsProvider.TemporaryCredential("ak", 
"sk", "token");
+            });
+
+    long before = System.currentTimeMillis();
+    Map<String, String> options = 
provider.storageOptions("tos://test-bucket/path/table.lance");
+
+    Assertions.assertEquals("test-bucket", requestedBucket.get());
+    Assertions.assertEquals("https://tos.example.com";, 
options.get("endpoint"));
+    Assertions.assertEquals("cn-test", options.get("region"));
+    Assertions.assertEquals("ak", options.get("access_key_id"));
+    Assertions.assertEquals("sk", options.get("secret_access_key"));
+    Assertions.assertEquals("token", options.get("security_token"));
+    Assertions.assertTrue(Long.parseLong(options.get("expires_at_millis")) > 
before);
+    Assertions.assertTrue(Long.parseLong(options.get("refresh_offset_millis")) 
> 0);
+    Assertions.assertEquals(
+        "tos://test-bucket/path/table.lance",
+        provider.datasetLocation("tos://test-bucket/path/table.lance"));
+  }
+
+  @Test
+  public void testStaticCredentialsRemainSupported() {
+    Configuration configuration = new Configuration(false);
+    configuration.set(LanceStorageOptionsProvider.TOS_ACCESS_KEY, "static-ak");
+    configuration.set(LanceStorageOptionsProvider.TOS_SECRET_KEY, "static-sk");
+
+    Map<String, String> options =
+        new LanceStorageOptionsProvider(configuration)
+            .storageOptions("tos://test-bucket/table.lance");
+
+    Assertions.assertEquals("static-ak", options.get("access_key_id"));
+    Assertions.assertEquals("static-sk", options.get("secret_access_key"));
+  }
+
+  @Test
+  public void testNonTosLocationHasNoOptions() {
+    Configuration configuration = new Configuration(false);
+    configuration.set(LanceStorageOptionsProvider.TOS_ACCESS_KEY, "ak");
+    configuration.set(LanceStorageOptionsProvider.TOS_SECRET_KEY, "sk");
+
+    Assertions.assertTrue(
+        new LanceStorageOptionsProvider(configuration)
+            .storageOptions("s3://bucket/table.lance")
+            .isEmpty());
+    Assertions.assertEquals(
+        "s3://bucket/table.lance",
+        new 
LanceStorageOptionsProvider(configuration).datasetLocation("s3://bucket/table.lance"));
+  }
+}
diff --git 
a/amoro-format-paimon/src/main/java/org/apache/amoro/formats/paimon/PaimonCatalogFactory.java
 
b/amoro-format-paimon/src/main/java/org/apache/amoro/formats/paimon/PaimonCatalogFactory.java
index 8c9b4bbba..6cfe9dfbb 100644
--- 
a/amoro-format-paimon/src/main/java/org/apache/amoro/formats/paimon/PaimonCatalogFactory.java
+++ 
b/amoro-format-paimon/src/main/java/org/apache/amoro/formats/paimon/PaimonCatalogFactory.java
@@ -36,6 +36,7 @@ import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.net.URL;
+import java.util.Arrays;
 import java.util.Map;
 import java.util.Optional;
 
@@ -48,6 +49,8 @@ public class PaimonCatalogFactory implements 
FormatCatalogFactory {
   public static final String PAIMON_OSS_ACCESS_KEY = "fs.oss.accessKeyId";
   public static final String PAIMON_OSS_SECRET_KEY = "fs.oss.accessKeySecret";
   public static final String PAIMON_OSS_ENDPOINT = "fs.oss.endpoint";
+  static final String HMS_DEFAULT_CATALOG = "metastore.catalog.default";
+  static final String HMS_DEFAULT_CATALOG_CACHE_KEY = "conf:" + 
HMS_DEFAULT_CATALOG;
 
   @Override
   public PaimonCatalog create(
@@ -57,6 +60,7 @@ public class PaimonCatalogFactory implements 
FormatCatalogFactory {
     // if format table enabled, paimon will load hive orc/parquet/csv table to 
paimon table
     catalogProperties.put(CatalogOptions.FORMAT_TABLE_ENABLED.key(), "false");
     catalogProperties.putAll(properties);
+    configureHiveClientPoolCache(metastoreType, catalogProperties, 
metaStore.getConfiguration());
     hiveSiteLocation.ifPresent(
         url ->
             catalogProperties.put(
@@ -88,6 +92,30 @@ public class PaimonCatalogFactory implements 
FormatCatalogFactory {
     return CatalogFactory.createCatalog(catalogContext);
   }
 
+  static void configureHiveClientPoolCache(
+      String metastoreType, Map<String, String> catalogProperties, 
Configuration configuration) {
+    if 
(!CatalogMetaProperties.CATALOG_TYPE_HIVE.equalsIgnoreCase(metastoreType)
+        || configuration.get(HMS_DEFAULT_CATALOG) == null) {
+      return;
+    }
+
+    String cacheKeys = 
catalogProperties.get(HiveCatalogOptions.CLIENT_POOL_CACHE_KEYS.key());
+    boolean alreadyConfigured =
+        cacheKeys != null
+            && Arrays.stream(cacheKeys.split(","))
+                .map(String::trim)
+                .anyMatch(HMS_DEFAULT_CATALOG_CACHE_KEY::equalsIgnoreCase);
+    if (alreadyConfigured) {
+      return;
+    }
+
+    catalogProperties.put(
+        HiveCatalogOptions.CLIENT_POOL_CACHE_KEYS.key(),
+        cacheKeys == null || cacheKeys.trim().isEmpty()
+            ? HMS_DEFAULT_CATALOG_CACHE_KEY
+            : cacheKeys + "," + HMS_DEFAULT_CATALOG_CACHE_KEY);
+  }
+
   @Override
   public TableFormat format() {
     return TableFormat.PAIMON;
diff --git 
a/amoro-format-paimon/src/test/java/org/apache/amoro/formats/paimon/TestPaimonCatalogFactory.java
 
b/amoro-format-paimon/src/test/java/org/apache/amoro/formats/paimon/TestPaimonCatalogFactory.java
new file mode 100644
index 000000000..886dd2568
--- /dev/null
+++ 
b/amoro-format-paimon/src/test/java/org/apache/amoro/formats/paimon/TestPaimonCatalogFactory.java
@@ -0,0 +1,74 @@
+/*
+ * 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.amoro.formats.paimon;
+
+import org.apache.amoro.properties.CatalogMetaProperties;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.paimon.hive.HiveCatalogOptions;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class TestPaimonCatalogFactory {
+
+  @Test
+  public void testHmsDefaultCatalogIsIncludedInClientPoolCacheKey() {
+    Configuration configuration = new Configuration(false);
+    configuration.set(PaimonCatalogFactory.HMS_DEFAULT_CATALOG, 
"tenant@catalog");
+    Map<String, String> properties = new HashMap<>();
+
+    PaimonCatalogFactory.configureHiveClientPoolCache(
+        CatalogMetaProperties.CATALOG_TYPE_HIVE, properties, configuration);
+
+    Assert.assertEquals(
+        PaimonCatalogFactory.HMS_DEFAULT_CATALOG_CACHE_KEY,
+        properties.get(HiveCatalogOptions.CLIENT_POOL_CACHE_KEYS.key()));
+  }
+
+  @Test
+  public void testExistingClientPoolCacheKeysArePreserved() {
+    Configuration configuration = new Configuration(false);
+    configuration.set(PaimonCatalogFactory.HMS_DEFAULT_CATALOG, 
"tenant@catalog");
+    Map<String, String> properties = new HashMap<>();
+    properties.put(HiveCatalogOptions.CLIENT_POOL_CACHE_KEYS.key(), "ugi");
+
+    PaimonCatalogFactory.configureHiveClientPoolCache(
+        CatalogMetaProperties.CATALOG_TYPE_HIVE, properties, configuration);
+    PaimonCatalogFactory.configureHiveClientPoolCache(
+        CatalogMetaProperties.CATALOG_TYPE_HIVE, properties, configuration);
+
+    Assert.assertEquals(
+        "ugi," + PaimonCatalogFactory.HMS_DEFAULT_CATALOG_CACHE_KEY,
+        properties.get(HiveCatalogOptions.CLIENT_POOL_CACHE_KEYS.key()));
+  }
+
+  @Test
+  public void testNonHiveCatalogIsUnchanged() {
+    Configuration configuration = new Configuration(false);
+    configuration.set(PaimonCatalogFactory.HMS_DEFAULT_CATALOG, 
"tenant@catalog");
+    Map<String, String> properties = new HashMap<>();
+
+    PaimonCatalogFactory.configureHiveClientPoolCache(
+        CatalogMetaProperties.CATALOG_TYPE_HADOOP, properties, configuration);
+
+    
Assert.assertFalse(properties.containsKey(HiveCatalogOptions.CLIENT_POOL_CACHE_KEYS.key()));
+  }
+}
diff --git a/amoro-web/mock/modules/catalogs.js 
b/amoro-web/mock/modules/catalogs.js
index 79195ae47..178d3d808 100644
--- a/amoro-web/mock/modules/catalogs.js
+++ b/amoro-web/mock/modules/catalogs.js
@@ -17,6 +17,15 @@
   */
 
 export default [
+  {
+    url: '/mock/api/ams/v1/namespaces',
+    method: 'get',
+    response: () => ({
+      "message": "success",
+      "code": 200,
+      "result": ["123456789", "987654321"]
+    }),
+  },
   {
     url: '/mock/api/ams/v1/catalogs',
     method: 'get',
@@ -25,7 +34,7 @@ export default [
       "code": 200,
       "result": [
         {
-          "catalogName": "test_catalog",
+          "catalogName": "123456789@las",
           "catalogType": "hadoop",
           "storageConfigs": {
             "storage.type": "Hadoop",
@@ -62,17 +71,17 @@ export default [
   },
 
   {
-    url: '/mock/api/ams/v1/catalogs/test_catalog/databases',
+    url: '/mock/api/ams/v1/catalogs/123456789@las/databases',
     method: 'get',
     response: () => {
-      return { "message": "success", "code": 200, "result": ["db", "test", 
"acc"] }
+      return { "message": "success", "code": 200, "result": ["warehouse", 
"analytics", "index_demo"] }
     },
   },
   {
-    url: '/mock/api/ams/v1/catalogs/test_catalog/databases/db/tables',
+    url: '/mock/api/ams/v1/catalogs/123456789@las/databases/warehouse/tables',
     method: 'get',
     response: () => {
-      return { "message": "success", "code": 200, "result": [{ "name": "user", 
"type": "ICEBERG" },{ "name": "wf", "type": "ICEBERG" }, { "name": "xcvz", 
"type": "ICEBERG" }] };
+      return { "message": "success", "code": 200, "result": [{ "name": 
"jk_large_table", "type": "ICEBERG" }, { "name": "orders", "type": "ICEBERG" }, 
{ "name": "events", "type": "PAIMON" }] };
     },
   },
   {
@@ -160,6 +169,24 @@ export default [
       ]
     }),
   },
+  {
+    url: '/mock/api/ams/v1/catalogs/metastore/:type/table-formats',
+    method: 'get',
+    response: () => ({
+      "message": "success",
+      "code": 200,
+      "result": ["ICEBERG", "PAIMON"]
+    }),
+  },
+  {
+    url: '/mock/api/ams/v1/catalogs/metastore/:type/storage-types',
+    method: 'get',
+    response: () => ({
+      "message": "success",
+      "code": 200,
+      "result": ["Hadoop"]
+    }),
+  },
   {
     url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/optimizing-processes/:processesId/tasks',
     method: 'get',
diff --git a/amoro-web/mock/modules/common.js b/amoro-web/mock/modules/common.js
index 8a2267fd2..1c04794a5 100644
--- a/amoro-web/mock/modules/common.js
+++ b/amoro-web/mock/modules/common.js
@@ -27,7 +27,8 @@ export default [
         "userName": "admin",
         "loginTime": "1703839452053",
         "role": "SERVICE_ADMIN",
-        "roles": ["SERVICE_ADMIN"]
+        "roles": ["SERVICE_ADMIN"],
+        "privileges": ["VIEW_SYSTEM", "VIEW_CATALOG", "VIEW_TABLE", 
"VIEW_OPTIMIZER", "MANAGE_CATALOG", "MANAGE_TABLE", "MANAGE_OPTIMIZER", 
"EXECUTE_SQL", "MANAGE_PLATFORM"]
       }
     }),
   },
@@ -41,7 +42,8 @@ export default [
         userName: 'admin',
         loginTime: '1703839452053',
         role: 'SERVICE_ADMIN',
-        roles: ['SERVICE_ADMIN']
+        roles: ['SERVICE_ADMIN'],
+        privileges: ['VIEW_SYSTEM', 'VIEW_CATALOG', 'VIEW_TABLE', 
'VIEW_OPTIMIZER', 'MANAGE_CATALOG', 'MANAGE_TABLE', 'MANAGE_OPTIMIZER', 
'EXECUTE_SQL', 'MANAGE_PLATFORM']
       }
     }),
   },
diff --git a/amoro-web/mock/modules/table.js b/amoro-web/mock/modules/table.js
index 119cf7b3f..fbaf68bdd 100644
--- a/amoro-web/mock/modules/table.js
+++ b/amoro-web/mock/modules/table.js
@@ -18,17 +18,17 @@
 
 export default [
   {
-    url: 
'/mock/api/ams/v1/tables/catalogs/test_catalog/dbs/db/tables/user/details',
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/details',
     method: 'get',
-    response: () => ({
+    response: ({ params }) => ({
       "message": "success",
       "code": 200,
       "result": {
         "tableType": "ICEBERG",
         "tableIdentifier": {
-          "catalog": "test_catalog",
-          "database": "db",
-          "tableName": "user"
+          "catalog": params.catalog,
+          "database": params.dbId,
+          "tableName": params.tableName
         },
         "schema": [
           {
@@ -91,7 +91,7 @@ export default [
     }),
   },
   {
-    url: 
'/mock/api/ams/v1/tables/catalogs/test_catalog/dbs/db/tables/user/partitions',
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/partitions',
     method: 'get',
     response: () => ({
       "message": "success",
@@ -120,7 +120,7 @@ export default [
     }),
   },
   {
-    url: 
'/mock/api/ams/v1/tables/catalogs/test_catalog/dbs/db/tables/user/branches',
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/branches',
     method: 'get',
     response: () => ({
       "message": "success",
@@ -141,12 +141,17 @@ export default [
     }),
   },
   {
-    url: 
'/mock/api/ams/v1/tables/catalogs/test_catalog/dbs/db/tables/user/tags',
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/tags',
+    method: 'get',
+    response: () => ({ "message": "success", "code": 200, "result": { "list": 
[], "total": 0 } }),
+  },
+  {
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/consumers',
     method: 'get',
     response: () => ({ "message": "success", "code": 200, "result": { "list": 
[], "total": 0 } }),
   },
   {
-    url: 
'/mock/api/ams/v1/tables/catalogs/test_catalog/dbs/db/tables/user/snapshots',
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/snapshots',
     method: 'get',
     response: () => ({
       "message": "success",
@@ -193,12 +198,12 @@ export default [
     }),
   },
   {
-    url: 
'/mock/api/ams/v1/tables/catalogs/test_catalog/dbs/db/tables/user/operations',
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/operations',
     method: 'get',
     response: () => ({ "message": "success", "code": 200, "result": { "list": 
[], "total": 0 } }),
   },
   {
-    url: 
'/mock/api/ams/v1/tables/catalogs/test_catalog/dbs/db/tables/user/partitions/:filter/files',
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/partitions/:filter/files',
     method: 'get',
     response: () => ({
       "message": "success",
@@ -223,7 +228,7 @@ export default [
     }),
   },
   {
-    url: 
'/mock/api/ams/v1/tables/catalogs/test_catalog/dbs/db/tables/user/snapshots/:snapshotId/detail',
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/snapshots/:snapshotId/detail',
     method: 'get',
     response: () => ({
       "message": "success",
@@ -260,7 +265,7 @@ export default [
     }),
   },
   {
-    url: 
'/mock/api/ams/v1/tables/catalogs/test_catalog/dbs/db/tables/user/optimizing-processes',
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/optimizing-processes',
     method: 'get',
     response: () => ({
       "message": "success",
@@ -307,7 +312,7 @@ export default [
     }),
   },
   {
-    url: 
'/mock/api/ams/v1/tables/catalogs/test_catalog/dbs/db/tables/user/process-types',
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/process-types',
     method: 'get',
     response: ({ query }) => {
       const processCategory = query?.processCategory || 'OPTIMIZING'
@@ -335,7 +340,7 @@ export default [
     },
   },
   {
-    url: 
'/mock/api/ams/v1/tables/catalogs/test_catalog/dbs/db/tables/user/operations',
+    url: 
'/mock/api/ams/v1/tables/catalogs/:catalog/dbs/:dbId/tables/:tableName/operations',
     method: 'get',
     response: () => ({
       "message": "success",
diff --git a/amoro-web/src/components/Sidebar.vue 
b/amoro-web/src/components/Sidebar.vue
index 058645e78..64335f7d3 100644
--- a/amoro-web/src/components/Sidebar.vue
+++ b/amoro-web/src/components/Sidebar.vue
@@ -147,6 +147,7 @@ export default defineComponent({
         let db: string | undefined
         let tableName: string | undefined
         let namespace: string | undefined
+        let type: string | undefined
 
         try {
           const stored = localStorage.getItem('easylake-menu-catalog-db-table')
@@ -156,11 +157,13 @@ export default defineComponent({
               catalog?: string
               database?: string
               tableName?: string
+              type?: string
             }
             namespace = parsed.namespace
             catalog = parsed.catalog
             db = parsed.database
             tableName = parsed.tableName
+            type = parsed.type
           }
         }
         catch (e) {
@@ -175,6 +178,7 @@ export default defineComponent({
               catalog,
               db,
               table: tableName,
+              ...(type ? { type } : {}),
             },
           })
         }
diff --git a/amoro-web/src/views/catalogs/index.vue 
b/amoro-web/src/views/catalogs/index.vue
index 743d12313..bf843c187 100644
--- a/amoro-web/src/views/catalogs/index.vue
+++ b/amoro-web/src/views/catalogs/index.vue
@@ -65,7 +65,7 @@ function initSelectCatalog() {
     catalogName: '',
     catalogType: '',
   }
-  if (decodeURIComponent(catalogname as string) === NEW_CATALOG) {
+  if (catalogname === NEW_CATALOG) {
     addCatalog()
     return
   }
@@ -98,7 +98,7 @@ async function selectCatalog(item: ICatalogItem) {
   await router.replace({
     path: '/catalogs',
     query: {
-      catalogname: encodeURIComponent(curCatalog.catalogName),
+      catalogname: curCatalog.catalogName,
       type: curCatalog.catalogType,
     },
   })
@@ -138,7 +138,7 @@ function addCatalog() {
     addNewCatalog()
   }
 }
-  async function addNewCatalog() {
+async function addNewCatalog() {
   const item: ICatalogItem = {
     catalogName: NEW_CATALOG,
     catalogType: '',
@@ -174,7 +174,7 @@ onBeforeRouteLeave((_to, _form, next) => {
 </script>
 
 <template>
-  <div class="page-scroll" ref="pageScrollRef">
+  <div ref="pageScrollRef" class="page-scroll">
     <div class="catalogs-wrap g-flex">
       <div class="catalog-list-left">
         <div class="catalog-header">
@@ -202,12 +202,9 @@ onBeforeRouteLeave((_to, _form, next) => {
   height: 100%;
   overflow-y: auto;
 }
-
-
-
- .catalogs-wrap {
-   height: 100%;
-   padding: 16px 24px;
+.catalogs-wrap {
+  height: 100%;
+  padding: 16px 24px;
   .catalog-list-left {
     width: 200px;
     height: 100%;
diff --git a/amoro-web/src/views/hive-details/index.vue 
b/amoro-web/src/views/hive-details/index.vue
index a23781ab1..542554fc8 100644
--- a/amoro-web/src/views/hive-details/index.vue
+++ b/amoro-web/src/views/hive-details/index.vue
@@ -153,7 +153,7 @@ export default defineComponent({
       () => route.query,
       (val, old) => {
         const { catalog, db, table } = val
-        if (route.path === '/hive-tables' && (catalog !== old.catalog || db 
!== old.db || table !== old.table)) {
+        if (!isSecondaryNav.value && (catalog !== old.catalog || db !== old.db 
|| table !== old.table)) {
           init()
         }
       },
@@ -164,6 +164,15 @@ export default defineComponent({
     })
 
     onMounted(() => {
+      if (route.path === '/hive-tables') {
+        router.replace({
+          path: '/tables',
+          query: {
+            ...route.query,
+          },
+        })
+        return
+      }
       init()
     })
 
diff --git a/amoro-web/src/views/tables/components/TableExplorer.vue 
b/amoro-web/src/views/tables/components/TableExplorer.vue
index f21946c9b..77546ef0b 100755
--- a/amoro-web/src/views/tables/components/TableExplorer.vue
+++ b/amoro-web/src/views/tables/components/TableExplorer.vue
@@ -41,6 +41,7 @@ interface TreeNode {
   db?: string
   table?: string
   tableType?: string
+  catalogNamespace?: string
 }
 
 const router = useRouter()
@@ -48,6 +49,7 @@ const route = useRoute()
 
 const storageTableKey = 'easylake-menu-catalog-db-table'
 const expandedKeysSessionKeyPrefix = 'tables_expanded_keys'
+const allNamespaceValue = '__all__'
 
 const state = reactive({
   loading: false,
@@ -58,6 +60,7 @@ const state = reactive({
   selectedKeys: [] as string[],
   namespaces: [] as string[],
   selectedNamespace: '',
+  selectedFormat: '',
   namespaceMode: false,
   // Cache
   catalogList: [] as string[],
@@ -65,6 +68,13 @@ const state = reactive({
   tablesByCatalogDb: {} as Record<string, TableItem[]>,
 })
 
+const formatOptions = [
+  { label: 'Iceberg', value: 'ICEBERG' },
+  { label: 'Paimon', value: 'PAIMON' },
+  { label: 'Hive', value: 'HIVE' },
+  { label: 'Hudi', value: 'HUDI' },
+]
+
 let namespaceGeneration = 0
 let initialized = false
 
@@ -72,21 +82,35 @@ function expandedKeysSessionKey() {
   return `${expandedKeysSessionKeyPrefix}:${state.selectedNamespace || 
'default'}`
 }
 
-function catalogDisplayName(catalog: string) {
+function catalogPresentation(catalog: string) {
+  if (state.namespaceMode && state.selectedNamespace === allNamespaceValue) {
+    const separatorIndex = catalog.indexOf('@')
+    if (separatorIndex > 0 && separatorIndex < catalog.length - 1) {
+      return {
+        title: catalog.slice(separatorIndex + 1),
+        namespace: catalog.slice(0, separatorIndex),
+      }
+    }
+  }
+
   if (!state.namespaceMode || !state.selectedNamespace) {
-    return catalog
+    return { title: catalog }
   }
   const prefix = `${state.selectedNamespace}@`
-  return catalog.startsWith(prefix) ? catalog.slice(prefix.length) : catalog
+  return {
+    title: catalog.startsWith(prefix) ? catalog.slice(prefix.length) : catalog,
+  }
 }
 
 function buildCatalogNode(catalog: string): TreeNode {
+  const presentation = catalogPresentation(catalog)
   return {
     key: `catalog:${catalog}`,
-    title: catalogDisplayName(catalog),
+    title: presentation.title,
     isLeaf: false,
     nodeType: 'catalog',
     catalog,
+    catalogNamespace: presentation.namespace,
   }
 }
 
@@ -140,7 +164,10 @@ async function initRootCatalogs() {
   const generation = namespaceGeneration
   state.loading = true
   try {
-    const res = await getCatalogList(state.namespaceMode ? 
state.selectedNamespace : undefined)
+    const namespace = state.namespaceMode && state.selectedNamespace !== 
allNamespaceValue
+      ? state.selectedNamespace
+      : undefined
+    const res = await getCatalogList(namespace)
     if (generation !== namespaceGeneration) {
       return
     }
@@ -160,6 +187,7 @@ function clearExplorerState() {
   state.loading = false
   state.searchKey = ''
   state.filterKey = ''
+  state.selectedFormat = ''
   state.treeData = []
   state.expandedKeys = []
   state.selectedKeys = []
@@ -168,6 +196,10 @@ function clearExplorerState() {
   state.tablesByCatalogDb = {}
 }
 
+function handleFormatChange(format?: string) {
+  state.selectedFormat = format || ''
+}
+
 async function loadChildren(node: any) {
   const data = node?.dataRef || node
   if (!data) {
@@ -286,13 +318,13 @@ function handleSelectTable(catalog: string, db: string, 
tableName: string, table
     catalog,
     database: db,
     tableName,
+    type,
   })
   localStorage.setItem(`${storageTableKey}:${namespace}`, storedSelection)
   localStorage.setItem(storageTableKey, storedSelection)
 
-  const path = type === 'HIVE' ? '/hive-tables' : '/tables'
   const pathQuery = {
-    path,
+    path: '/tables',
     query: {
       ...(state.namespaceMode ? { namespace: state.selectedNamespace } : {}),
       catalog,
@@ -399,7 +431,10 @@ function filterBySingleKeyword(nodes: TreeNode[], keyword: 
string, expandedSet:
   const result: TreeNode[] = []
 
   nodes.forEach((node) => {
-    const titleMatch = node.title.toLowerCase().includes(keyword)
+    const searchText = node.catalogNamespace
+      ? `${node.title} @${node.catalogNamespace}`
+      : node.title
+    const titleMatch = searchText.toLowerCase().includes(keyword)
     let childrenMatches: TreeNode[] = []
 
     if (node.children && node.children.length) {
@@ -427,7 +462,10 @@ function filterByHierarchical(nodes: TreeNode[], parts: 
string[], expandedSet: S
     if (catalogNode.nodeType !== 'catalog') {
       return
     }
-    if (!catalogNode.title.toLowerCase().includes(catalogPart)) {
+    const catalogSearchText = catalogNode.catalogNamespace
+      ? `${catalogNode.title} @${catalogNode.catalogNamespace}`
+      : catalogNode.title
+    if (!catalogSearchText.toLowerCase().includes(catalogPart)) {
       return
     }
 
@@ -492,6 +530,24 @@ function filterTree(source: TreeNode[], rawKeyword: 
string): { tree: TreeNode[],
   }
 }
 
+function filterTreeByFormat(source: TreeNode[], format: string): TreeNode[] {
+  return source.reduce<TreeNode[]>((result, node) => {
+    if (node.nodeType === 'table') {
+      const tableType = (node.tableType || '').toUpperCase()
+      if (tableType === format || tableType.endsWith(`_${format}`)) {
+        result.push(node)
+      }
+      return result
+    }
+
+    result.push({
+      ...node,
+      children: node.children ? filterTreeByFormat(node.children, format) : 
node.children,
+    })
+    return result
+  }, [])
+}
+
 let searchTimer: any = null
 
 watch(
@@ -522,7 +578,9 @@ watch(
     } = (oldValue || {}) as any
 
     if (state.namespaceMode && namespace !== oldNamespace) {
-      const nextNamespace = state.namespaces.includes(namespace as string) ? 
namespace as string : ''
+      const nextNamespace = namespace === allNamespaceValue || 
state.namespaces.includes(namespace as string)
+        ? namespace as string
+        : ''
       if (nextNamespace !== state.selectedNamespace) {
         clearExplorerState()
         state.selectedNamespace = nextNamespace
@@ -561,14 +619,17 @@ watch(
 )
 
 const searchResult = computed(() => {
+  const source = state.selectedFormat
+    ? filterTreeByFormat(state.treeData, state.selectedFormat)
+    : state.treeData
   const keyword = normalizeKeyword(state.filterKey)
   if (!keyword) {
     return {
-      tree: state.treeData,
+      tree: source,
       expandedKeys: state.expandedKeys,
     }
   }
-  return filterTree(state.treeData, keyword)
+  return filterTree(source, keyword)
 })
 
 const displayTreeData = computed(() => searchResult.value.tree)
@@ -652,7 +713,9 @@ async function initializeNamespaces() {
   }
 
   const routeNamespace = (route.query.namespace as string) || ''
-  state.selectedNamespace = namespaces.includes(routeNamespace) ? 
routeNamespace : ''
+  state.selectedNamespace = routeNamespace === allNamespaceValue || 
namespaces.includes(routeNamespace)
+    ? routeNamespace
+    : ''
 }
 
 async function handleNamespaceChange(namespace?: string) {
@@ -682,18 +745,29 @@ onBeforeMount(async () => {
 <template>
   <div class="table-explorer">
     <div class="table-explorer-header">
-      <div class="namespace-selector">
-        <span class="namespace-label">Namespace:</span>
+      <div class="explorer-filter-row">
         <a-select
-          v-if="state.namespaceMode"
-          :value="state.selectedNamespace || undefined"
-          :options="state.namespaces.map(namespace => ({ label: namespace, 
value: namespace }))"
-          placeholder="Select namespace"
-          class="namespace-select"
-          allow-clear
+          :value="state.namespaceMode ? (state.selectedNamespace || undefined) 
: 'default'"
+          :options="state.namespaceMode
+            ? [
+              { label: 'All', value: allNamespaceValue },
+              ...state.namespaces.map(namespace => ({ label: namespace, value: 
namespace })),
+            ]
+            : [{ label: 'default', value: 'default' }]"
+          :disabled="!state.namespaceMode"
+          :allow-clear="state.namespaceMode"
+          placeholder="Namespace"
+          class="explorer-filter-select namespace-filter-select"
           @change="handleNamespaceChange"
         />
-        <span v-else class="namespace-default">default</span>
+        <a-select
+          :value="state.selectedFormat || undefined"
+          :options="formatOptions"
+          placeholder="Format"
+          class="explorer-filter-select format-filter-select"
+          allow-clear
+          @change="handleFormatChange"
+        />
       </div>
       <a-input
         v-model:value="state.searchKey"
@@ -732,6 +806,9 @@ onBeforeMount(async () => {
             <span class="tree-node-text">
               {{ dataRef.title }}
             </span>
+            <span v-if="dataRef.catalogNamespace" 
class="catalog-namespace-label">
+              @{{ dataRef.catalogNamespace }}
+            </span>
           </span>
         </template>
       </a-tree>
@@ -759,43 +836,43 @@ onBeforeMount(async () => {
     padding: 0 7px 0 8px;
     margin-bottom: 8px;
 
-    .namespace-selector {
+    .explorer-filter-row {
       display: flex;
-      align-items: center;
-      min-height: 24px;
+      gap: 8px;
       margin-bottom: 8px;
-      font-size: 13px;
-
-      .namespace-label {
-        margin-right: 6px;
-        color: #666;
-      }
 
-      .namespace-default {
-        color: #262626;
-      }
-
-      .namespace-select {
-        flex: 1;
+      .explorer-filter-select {
         min-width: 0;
 
         :deep(.ant-select-selector) {
-          height: 24px;
+          height: 32px;
+          display: flex;
+          align-items: center;
         }
 
         :deep(.ant-select-selection-item),
         :deep(.ant-select-selection-placeholder) {
-          line-height: 22px;
+          line-height: 30px;
+          font-size: 14px;
+        }
+
+        :deep(.ant-select-selection-placeholder) {
+          color: #ccc;
         }
       }
+
+      .namespace-filter-select {
+        flex: 3;
+      }
+
+      .format-filter-select {
+        flex: 2;
+      }
     }
 
     .search-input {
       width: 100%;
-
-      :deep(.ant-input-affix-wrapper) {
-        height: 24px;
-      }
+      height: 32px;
 
       :deep(.ant-input-prefix) {
         display: flex;
@@ -806,11 +883,11 @@ onBeforeMount(async () => {
 
       :deep(.search-input-prefix-icon) {
         font-size: 16px;
-        line-height: 24px;
+        line-height: 30px;
       }
 
       :deep(.ant-input) {
-        line-height: 24px;
+        line-height: 30px;
         font-size: 14px;
 
         &::placeholder {
@@ -847,6 +924,13 @@ onBeforeMount(async () => {
       .tree-node-icon {
         margin-right: 6px;
       }
+
+      .catalog-namespace-label {
+        margin-left: 4px;
+        color: #999;
+        font-size: 11px;
+        font-weight: normal;
+      }
     }
 
     .empty-placeholder {
diff --git a/amoro-web/src/views/tables/index.vue 
b/amoro-web/src/views/tables/index.vue
index b3413d982..0eb630be4 100644
--- a/amoro-web/src/views/tables/index.vue
+++ b/amoro-web/src/views/tables/index.vue
@@ -28,6 +28,7 @@ import UCleanup from './components/Cleanup.vue'
 import UProfiling from './components/Profiling.vue'
 import UHealthScore from './components/HealthScoreDetails.vue'
 import TableExplorer from './components/TableExplorer.vue'
+import HiveTableDetails from '@/views/hive-details/index.vue'
 import useStore from '@/store/index'
 import type { IBaseDetailInfo } from '@/types/common.type'
 import { usePageScroll } from '@/hooks/usePageScroll'
@@ -44,6 +45,7 @@ export default defineComponent({
     UProfiling,
     UHealthScore,
     TableExplorer,
+    HiveTableDetails,
   },
   setup() {
     const router = useRouter()
@@ -139,6 +141,10 @@ export default defineComponent({
       return state.baseInfo.tableType === 'ICEBERG'
     })
 
+    const isHiveTable = computed(() => {
+      return String(route.query?.type || '').toUpperCase() === 'HIVE'
+    })
+
     const hasSelectedTable = computed(() => !!(route.query?.catalog && 
route.query?.db && route.query?.table))
 
     const setBaseDetailInfo = (baseInfo: IBaseDetailInfo & { comment?: string 
}) => {
@@ -232,6 +238,7 @@ export default defineComponent({
       tabConfigs,
       store,
       isIceberg,
+      isHiveTable,
       hasSelectedTable,
       setBaseDetailInfo,
       handleTableNotFound,
@@ -246,7 +253,7 @@ export default defineComponent({
 </script>
 
 <template>
-  <div class="page-scroll" ref="pageScrollRef">
+  <div ref="pageScrollRef" class="page-scroll">
     <div class="tables-wrap">
       <div v-if="!isSecondaryNav" class="tables-content">
         <div
@@ -258,45 +265,48 @@ export default defineComponent({
         <div class="tables-divider" aria-hidden="true" 
@mousedown="startSidebarResize" />
         <div class="tables-main">
           <template v-if="hasSelectedTable">
-            <div class="tables-main-header g-flex-jsb">
-              <div class="g-flex-col">
-                <div class="g-flex">
-                  <span :title="baseInfo.tableName" class="table-name 
g-text-nowrap">{{ baseInfo.tableName }}</span>
-                </div>
-                <div v-if="baseInfo.comment" class="table-info g-flex-ac">
-                  <p>{{ $t('Comment') }}: <span class="text-color">{{ 
baseInfo.comment }}</span></p>
-                </div>
-                <div class="table-info g-flex-ac">
-                  <p>{{ $t('optimizingStatus') }}: <span class="text-color">{{ 
baseInfo.optimizingStatus }}</span></p>
-                  <a-divider type="vertical" />
-                  <p>{{ $t('records') }}: <span class="text-color">{{ 
baseInfo.records }}</span></p>
-                  <a-divider type="vertical" />
-                  <template v-if="!isIceberg">
-                    <p>{{ $t('createTime') }}: <span class="text-color">{{ 
baseInfo.createTime }}</span></p>
+            <HiveTableDetails v-if="isHiveTable" />
+            <template v-else>
+              <div class="tables-main-header g-flex-jsb">
+                <div class="g-flex-col">
+                  <div class="g-flex">
+                    <span :title="baseInfo.tableName" class="table-name 
g-text-nowrap">{{ baseInfo.tableName }}</span>
+                  </div>
+                  <div v-if="baseInfo.comment" class="table-info g-flex-ac">
+                    <p>{{ $t('Comment') }}: <span class="text-color">{{ 
baseInfo.comment }}</span></p>
+                  </div>
+                  <div class="table-info g-flex-ac">
+                    <p>{{ $t('optimizingStatus') }}: <span 
class="text-color">{{ baseInfo.optimizingStatus }}</span></p>
                     <a-divider type="vertical" />
-                  </template>
-                  <p>{{ $t('tableFormat') }}: <span class="text-color">{{ 
baseInfo.tableFormat }}</span></p>
-                  <a-divider type="vertical" />
-                  <p>
-                    {{ $t('healthScore') }}:
-                    <UHealthScore :base-info="baseInfo" />
-                  </p>
+                    <p>{{ $t('records') }}: <span class="text-color">{{ 
baseInfo.records }}</span></p>
+                    <a-divider type="vertical" />
+                    <template v-if="!isIceberg">
+                      <p>{{ $t('createTime') }}: <span class="text-color">{{ 
baseInfo.createTime }}</span></p>
+                      <a-divider type="vertical" />
+                    </template>
+                    <p>{{ $t('tableFormat') }}: <span class="text-color">{{ 
baseInfo.tableFormat }}</span></p>
+                    <a-divider type="vertical" />
+                    <p>
+                      {{ $t('healthScore') }}:
+                      <UHealthScore :base-info="baseInfo" />
+                    </p>
+                  </div>
                 </div>
               </div>
-            </div>
-            <div class="tables-main-body">
-              <a-tabs v-model:activeKey="activeKey" destroy-inactive-tab-pane 
@change="onChangeTab">
-                <a-tab-pane key="Details" :tab="$t('details')" force-render>
-                  <UDetails ref="detailRef" 
@set-base-detail-info="setBaseDetailInfo" 
@table-not-found="handleTableNotFound" />
-                </a-tab-pane>
-                <a-tab-pane v-if="detailLoaded" key="Files" :tab="$t('files')">
-                  <UFiles :has-partition="baseInfo.hasPartition" />
-                </a-tab-pane>
-                <a-tab-pane v-for="tab in tabConfigs" :key="tab.key" 
:tab="$t(tab.label)">
-                  <component :is="`U${tab.key}`" />
-                </a-tab-pane>
-              </a-tabs>
-            </div>
+              <div class="tables-main-body">
+                <a-tabs v-model:activeKey="activeKey" 
destroy-inactive-tab-pane @change="onChangeTab">
+                  <a-tab-pane key="Details" :tab="$t('details')" force-render>
+                    <UDetails ref="detailRef" 
@set-base-detail-info="setBaseDetailInfo" 
@table-not-found="handleTableNotFound" />
+                  </a-tab-pane>
+                  <a-tab-pane v-if="detailLoaded" key="Files" 
:tab="$t('files')">
+                    <UFiles :has-partition="baseInfo.hasPartition" />
+                  </a-tab-pane>
+                  <a-tab-pane v-for="tab in tabConfigs" :key="tab.key" 
:tab="$t(tab.label)">
+                    <component :is="`U${tab.key}`" />
+                  </a-tab-pane>
+                </a-tabs>
+              </div>
+            </template>
           </template>
           <div v-else class="empty-page" />
         </div>
diff --git a/dist/src/main/amoro-bin/bin/load-config.sh 
b/dist/src/main/amoro-bin/bin/load-config.sh
index 69dc95462..aad3adadd 100644
--- a/dist/src/main/amoro-bin/bin/load-config.sh
+++ b/dist/src/main/amoro-bin/bin/load-config.sh
@@ -70,6 +70,16 @@ export JVM_EXTRA_CONFIG
 
 test -f ${AMORO_ENV_FILE} && source ${AMORO_ENV_FILE}
 
+# LAS deployments expose the existing service credential names. Proton's 
built-in
+# AssumeIamRoleCredentialProvider uses fixed environment names, so keep those 
as internal aliases
+# without requiring a second pair of K8S Secret keys.
+if [ -n "${LAS_SERVICE_AK}" ] && [ -z "${ASSUME_ROLE_ACCESS_KEY}" ]; then
+    export ASSUME_ROLE_ACCESS_KEY="${LAS_SERVICE_AK}"
+fi
+if [ -n "${LAS_SERVICE_SK}" ] && [ -z "${ASSUME_ROLE_SECRET_KEY}" ]; then
+    export ASSUME_ROLE_SECRET_KEY="${LAS_SERVICE_SK}"
+fi
+
 # set env variable amoro-addition-classpath if not exists
 if [ -z "${AMORO_ADDITION_CLASSPATH}" ]; then
     export AMORO_ADDITION_CLASSPATH=
diff --git a/dist/src/main/amoro-bin/conf/config.yaml 
b/dist/src/main/amoro-bin/conf/config.yaml
index 2c2ad6543..4aff943b0 100644
--- a/dist/src/main/amoro-bin/conf/config.yaml
+++ b/dist/src/main/amoro-bin/conf/config.yaml
@@ -75,9 +75,8 @@ ams:
   # Values may also be supplied through environment variables, for example:
   # AMS_LAS_INTEGRATION_ENABLED=true
   # 
AMS_LAS_INTEGRATION_HMS__URI=thrift://hms-service.<namespace>.svc.cluster.local:9083
-  # Keep IAM bootstrap credentials in secret-backed environment variables:
-  # AMS_LAS_INTEGRATION_IAM_BOOTSTRAP__ACCESS__KEY and
-  # AMS_LAS_INTEGRATION_IAM_BOOTSTRAP__SECRET__KEY.
+  # Reuse the existing LAS service credential environment variables. Inject 
both from a K8S
+  # Secret; do not put their values in this file: LAS_SERVICE_AK and 
LAS_SERVICE_SK.
   las:
     integration:
       enabled: false
@@ -91,8 +90,9 @@ ams:
       connect-timeout: 30s
       read-timeout: 30s
       iam:
-        # bootstrap-access-key: supplied by a secret-backed environment 
variable
-        # bootstrap-secret-key: supplied by a secret-backed environment 
variable
+        # Optional explicit overrides. LAS deployments should use 
LAS_SERVICE_AK/LAS_SERVICE_SK.
+        # bootstrap-access-key: supplied by a secret-backed configuration
+        # bootstrap-secret-key: supplied by a secret-backed configuration
         # bootstrap-session-token: optional, supplied with temporary bootstrap 
credentials
         role-session-name: AmoroAssumeRoleSession
         assume-role-ttl: 1h
@@ -152,6 +152,7 @@ ams:
   # catalog explorer. LAS deployments typically set 
refresh-external-catalogs.interval to 10min.
   catalog:
     namespace-enabled: true
+    namespace-allowlist-enabled: true
 
   # Support for encrypted sensitive configuration items
   shade:
diff --git a/pom.xml b/pom.xml
index edba971c8..e8b666070 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1456,6 +1456,18 @@
             
<url>https://repository.apache.org/content/repositories/releases/</url>
         </repository>
 
+        <repository>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+            <id>bytedance-releases</id>
+            <name>ByteDance Maven Releases</name>
+            <url>https://maven.byted.org/repository/releases/</url>
+        </repository>
+
         <!--
         Uncomment the following "repository" tag if your maven fails to 
download amoro-shade snapshot files.
         We comment these tags by default because downloading from apache 
repositories is very slow for both github and developers.

Reply via email to