You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by zh...@apache.org on 2026/08/09 03:27:33 UTC

(dubbo-kubernetes) branch master updated: feat: add mesh-native AI service routing (#1007)

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

mfordjody pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git


The following commit(s) were added to refs/heads/master by this push:
     new e4fe0b73 feat: add mesh-native AI service routing (#1007)
e4fe0b73 is described below

commit e4fe0b7338564c947efc7d916d95928c29a62671
Author: mfordjody <11...@qq.com>
AuthorDate: Sun Aug 9 11:27:28 2026 +0800

    feat: add mesh-native AI service routing (#1007)
---
 .github/workflows/ci.yml                           |   3 +-
 dubbod/discovery/pkg/bootstrap/server.go           |  59 +--
 dubbod/discovery/pkg/bootstrap/server_test.go      |  32 ++
 .../pkg/config/kube/crdclient/types.gen.go         |  51 +++
 .../kube/gateway/deployment_controller_test.go     |  77 ++--
 dubbod/discovery/pkg/model/push_context.go         |  39 ++
 .../pkg/networking/grpcgen/agent_config.go         | 430 +++++++++++++++++++++
 dubbod/discovery/pkg/networking/grpcgen/rds.go     |  52 ++-
 .../discovery/pkg/networking/grpcgen/rds_test.go   | 170 ++++++++
 go.mod                                             |   6 +-
 go.sum                                             |  12 +-
 manifests/charts/base/files/crd-all.gen.yaml       | 365 +++++++++++++++++
 manifests/charts/dubbod/files/kube-gateway.yaml    |  36 +-
 manifests/charts/dubbod/templates/clusterrole.yaml |  12 +
 .../dubbod/templates/configmap-injector.yaml       |  17 +
 pkg/config/schema/collections/collections.gen.go   |  19 +
 pkg/config/schema/gvk/resources.gen.go             |   7 +
 pkg/config/schema/gvr/resources.gen.go             |   3 +
 pkg/config/schema/kind/resources.gen.go            |   5 +
 pkg/config/schema/kubeclient/resources.gen.go      |  13 +
 pkg/config/schema/kubetypes/resources.gen.go       |   4 +
 pkg/config/schema/metadata.yaml                    |  10 +
 pkg/config/validation/validators.go                | 151 ++++++++
 pkg/config/validation/validators_test.go           | 106 +++++
 samples/ai-mesh/README.md                          |  18 +
 samples/ai-mesh/backends.yaml                      | 119 ++++++
 samples/ai-mesh/gateway.yaml                       |  13 +
 samples/ai-mesh/routes.yaml                        |  86 +++++
 samples/ai-mesh/services.yaml                      |  80 ++++
 tests/e2e/agentmock/Dockerfile                     |   9 +
 tests/e2e/agentmock/main.go                        |  94 +++++
 tests/e2e/run.sh                                   | 134 ++++++-
 32 files changed, 2132 insertions(+), 100 deletions(-)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 79b5fdb0..ecc8143e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -251,7 +251,7 @@ jobs:
         uses: actions/checkout@v4
         with:
           repository: kdubbo/dxgate
-          ref: d683797f44c9ba4c6e1eb6cdb2da21460b00cbfd
+          ref: c8e423501f103c7faa08a0cee7a43680a5054138
           path: .e2e/dxgate
 
       - name: Build dxgate activation image
@@ -266,6 +266,7 @@ jobs:
       - name: Run real KEDA activation
         env:
           ACTIVATION_E2E: '1'
+          AI_MESH_E2E: '1'
           CLUSTER_NAME: dubbo-activation-ga
           DXGATE_IMAGE: kdubbo/dxgate:ga-e2e
           IMAGE: kdubbo/dubbod:activation-ga-e2e
diff --git a/dubbod/discovery/pkg/bootstrap/server.go b/dubbod/discovery/pkg/bootstrap/server.go
index bcb49a48..a6a2faf5 100644
--- a/dubbod/discovery/pkg/bootstrap/server.go
+++ b/dubbod/discovery/pkg/bootstrap/server.go
@@ -500,31 +500,8 @@ func (s *Server) initRegistryEventHandlers() {
 		log.Infof("processing config change, schema identifier=%s, GVK=%v, name=%s/%s, event=%s",
 			schemaID, cfg.GroupVersionKind, cfg.Namespace, cfg.Name, event)
 
-		var configKind kind.Kind
-		switch schemaID {
-		case "AuthorizationPolicy":
-			configKind = kind.AuthorizationPolicy
-		case "PeerAuthentication":
-			configKind = kind.PeerAuthentication
-		case "RequestAuthentication":
-			configKind = kind.RequestAuthentication
-		case "ReferenceGrant":
-			configKind = kind.ReferenceGrant
-		case "GatewayClass":
-			configKind = kind.GatewayClass
-		case "KubernetesGateway":
-			configKind = kind.KubernetesGateway
-		case "HTTPRoute":
-			configKind = kind.HTTPRoute
-		case "BackendTLSPolicy":
-			configKind = kind.BackendTLSPolicy
-		case "CircuitBreakerPolicy":
-			configKind = kind.CircuitBreakerPolicy
-		case "FaultInjectionPolicy":
-			configKind = kind.FaultInjectionPolicy
-		case "ServiceActivationPolicy":
-			configKind = kind.ServiceActivationPolicy
-		default:
+		configKind, found := configKindForSchemaIdentifier(schemaID)
+		if !found {
 			log.Debugf("unknown schema identifier %s for %v, skipping", schemaID, cfg.GroupVersionKind)
 			return
 		}
@@ -557,6 +534,7 @@ func (s *Server) initRegistryEventHandlers() {
 			configKind == kind.ReferenceGrant ||
 			configKind == kind.CircuitBreakerPolicy ||
 			configKind == kind.FaultInjectionPolicy ||
+			configKind == kind.DxgateService ||
 			configKind == kind.ServiceActivationPolicy
 
 		// Trigger ConfigUpdate to push changes to all connected proxies
@@ -618,6 +596,37 @@ func (s *Server) initReadinessProbes() {
 	}
 }
 
+func configKindForSchemaIdentifier(schemaID string) (kind.Kind, bool) {
+	switch schemaID {
+	case "AuthorizationPolicy":
+		return kind.AuthorizationPolicy, true
+	case "PeerAuthentication":
+		return kind.PeerAuthentication, true
+	case "RequestAuthentication":
+		return kind.RequestAuthentication, true
+	case "ReferenceGrant":
+		return kind.ReferenceGrant, true
+	case "GatewayClass":
+		return kind.GatewayClass, true
+	case "KubernetesGateway":
+		return kind.KubernetesGateway, true
+	case "HTTPRoute":
+		return kind.HTTPRoute, true
+	case "BackendTLSPolicy":
+		return kind.BackendTLSPolicy, true
+	case "CircuitBreakerPolicy":
+		return kind.CircuitBreakerPolicy, true
+	case "FaultInjectionPolicy":
+		return kind.FaultInjectionPolicy, true
+	case "DxgateService":
+		return kind.DxgateService, true
+	case "ServiceActivationPolicy":
+		return kind.ServiceActivationPolicy, true
+	default:
+		return 0, false
+	}
+}
+
 func (s *Server) initMulticluster(args *DubboArgs) {
 	if s.kubeClient == nil {
 		return
diff --git a/dubbod/discovery/pkg/bootstrap/server_test.go b/dubbod/discovery/pkg/bootstrap/server_test.go
new file mode 100644
index 00000000..a04ae89c
--- /dev/null
+++ b/dubbod/discovery/pkg/bootstrap/server_test.go
@@ -0,0 +1,32 @@
+// 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 bootstrap
+
+import (
+	"testing"
+
+	"github.com/apache/dubbo-kubernetes/pkg/config/schema/kind"
+)
+
+func TestConfigKindForSchemaIdentifierIncludesDxgateService(t *testing.T) {
+	got, found := configKindForSchemaIdentifier("DxgateService")
+	if !found {
+		t.Fatal("DxgateService schema identifier was not mapped")
+	}
+	if got != kind.DxgateService {
+		t.Fatalf("kind = %v, want %v", got, kind.DxgateService)
+	}
+}
diff --git a/dubbod/discovery/pkg/config/kube/crdclient/types.gen.go b/dubbod/discovery/pkg/config/kube/crdclient/types.gen.go
index 6eaf61e3..731cc4db 100755
--- a/dubbod/discovery/pkg/config/kube/crdclient/types.gen.go
+++ b/dubbod/discovery/pkg/config/kube/crdclient/types.gen.go
@@ -50,6 +50,11 @@ func create(c kube.Client, cfg config.Config, objMeta metav1.ObjectMeta) (metav1
 			ObjectMeta: objMeta,
 			Spec:       *(cfg.Spec.(*githubcomkdubboapinetworkingv1alpha3.CircuitBreakerPolicy)),
 		}, metav1.CreateOptions{})
+	case gvk.DxgateService:
+		return c.Dubbo().NetworkingV1alpha3().DxgateServices(cfg.Namespace).Create(context.TODO(), &apigithubcomapachedubbokubernetesapinetworkingv1alpha3.DxgateService{
+			ObjectMeta: objMeta,
+			Spec:       *(cfg.Spec.(*githubcomkdubboapinetworkingv1alpha3.DxgateService)),
+		}, metav1.CreateOptions{})
 	case gvk.FaultInjectionPolicy:
 		return c.Dubbo().NetworkingV1alpha3().FaultInjectionPolicies(cfg.Namespace).Create(context.TODO(), &apigithubcomapachedubbokubernetesapinetworkingv1alpha3.FaultInjectionPolicy{
 			ObjectMeta: objMeta,
@@ -127,6 +132,11 @@ func update(c kube.Client, cfg config.Config, objMeta metav1.ObjectMeta) (metav1
 			ObjectMeta: objMeta,
 			Spec:       *(cfg.Spec.(*githubcomkdubboapinetworkingv1alpha3.CircuitBreakerPolicy)),
 		}, metav1.UpdateOptions{})
+	case gvk.DxgateService:
+		return c.Dubbo().NetworkingV1alpha3().DxgateServices(cfg.Namespace).Update(context.TODO(), &apigithubcomapachedubbokubernetesapinetworkingv1alpha3.DxgateService{
+			ObjectMeta: objMeta,
+			Spec:       *(cfg.Spec.(*githubcomkdubboapinetworkingv1alpha3.DxgateService)),
+		}, metav1.UpdateOptions{})
 	case gvk.FaultInjectionPolicy:
 		return c.Dubbo().NetworkingV1alpha3().FaultInjectionPolicies(cfg.Namespace).Update(context.TODO(), &apigithubcomapachedubbokubernetesapinetworkingv1alpha3.FaultInjectionPolicy{
 			ObjectMeta: objMeta,
@@ -204,6 +214,11 @@ func updateStatus(c kube.Client, cfg config.Config, objMeta metav1.ObjectMeta) (
 			ObjectMeta: objMeta,
 			Status:     *(cfg.Status.(*githubcomkdubboapimetav1alpha1.DubboStatus)),
 		}, metav1.UpdateOptions{})
+	case gvk.DxgateService:
+		return c.Dubbo().NetworkingV1alpha3().DxgateServices(cfg.Namespace).UpdateStatus(context.TODO(), &apigithubcomapachedubbokubernetesapinetworkingv1alpha3.DxgateService{
+			ObjectMeta: objMeta,
+			Status:     *(cfg.Status.(*githubcomkdubboapimetav1alpha1.DubboStatus)),
+		}, metav1.UpdateOptions{})
 	case gvk.FaultInjectionPolicy:
 		return c.Dubbo().NetworkingV1alpha3().FaultInjectionPolicies(cfg.Namespace).UpdateStatus(context.TODO(), &apigithubcomapachedubbokubernetesapinetworkingv1alpha3.FaultInjectionPolicy{
 			ObjectMeta: objMeta,
@@ -309,6 +324,21 @@ func patch(c kube.Client, orig config.Config, origMeta metav1.ObjectMeta, mod co
 		}
 		return c.Dubbo().NetworkingV1alpha3().CircuitBreakerPolicies(orig.Namespace).
 			Patch(context.TODO(), orig.Name, typ, patchBytes, metav1.PatchOptions{FieldManager: "pilot-discovery"})
+	case gvk.DxgateService:
+		oldRes := &apigithubcomapachedubbokubernetesapinetworkingv1alpha3.DxgateService{
+			ObjectMeta: origMeta,
+			Spec:       *(orig.Spec.(*githubcomkdubboapinetworkingv1alpha3.DxgateService)),
+		}
+		modRes := &apigithubcomapachedubbokubernetesapinetworkingv1alpha3.DxgateService{
+			ObjectMeta: modMeta,
+			Spec:       *(mod.Spec.(*githubcomkdubboapinetworkingv1alpha3.DxgateService)),
+		}
+		patchBytes, err := genPatchBytes(oldRes, modRes, typ)
+		if err != nil {
+			return nil, err
+		}
+		return c.Dubbo().NetworkingV1alpha3().DxgateServices(orig.Namespace).
+			Patch(context.TODO(), orig.Name, typ, patchBytes, metav1.PatchOptions{FieldManager: "pilot-discovery"})
 	case gvk.FaultInjectionPolicy:
 		oldRes := &apigithubcomapachedubbokubernetesapinetworkingv1alpha3.FaultInjectionPolicy{
 			ObjectMeta: origMeta,
@@ -491,6 +521,8 @@ func delete(c kube.Client, typ config.GroupVersionKind, name, namespace string,
 		return c.GatewayAPI().GatewayV1().BackendTLSPolicies(namespace).Delete(context.TODO(), name, deleteOptions)
 	case gvk.CircuitBreakerPolicy:
 		return c.Dubbo().NetworkingV1alpha3().CircuitBreakerPolicies(namespace).Delete(context.TODO(), name, deleteOptions)
+	case gvk.DxgateService:
+		return c.Dubbo().NetworkingV1alpha3().DxgateServices(namespace).Delete(context.TODO(), name, deleteOptions)
 	case gvk.FaultInjectionPolicy:
 		return c.Dubbo().NetworkingV1alpha3().FaultInjectionPolicies(namespace).Delete(context.TODO(), name, deleteOptions)
 	case gvk.GatewayClass:
@@ -648,6 +680,25 @@ var translationMap = map[config.GroupVersionKind]func(r runtime.Object) config.C
 			Spec: &obj.Spec,
 		}
 	},
+	gvk.DxgateService: func(r runtime.Object) config.Config {
+		obj := r.(*apigithubcomapachedubbokubernetesapinetworkingv1alpha3.DxgateService)
+		return config.Config{
+			Meta: config.Meta{
+				GroupVersionKind:  gvk.DxgateService,
+				Name:              obj.Name,
+				Namespace:         obj.Namespace,
+				Labels:            obj.Labels,
+				Annotations:       obj.Annotations,
+				ResourceVersion:   obj.ResourceVersion,
+				CreationTimestamp: obj.CreationTimestamp.Time,
+				OwnerReferences:   obj.OwnerReferences,
+				UID:               string(obj.UID),
+				Generation:        obj.Generation,
+			},
+			Spec:   &obj.Spec,
+			Status: &obj.Status,
+		}
+	},
 	gvk.EndpointSlice: func(r runtime.Object) config.Config {
 		obj := r.(*k8sioapidiscoveryv1.EndpointSlice)
 		return config.Config{
diff --git a/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go b/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go
index 826f4701..cd259594 100644
--- a/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go
+++ b/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go
@@ -691,8 +691,8 @@ func TestKubeGatewayTemplateRendersDxgateResources(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if len(rendered) != 4 {
-		t.Fatalf("expected 4 rendered resources, got %d", len(rendered))
+	if len(rendered) != 6 {
+		t.Fatalf("expected 6 rendered resources, got %d", len(rendered))
 	}
 	for i, doc := range rendered {
 		var obj map[string]any
@@ -706,25 +706,30 @@ func TestKubeGatewayTemplateRendersDxgateResources(t *testing.T) {
 	if !strings.Contains(rendered[0], "bootstrap.json") || !strings.Contains(rendered[0], `"xds_address": "http://dubbod.dubbo-system.svc:26010"`) {
 		t.Fatalf("configmap did not render dxgate bootstrap:\n%s", rendered[0])
 	}
-	if !strings.Contains(rendered[2], "image: kdubbo/dxgate:test") {
-		t.Fatalf("deployment did not render dxgate image:\n%s", rendered[2])
+	if !strings.Contains(rendered[1], "automountServiceAccountToken: true") ||
+		!strings.Contains(rendered[2], "resources: [\"secrets\"]") ||
+		!strings.Contains(rendered[3], "kind: RoleBinding") {
+		t.Fatalf("credential Secret RBAC not rendered:\n%s", strings.Join(rendered[1:4], "\n---\n"))
 	}
-	if !strings.Contains(rendered[2], "DXGATE_BOOTSTRAP") || strings.Contains(rendered[2], "DXGATE_STATIC_CONFIG") {
-		t.Fatalf("deployment did not switch from static config to bootstrap:\n%s", rendered[2])
+	if !strings.Contains(rendered[4], "image: kdubbo/dxgate:test") {
+		t.Fatalf("deployment did not render dxgate image:\n%s", rendered[4])
 	}
-	if !strings.Contains(rendered[2], `proxyless.dubbo.apache.org/inject: "true"`) {
-		t.Fatalf("deployment pod template did not enable proxyless injection for dxgate mTLS certs:\n%s", rendered[2])
+	if !strings.Contains(rendered[4], "DXGATE_BOOTSTRAP") || strings.Contains(rendered[4], "DXGATE_STATIC_CONFIG") {
+		t.Fatalf("deployment did not switch from static config to bootstrap:\n%s", rendered[4])
 	}
-	if !strings.Contains(rendered[2], "inject.dubbo.apache.org/templates: grpc-engine") {
-		t.Fatalf("deployment pod template did not request grpc-engine injection:\n%s", rendered[2])
+	if !strings.Contains(rendered[4], `proxyless.dubbo.apache.org/inject: "true"`) {
+		t.Fatalf("deployment pod template did not enable proxyless injection for dxgate mTLS certs:\n%s", rendered[4])
 	}
-	if !strings.Contains(rendered[2], `prometheus.io/scrape: "true"`) ||
-		!strings.Contains(rendered[2], "prometheus.io/path: /metrics") ||
-		!strings.Contains(rendered[2], `prometheus.io/port: "26021"`) {
-		t.Fatalf("deployment pod template did not render prometheus scrape annotations:\n%s", rendered[2])
+	if !strings.Contains(rendered[4], "inject.dubbo.apache.org/templates: grpc-engine") {
+		t.Fatalf("deployment pod template did not request grpc-engine injection:\n%s", rendered[4])
 	}
-	if strings.Contains(rendered[2], "DXGATE_OTEL_ENDPOINT") {
-		t.Fatalf("deployment rendered OTEL endpoint without annotation:\n%s", rendered[2])
+	if !strings.Contains(rendered[4], `prometheus.io/scrape: "true"`) ||
+		!strings.Contains(rendered[4], "prometheus.io/path: /metrics") ||
+		!strings.Contains(rendered[4], `prometheus.io/port: "26021"`) {
+		t.Fatalf("deployment pod template did not render prometheus scrape annotations:\n%s", rendered[4])
+	}
+	if strings.Contains(rendered[4], "DXGATE_OTEL_ENDPOINT") {
+		t.Fatalf("deployment rendered OTEL endpoint without annotation:\n%s", rendered[4])
 	}
 	for _, want := range []string{
 		"DXGATE_GATEWAY_NAME",
@@ -737,21 +742,21 @@ func TestKubeGatewayTemplateRendersDxgateResources(t *testing.T) {
 		"DXGATE_ACCESS_LOG_FORMAT",
 		`value: "text"`,
 	} {
-		if !strings.Contains(rendered[2], want) {
-			t.Fatalf("deployment did not render observability env %q:\n%s", want, rendered[2])
+		if !strings.Contains(rendered[4], want) {
+			t.Fatalf("deployment did not render observability env %q:\n%s", want, rendered[4])
 		}
 	}
-	if !strings.Contains(rendered[2], "app.kubernetes.io/instance: public-dubbo") {
-		t.Fatalf("deployment did not render stable dxgate instance label:\n%s", rendered[2])
+	if !strings.Contains(rendered[4], "app.kubernetes.io/instance: public-dubbo") {
+		t.Fatalf("deployment did not render stable dxgate instance label:\n%s", rendered[4])
 	}
-	if !strings.Contains(rendered[3], "targetPort: 15080") {
-		t.Fatalf("service did not target grpc-inbound port:\n%s", rendered[3])
+	if !strings.Contains(rendered[5], "targetPort: 15080") {
+		t.Fatalf("service did not target grpc-inbound port:\n%s", rendered[5])
 	}
-	if !strings.Contains(rendered[3], `proxyless.dubbo.apache.org/inject: "false"`) {
-		t.Fatalf("service did not opt out of proxyless targetPort rewriting:\n%s", rendered[3])
+	if !strings.Contains(rendered[5], `proxyless.dubbo.apache.org/inject: "false"`) {
+		t.Fatalf("service did not opt out of proxyless targetPort rewriting:\n%s", rendered[5])
 	}
-	if !strings.Contains(rendered[3], "app.kubernetes.io/instance: public-dubbo") {
-		t.Fatalf("service did not render stable dxgate instance selector:\n%s", rendered[3])
+	if !strings.Contains(rendered[5], "app.kubernetes.io/instance: public-dubbo") {
+		t.Fatalf("service did not render stable dxgate instance selector:\n%s", rendered[5])
 	}
 
 	input.OtelEndpoint = "http://tracing.dubbo-system.svc:4317"
@@ -759,9 +764,9 @@ func TestKubeGatewayTemplateRendersDxgateResources(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if !strings.Contains(rendered[2], "DXGATE_OTEL_ENDPOINT") ||
-		!strings.Contains(rendered[2], `value: "http://tracing.dubbo-system.svc:4317"`) {
-		t.Fatalf("deployment did not render dxgate OTEL endpoint:\n%s", rendered[2])
+	if !strings.Contains(rendered[4], "DXGATE_OTEL_ENDPOINT") ||
+		!strings.Contains(rendered[4], `value: "http://tracing.dubbo-system.svc:4317"`) {
+		t.Fatalf("deployment did not render dxgate OTEL endpoint:\n%s", rendered[4])
 	}
 }
 
@@ -803,8 +808,8 @@ func TestKubeGatewayTemplateRendersHighAvailabilityResources(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if len(rendered) != 5 {
-		t.Fatalf("rendered %d resources, want 5 including the disruption budget", len(rendered))
+	if len(rendered) != 7 {
+		t.Fatalf("rendered %d resources, want 7 including credential RBAC and disruption budget", len(rendered))
 	}
 	joined := strings.Join(rendered, "\n---\n")
 	for _, want := range []string{
@@ -825,8 +830,8 @@ func TestKubeGatewayTemplateRendersHighAvailabilityResources(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if len(rendered) != 4 {
-		t.Fatalf("rendered %d resources at one replica, want 4 without a disruption budget", len(rendered))
+	if len(rendered) != 6 {
+		t.Fatalf("rendered %d resources at one replica, want 6 without a disruption budget", len(rendered))
 	}
 	joined = strings.Join(rendered, "\n---\n")
 	if strings.Contains(joined, "kind: PodDisruptionBudget") {
@@ -934,7 +939,7 @@ func TestKubeGatewayTemplateRendersActivationEnv(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	deployment := rendered[2]
+	deployment := rendered[4]
 	if !strings.Contains(deployment, "DXGATE_ACTIVATION_CONTROL_PLANE") ||
 		!strings.Contains(deployment, "dubbod-activation-replicas.dubbo-system.svc.cluster.local:26030") {
 		t.Fatalf("deployment did not render the activation control plane:\n%s", deployment)
@@ -954,7 +959,7 @@ func TestKubeGatewayTemplateRendersActivationEnv(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	if strings.Contains(off[2], "DXGATE_ACTIVATION") {
-		t.Fatalf("activation env leaked into a gateway with activation disabled:\n%s", off[2])
+	if strings.Contains(off[4], "DXGATE_ACTIVATION") {
+		t.Fatalf("activation env leaked into a gateway with activation disabled:\n%s", off[4])
 	}
 }
diff --git a/dubbod/discovery/pkg/model/push_context.go b/dubbod/discovery/pkg/model/push_context.go
index b9bf9a9c..72742d38 100644
--- a/dubbod/discovery/pkg/model/push_context.go
+++ b/dubbod/discovery/pkg/model/push_context.go
@@ -75,6 +75,7 @@ type PushContext struct {
 	ServiceIndex           serviceIndex
 	virtualServiceIndex    virtualServiceIndex
 	httpRouteIndex         httpRouteIndex
+	dxgateServiceIndex     dxgateServiceIndex
 	backendTLSPolicyIndex  backendTLSPolicyIndex
 	faultInjectionIndex    faultInjectionPolicyIndex
 	serviceActivationIndex serviceActivationPolicyIndex
@@ -165,6 +166,10 @@ type httpRouteIndex struct {
 	hostToRoutes map[host.Name][]config.Config
 }
 
+type dxgateServiceIndex struct {
+	byNamespace map[string]map[string]config.Config
+}
+
 type backendTLSPolicyIndex struct {
 	serviceTLS map[string]BackendTLSSettings
 }
@@ -193,6 +198,7 @@ func NewPushContext() *PushContext {
 	return &PushContext{
 		ServiceIndex:          newServiceIndex(),
 		virtualServiceIndex:   newVirtualServiceIndex(),
+		dxgateServiceIndex:    dxgateServiceIndex{byNamespace: map[string]map[string]config.Config{}},
 		backendTLSPolicyIndex: backendTLSPolicyIndex{serviceTLS: map[string]BackendTLSSettings{}},
 		serviceActivationIndex: serviceActivationPolicyIndex{
 			services: map[string][]string{},
@@ -628,6 +634,7 @@ func (ps *PushContext) createNewContext(env *Environment) {
 	// Initialize Kubernetes Gateway API resources if the controller is enabled.
 	ps.initKubernetesGateways(env)
 	ps.initHTTPRoutes(env)
+	ps.initDxgateServices(env)
 	ps.initBackendTLSPolicies(env)
 	ps.initFaultInjectionPolicies(env)
 	ps.initServiceActivationPolicies(env)
@@ -687,6 +694,13 @@ func (ps *PushContext) updateContext(env *Environment, oldPushContext *PushConte
 		ps.httpRouteIndex = oldPushContext.httpRouteIndex
 	}
 
+	dxgateServicesChanged := pushReq != nil && HasConfigsOfKind(pushReq.ConfigsUpdated, kind.DxgateService)
+	if dxgateServicesChanged {
+		ps.initDxgateServices(env)
+	} else {
+		ps.dxgateServiceIndex = oldPushContext.dxgateServiceIndex
+	}
+
 	backendTLSPoliciesChanged := pushReq != nil && HasConfigsOfKind(pushReq.ConfigsUpdated, kind.BackendTLSPolicy)
 	if backendTLSPoliciesChanged {
 		log.Debugf("BackendTLSPolicies changed, re-initializing BackendTLSPolicy index")
@@ -866,6 +880,31 @@ func (ps *PushContext) initHTTPRoutes(env *Environment) {
 	}
 }
 
+func (ps *PushContext) initDxgateServices(env *Environment) {
+	services := sortConfigByCreationTime(env.List(gvk.DxgateService, NamespaceAll))
+	index := make(map[string]map[string]config.Config)
+	for _, service := range services {
+		if index[service.Namespace] == nil {
+			index[service.Namespace] = make(map[string]config.Config)
+		}
+		index[service.Namespace][service.Name] = service
+	}
+	ps.dxgateServiceIndex = dxgateServiceIndex{byNamespace: index}
+}
+
+// DxgateService resolves the mesh-native backend named by an HTTPRoute.
+func (ps *PushContext) DxgateService(namespace, name string) (config.Config, bool) {
+	if ps == nil {
+		return config.Config{}, false
+	}
+	services := ps.dxgateServiceIndex.byNamespace[namespace]
+	if services == nil {
+		return config.Config{}, false
+	}
+	service, found := services[name]
+	return service, found
+}
+
 // HTTPRouteForHost returns HTTPRoutes that match the given hostname.
 func (ps *PushContext) HTTPRouteForHost(hostname host.Name) []config.Config {
 	var routes []config.Config
diff --git a/dubbod/discovery/pkg/networking/grpcgen/agent_config.go b/dubbod/discovery/pkg/networking/grpcgen/agent_config.go
new file mode 100644
index 00000000..86847fd3
--- /dev/null
+++ b/dubbod/discovery/pkg/networking/grpcgen/agent_config.go
@@ -0,0 +1,430 @@
+//
+// 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 grpcgen
+
+import (
+	"fmt"
+	"sort"
+	"strings"
+
+	"github.com/apache/dubbo-kubernetes/dubbod/discovery/pkg/model"
+	"github.com/apache/dubbo-kubernetes/pkg/config"
+	networking "github.com/kdubbo/api/networking/v1alpha3"
+	route "github.com/kdubbo/xds-api/route/v1"
+	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+)
+
+const (
+	dxgateServiceGroup = "networking.dubbo.apache.org"
+	dxgateServiceKind  = "DxgateService"
+)
+
+func isDxgateServiceBackend(ref gatewayv1.HTTPBackendRef) bool {
+	group := ""
+	if ref.Group != nil {
+		group = string(*ref.Group)
+	}
+	kind := "Service"
+	if ref.Kind != nil {
+		kind = string(*ref.Kind)
+	}
+	return group == dxgateServiceGroup && kind == dxgateServiceKind
+}
+
+func ruleUsesDxgateService(rule gatewayv1.HTTPRouteRule) bool {
+	for _, ref := range rule.BackendRefs {
+		if isDxgateServiceBackend(ref) {
+			return true
+		}
+	}
+	return false
+}
+
+func buildAgentConfig(push *model.PushContext, httpRoutes []config.Config) *route.AgentConfig {
+	if push == nil {
+		return nil
+	}
+	out := &route.AgentConfig{}
+	providers := map[string]bool{}
+	backends := map[string]bool{}
+	policies := map[string]bool{}
+
+	for _, routeConfig := range httpRoutes {
+		spec, ok := routeConfig.Spec.(*gatewayv1.HTTPRouteSpec)
+		if !ok {
+			continue
+		}
+		for ruleIndex, rule := range spec.Rules {
+			if !ruleUsesDxgateService(rule) {
+				continue
+			}
+			if len(rule.BackendRefs) == 0 {
+				continue
+			}
+
+			var protocol route.AgentProtocol
+			var weighted []*route.WeightedBackend
+			valid := true
+			for _, ref := range rule.BackendRefs {
+				if !isDxgateServiceBackend(ref) {
+					log.Warnf("HTTPRoute %s/%s rule[%d] mixes Service and DxgateService backends", routeConfig.Namespace, routeConfig.Name, ruleIndex)
+					valid = false
+					break
+				}
+				namespace := routeConfig.Namespace
+				if ref.Namespace != nil {
+					namespace = string(*ref.Namespace)
+				}
+				if namespace != routeConfig.Namespace {
+					log.Warnf("HTTPRoute %s/%s rule[%d] cross-namespace DxgateService reference %s/%s is not supported", routeConfig.Namespace, routeConfig.Name, ruleIndex, namespace, ref.Name)
+					valid = false
+					break
+				}
+				serviceConfig, found := push.DxgateService(namespace, string(ref.Name))
+				if !found {
+					log.Warnf("HTTPRoute %s/%s rule[%d] references missing DxgateService %s/%s", routeConfig.Namespace, routeConfig.Name, ruleIndex, namespace, ref.Name)
+					valid = false
+					break
+				}
+				service, ok := serviceConfig.Spec.(*networking.DxgateService)
+				if !ok {
+					valid = false
+					break
+				}
+				compiled, serviceProtocol, err := compileDxgateService(serviceConfig, service)
+				if err != nil {
+					log.Warnf("DxgateService %s/%s cannot be compiled: %v", namespace, ref.Name, err)
+					valid = false
+					break
+				}
+				if protocol != route.AgentProtocol_AGENT_PROTOCOL_UNSPECIFIED && protocol != serviceProtocol {
+					log.Warnf("HTTPRoute %s/%s rule[%d] mixes DxgateService protocols", routeConfig.Namespace, routeConfig.Name, ruleIndex)
+					valid = false
+					break
+				}
+				protocol = serviceProtocol
+				if compiled.provider != nil && !providers[compiled.provider.Name] {
+					out.Providers = append(out.Providers, compiled.provider)
+					providers[compiled.provider.Name] = true
+				}
+				if compiled.policy != nil && !policies[compiled.policy.Name] {
+					out.Policies = append(out.Policies, compiled.policy)
+					policies[compiled.policy.Name] = true
+				}
+				weight := uint32(1)
+				if ref.Weight != nil && *ref.Weight > 0 {
+					weight = uint32(*ref.Weight)
+				}
+				for _, backend := range compiled.backends {
+					if !backends[backend.Name] {
+						out.Backends = append(out.Backends, backend)
+						backends[backend.Name] = true
+					}
+					weighted = append(weighted, &route.WeightedBackend{Name: backend.Name, Weight: weight})
+				}
+			}
+			if !valid || len(weighted) == 0 {
+				continue
+			}
+			out.AgentRoutes = append(out.AgentRoutes, &route.AgentRoute{
+				Name:             fmt.Sprintf("%s/%s/%d", routeConfig.Namespace, routeConfig.Name, ruleIndex),
+				Protocol:         protocol,
+				Matches:          compileAgentMatches(spec.Hostnames, rule.Matches),
+				WeightedBackends: weighted,
+				Policies:         policyNames(weighted, out.Backends),
+				Rewrite:          compileAgentRewrite(rule.Filters),
+			})
+		}
+	}
+	if len(out.AgentRoutes) == 0 {
+		return nil
+	}
+	sort.Slice(out.Providers, func(i, j int) bool { return out.Providers[i].Name < out.Providers[j].Name })
+	sort.Slice(out.Backends, func(i, j int) bool { return out.Backends[i].Name < out.Backends[j].Name })
+	sort.Slice(out.Policies, func(i, j int) bool { return out.Policies[i].Name < out.Policies[j].Name })
+	sort.Slice(out.AgentRoutes, func(i, j int) bool { return out.AgentRoutes[i].Name < out.AgentRoutes[j].Name })
+	return out
+}
+
+type compiledDxgateService struct {
+	provider *route.AgentProvider
+	backends []*route.AgentBackend
+	policy   *route.AgentPolicy
+}
+
+func compileDxgateService(cfg config.Config, service *networking.DxgateService) (compiledDxgateService, route.AgentProtocol, error) {
+	prefix := fmt.Sprintf("%s.%s", cfg.Name, cfg.Namespace)
+	policyName := prefix + ".policy"
+	policy := compileAgentPolicy(policyName, cfg.Namespace, service.GetPolicies())
+	policyRefs := []string(nil)
+	if policy != nil {
+		policyRefs = []string{policyName}
+	}
+
+	switch {
+	case service.GetAi() != nil:
+		ai := service.GetAi()
+		if ai.GetProvider() == nil {
+			return compiledDxgateService{}, 0, fmt.Errorf("provider is not set")
+		}
+		providerName := prefix + ".provider"
+		provider := &route.AgentProvider{
+			Name:           providerName,
+			BaseUrl:        ai.GetEndpoint(),
+			Credential:     compileSecretReference(cfg.Namespace, ai.GetProvider().GetCredential()),
+			RequestHeaders: compileHeaderValues(ai.GetProvider().GetRequestHeaders()),
+		}
+		switch {
+		case ai.GetProvider().GetOpenai() != nil:
+			provider.Kind = route.AgentProviderKind_OPENAI
+		case ai.GetProvider().GetAnthropic() != nil:
+			provider.Kind = route.AgentProviderKind_ANTHROPIC
+		default:
+			return compiledDxgateService{}, 0, fmt.Errorf("provider is not set")
+		}
+		backend := &route.AgentBackend{
+			Name: prefix,
+			Backend: &route.AgentBackend_Llm{Llm: &route.LLMBackend{
+				Provider:      providerName,
+				Models:        append([]string(nil), ai.GetModels()...),
+				ModelRewrites: cloneStringMap(ai.GetModelRewrites()),
+			}},
+			Policies: policyRefs,
+		}
+		return compiledDxgateService{provider: provider, backends: []*route.AgentBackend{backend}, policy: policy}, route.AgentProtocol_LLM, nil
+	case service.GetMcp() != nil:
+		backends := make([]*route.AgentBackend, 0, len(service.GetMcp().GetTargets()))
+		for _, target := range service.GetMcp().GetTargets() {
+			static := target.GetStatic()
+			if static == nil || static.GetBackendRef() == nil {
+				continue
+			}
+			namespace := static.GetBackendRef().GetNamespace()
+			if namespace == "" {
+				namespace = cfg.Namespace
+			}
+			backends = append(backends, &route.AgentBackend{
+				Name: fmt.Sprintf("%s.%s", prefix, target.GetName()),
+				Backend: &route.AgentBackend_Mcp{Mcp: &route.MCPBackend{
+					Endpoint: serviceEndpoint(static.GetBackendRef().GetName(), namespace, static.GetPort(), static.GetPath()),
+					Tools:    append([]string(nil), target.GetTools()...),
+				}},
+				Policies: policyRefs,
+			})
+		}
+		return compiledDxgateService{backends: backends, policy: policy}, route.AgentProtocol_MCP, nil
+	case service.GetA2A() != nil:
+		a2a := service.GetA2A()
+		endpoint := ""
+		if ref := a2a.GetBackendRef(); ref != nil {
+			namespace := ref.GetNamespace()
+			if namespace == "" {
+				namespace = cfg.Namespace
+			}
+			endpoint = serviceEndpoint(ref.GetName(), namespace, a2a.GetPort(), a2a.GetPath())
+		} else {
+			endpoint = hostEndpoint(a2a.GetHost(), a2a.GetPort(), a2a.GetPath())
+		}
+		backend := &route.AgentBackend{
+			Name: prefix,
+			Backend: &route.AgentBackend_A2A{A2A: &route.A2ABackend{
+				Endpoint: endpoint,
+				Agent:    a2a.GetAgent(),
+			}},
+			Policies: policyRefs,
+		}
+		return compiledDxgateService{backends: []*route.AgentBackend{backend}, policy: policy}, route.AgentProtocol_A2A, nil
+	default:
+		return compiledDxgateService{}, 0, fmt.Errorf("service type is not set")
+	}
+}
+
+func serviceEndpoint(name, namespace string, port uint32, path string) string {
+	return hostEndpoint(fmt.Sprintf("%s.%s.svc.cluster.local", name, namespace), port, path)
+}
+
+func hostEndpoint(host string, port uint32, path string) string {
+	return fmt.Sprintf("http://%s:%d%s", host, port, normalizeEndpointPath(path))
+}
+
+func normalizeEndpointPath(path string) string {
+	if path == "" || path == "/" {
+		return ""
+	}
+	return "/" + strings.Trim(path, "/")
+}
+
+func compileAgentMatches(hostnames []gatewayv1.Hostname, matches []gatewayv1.HTTPRouteMatch) []*route.AgentRouteMatch {
+	hosts := make([]string, 0, len(hostnames))
+	for _, hostname := range hostnames {
+		hosts = append(hosts, string(hostname))
+	}
+	if len(hosts) == 0 {
+		hosts = []string{""}
+	}
+	if len(matches) == 0 {
+		matches = []gatewayv1.HTTPRouteMatch{{}}
+	}
+	out := make([]*route.AgentRouteMatch, 0, len(hosts)*len(matches))
+	for _, hostname := range hosts {
+		for _, match := range matches {
+			compiled := &route.AgentRouteMatch{
+				Host:    hostname,
+				Path:    compileAgentPathMatch(match.Path),
+				Headers: make([]*route.AgentHeaderMatch, 0, len(match.Headers)),
+			}
+			if match.Method != nil {
+				compiled.Method = string(*match.Method)
+			}
+			for _, header := range match.Headers {
+				compiled.Headers = append(compiled.Headers, &route.AgentHeaderMatch{
+					Name: string(header.Name), Value: header.Value,
+				})
+			}
+			out = append(out, compiled)
+		}
+	}
+	return out
+}
+
+func compileAgentPathMatch(match *gatewayv1.HTTPPathMatch) *route.AgentPathMatch {
+	if match == nil || match.Value == nil {
+		return &route.AgentPathMatch{Match: &route.AgentPathMatch_Prefix{Prefix: "/"}}
+	}
+	if match.Type != nil && *match.Type == gatewayv1.PathMatchExact {
+		return &route.AgentPathMatch{Match: &route.AgentPathMatch_Exact{Exact: *match.Value}}
+	}
+	return &route.AgentPathMatch{Match: &route.AgentPathMatch_Prefix{Prefix: *match.Value}}
+}
+
+func compileAgentRewrite(filters []gatewayv1.HTTPRouteFilter) *route.PathRewrite {
+	for _, filter := range filters {
+		if filter.Type != gatewayv1.HTTPRouteFilterURLRewrite || filter.URLRewrite == nil || filter.URLRewrite.Path == nil {
+			continue
+		}
+		if filter.URLRewrite.Path.Type == gatewayv1.PrefixMatchHTTPPathModifier && filter.URLRewrite.Path.ReplacePrefixMatch != nil {
+			return &route.PathRewrite{ReplacePrefixMatch: *filter.URLRewrite.Path.ReplacePrefixMatch}
+		}
+	}
+	return nil
+}
+
+func compileAgentPolicy(name, namespace string, in *networking.DxgateServicePolicies) *route.AgentPolicy {
+	if in == nil {
+		return nil
+	}
+	out := &route.AgentPolicy{
+		Name:            name,
+		Timeout:         in.GetTimeout(),
+		MaxBodyBytes:    uint64(max(in.GetMaxBodyBytes(), 0)),
+		RequestHeaders:  compileHeaderTransform(in.GetRequestHeaders()),
+		ResponseHeaders: compileHeaderTransform(in.GetResponseHeaders()),
+	}
+	if auth := in.GetAuth(); auth != nil {
+		out.Auth = &route.ClientAuthPolicy{
+			Header:    auth.GetHeader(),
+			SecretRef: compileSecretReference(namespace, auth.GetSecretRef()),
+		}
+	}
+	if limit := in.GetRateLimit(); limit != nil {
+		out.RateLimit = &route.AgentRateLimitPolicy{
+			Requests: limit.GetRequests(), Window: limit.GetWindow(),
+			Key: compileRateLimitKey(limit.GetKey()), Header: limit.GetHeader(),
+		}
+	}
+	if limit := in.GetTokenLimit(); limit != nil {
+		out.TokenLimit = &route.AgentTokenLimitPolicy{
+			Tokens: limit.GetTokens(), Window: limit.GetWindow(),
+			Key: compileRateLimitKey(limit.GetKey()), Header: limit.GetHeader(),
+		}
+	}
+	if retry := in.GetRetry(); retry != nil {
+		out.Retry = &route.AgentRetryPolicy{
+			Attempts: retry.GetAttempts(), StatusCodes: append([]uint32(nil), retry.GetStatusCodes()...),
+		}
+	}
+	return out
+}
+
+func compileSecretReference(namespace string, ref *networking.SecretKeyReference) *route.SecretKeyReference {
+	if ref == nil {
+		return nil
+	}
+	return &route.SecretKeyReference{Namespace: namespace, Name: ref.GetName(), Key: ref.GetKey()}
+}
+
+func compileHeaderTransform(in *networking.HeaderTransform) *route.AgentHeaderTransform {
+	if in == nil {
+		return nil
+	}
+	return &route.AgentHeaderTransform{
+		Add:    compileHeaderValues(in.GetAdd()),
+		Remove: append([]string(nil), in.GetRemove()...),
+	}
+}
+
+func compileHeaderValues(in []*networking.HeaderValue) []*route.AgentHeaderValue {
+	out := make([]*route.AgentHeaderValue, 0, len(in))
+	for _, value := range in {
+		if value != nil {
+			out = append(out, &route.AgentHeaderValue{Name: value.GetName(), Value: value.GetValue()})
+		}
+	}
+	return out
+}
+
+func compileRateLimitKey(in networking.RateLimitKey) route.AgentRateLimitKey {
+	switch in {
+	case networking.RateLimitKey_BACKEND:
+		return route.AgentRateLimitKey_BACKEND
+	case networking.RateLimitKey_HEADER:
+		return route.AgentRateLimitKey_HEADER
+	default:
+		return route.AgentRateLimitKey_ROUTE
+	}
+}
+
+func policyNames(weighted []*route.WeightedBackend, backends []*route.AgentBackend) []string {
+	seen := map[string]bool{}
+	var out []string
+	for _, selected := range weighted {
+		for _, backend := range backends {
+			if backend.Name != selected.Name {
+				continue
+			}
+			for _, policy := range backend.Policies {
+				if !seen[policy] {
+					out = append(out, policy)
+					seen[policy] = true
+				}
+			}
+		}
+	}
+	sort.Strings(out)
+	return out
+}
+
+func cloneStringMap(in map[string]string) map[string]string {
+	if len(in) == 0 {
+		return nil
+	}
+	out := make(map[string]string, len(in))
+	for key, value := range in {
+		out[key] = value
+	}
+	return out
+}
diff --git a/dubbod/discovery/pkg/networking/grpcgen/rds.go b/dubbod/discovery/pkg/networking/grpcgen/rds.go
index 7a7df64e..9a718dd8 100644
--- a/dubbod/discovery/pkg/networking/grpcgen/rds.go
+++ b/dubbod/discovery/pkg/networking/grpcgen/rds.go
@@ -241,6 +241,7 @@ func buildHTTPRoute(node *model.Proxy, push *model.PushContext, routeName string
 
 		// Filter HTTPRoutes by parentRef to match this Gateway
 		httpRoutes := filterHTTPRoutesByGateway(allHTTPRoutes, gatewayName, gatewayNamespace, gatewayListenerPort)
+		agentConfig := buildAgentConfig(push, httpRoutes)
 		log.Debugf("Gateway Pod inbound listener, filtered to %d HTTPRoute(s) matching gateway %s/%s listener port %d", len(httpRoutes), gatewayNamespace, gatewayName, gatewayListenerPort)
 
 		// For Gateway Pod, we also need to collect HTTPRoutes with specific hostnames
@@ -313,6 +314,7 @@ func buildHTTPRoute(node *model.Proxy, push *model.PushContext, routeName string
 		return &route.RouteConfiguration{
 			Name:         routeName,
 			VirtualHosts: virtualHosts,
+			AgentConfig:  agentConfig,
 		}
 	}
 
@@ -429,6 +431,13 @@ func buildRoutesFromGatewayHTTPRoute(httpRoutes []config.Config, hostName host.N
 				log.Debugf("HTTPRoute %s/%s rule[%d] has no backendRefs, skipping", hrConfig.Namespace, hrConfig.Name, ruleIdx)
 				continue
 			}
+			if ruleUsesDxgateService(rule) {
+				// Mesh-native LLM/MCP/A2A backends are carried in AgentConfig.
+				// Emitting them as ordinary clusters would fabricate a
+				// Kubernetes Service with the same name and bypass protocol
+				// translation.
+				continue
+			}
 
 			// Build weighted clusters from backendRefs
 			weights := make([]*route.WeightedCluster_ClusterWeight, 0, len(rule.BackendRefs))
@@ -483,9 +492,6 @@ func buildRoutesFromGatewayHTTPRoute(httpRoutes []config.Config, hostName host.N
 				weightedClusters.TotalWeight = wrapperspb.UInt32(totalWeight)
 			}
 
-			// Build route match from HTTPRoute matches
-			routeMatch := buildRouteMatchFromHTTPRouteMatches(rule.Matches)
-
 			routeAction := &route.RouteAction{
 				ClusterSpecifier: &route.RouteAction_WeightedClusters{
 					WeightedClusters: weightedClusters,
@@ -499,16 +505,18 @@ func buildRoutesFromGatewayHTTPRoute(httpRoutes []config.Config, hostName host.N
 			routeAction.RetryPolicy = gatewayAPIRetryPolicy(rule.Retry, rule.Timeouts)
 			routeAction.FaultPolicy = faultPolicy
 
-			builtRoute := &route.Route{
-				Match: routeMatch,
-				Action: &route.Route_Route{
-					Route: routeAction,
-				},
+			routeMatches := buildRouteMatchesFromHTTPRouteMatches(rule.Matches)
+			for _, routeMatch := range routeMatches {
+				allRoutes = append(allRoutes, &route.Route{
+					Match: routeMatch,
+					Action: &route.Route_Route{
+						Route: routeAction,
+					},
+				})
 			}
 
-			log.Infof("HTTPRoute %s/%s rule[%d] -> built route with %d clusters, totalWeight=%d",
-				hrConfig.Namespace, hrConfig.Name, ruleIdx, len(weights), totalWeight)
-			allRoutes = append(allRoutes, builtRoute)
+			log.Infof("HTTPRoute %s/%s rule[%d] -> built %d routes with %d clusters, totalWeight=%d",
+				hrConfig.Namespace, hrConfig.Name, ruleIdx, len(routeMatches), len(weights), totalWeight)
 		}
 	}
 
@@ -701,19 +709,21 @@ func isServiceParentRef(parentRef sigsk8siogatewayapiapisv1.ParentReference) boo
 	return parentRef.Group == nil || string(*parentRef.Group) == ""
 }
 
-// buildRouteMatchFromHTTPRouteMatches converts Gateway API HTTPRouteMatch to XDS RouteMatch
-func buildRouteMatchFromHTTPRouteMatches(matches []sigsk8siogatewayapiapisv1.HTTPRouteMatch) *route.RouteMatch {
+// buildRouteMatchesFromHTTPRouteMatches preserves Gateway API OR semantics:
+// every HTTPRouteMatch in a rule becomes an independent xDS route.
+func buildRouteMatchesFromHTTPRouteMatches(matches []sigsk8siogatewayapiapisv1.HTTPRouteMatch) []*route.RouteMatch {
 	if len(matches) == 0 {
-		// No matches means match all
-		return &route.RouteMatch{
-			PathSpecifier: &route.RouteMatch_Prefix{
-				Prefix: "/",
-			},
-		}
+		matches = []sigsk8siogatewayapiapisv1.HTTPRouteMatch{{}}
 	}
 
-	// For now, we'll use the first match. In a full implementation, we might need to merge multiple matches.
-	match := matches[0]
+	out := make([]*route.RouteMatch, 0, len(matches))
+	for _, match := range matches {
+		out = append(out, buildRouteMatchFromHTTPRouteMatch(match))
+	}
+	return out
+}
+
+func buildRouteMatchFromHTTPRouteMatch(match sigsk8siogatewayapiapisv1.HTTPRouteMatch) *route.RouteMatch {
 	routeMatch := &route.RouteMatch{}
 
 	// Handle path match
diff --git a/dubbod/discovery/pkg/networking/grpcgen/rds_test.go b/dubbod/discovery/pkg/networking/grpcgen/rds_test.go
index d2b035c9..babdd1dd 100644
--- a/dubbod/discovery/pkg/networking/grpcgen/rds_test.go
+++ b/dubbod/discovery/pkg/networking/grpcgen/rds_test.go
@@ -17,6 +17,7 @@ package grpcgen
 
 import (
 	"reflect"
+	"strings"
 	"testing"
 	"time"
 
@@ -37,6 +38,26 @@ import (
 	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 )
 
+func TestBuildRouteMatchesPreservesHTTPRouteORSemantics(t *testing.T) {
+	pathType := gatewayv1.PathMatchPathPrefix
+	users := "/users"
+	orders := "/orders"
+	matches := buildRouteMatchesFromHTTPRouteMatches([]gatewayv1.HTTPRouteMatch{
+		{Path: &gatewayv1.HTTPPathMatch{Type: &pathType, Value: &users}},
+		{Path: &gatewayv1.HTTPPathMatch{Type: &pathType, Value: &orders}},
+	})
+
+	if len(matches) != 2 {
+		t.Fatalf("matches = %d, want 2 independent xDS routes", len(matches))
+	}
+	if got := matches[0].GetPrefix(); got != users {
+		t.Fatalf("first prefix = %q, want %q", got, users)
+	}
+	if got := matches[1].GetPrefix(); got != orders {
+		t.Fatalf("second prefix = %q, want %q", got, orders)
+	}
+}
+
 func TestBuildHTTPRouteProxylessOutboundIgnoresGatewayAttachedHTTPRoute(t *testing.T) {
 	push := newRDSTestPushContext(t, []config.Config{
 		newWildcardHTTPRouteConfig("httpbin", "default", 8000),
@@ -335,6 +356,108 @@ func TestGatewayInboundTargetPortIncludesActivationRoutes(t *testing.T) {
 	t.Fatalf("activation virtual host not found on Gateway targetPort: %v", rc.GetVirtualHosts())
 }
 
+func TestBuildAgentConfigCompilesDxgateServiceHTTPRoute(t *testing.T) {
+	routeConfig := newDxgateHTTPRouteConfig("anthropic", "app", "/anthropic", "/v1/chat/completions")
+	serviceConfig := config.Config{
+		Meta: config.Meta{
+			GroupVersionKind: gvk.DxgateService,
+			Name:             "anthropic",
+			Namespace:        "app",
+		},
+		Spec: &networking.DxgateService{
+			Service: &networking.DxgateService_Ai{Ai: &networking.AIService{
+				Provider: &networking.AIProvider{
+					Provider: &networking.AIProvider_Anthropic{Anthropic: &networking.AnthropicProvider{
+						Model: "claude-test",
+					}},
+					Credential: &networking.SecretKeyReference{Name: "anthropic", Key: "api-key"},
+				},
+				Models:   []string{"claude-test"},
+				Endpoint: "http://anthropic-mock.app.svc:8080",
+			}},
+			Policies: &networking.DxgateServicePolicies{
+				Timeout: durationpb.New(5 * time.Second),
+				Retry:   &networking.RetryPolicy{Attempts: 2, StatusCodes: []uint32{503}},
+			},
+		},
+	}
+	push := newRDSTestPushContext(t, []config.Config{routeConfig, serviceConfig}, nil)
+
+	got := buildAgentConfig(push, []config.Config{routeConfig})
+	if got == nil {
+		t.Fatal("AgentConfig = nil")
+	}
+	if len(got.GetProviders()) != 1 || got.GetProviders()[0].GetKind() != route.AgentProviderKind_ANTHROPIC {
+		t.Fatalf("providers = %v, want one Anthropic provider", got.GetProviders())
+	}
+	if credential := got.GetProviders()[0].GetCredential(); credential.GetNamespace() != "app" ||
+		credential.GetName() != "anthropic" || credential.GetKey() != "api-key" {
+		t.Fatalf("credential = %v", credential)
+	}
+	if len(got.GetBackends()) != 1 || got.GetBackends()[0].GetLlm() == nil {
+		t.Fatalf("backends = %v, want one LLM backend", got.GetBackends())
+	}
+	if len(got.GetAgentRoutes()) != 1 {
+		t.Fatalf("agent routes = %d, want 1", len(got.GetAgentRoutes()))
+	}
+	agentRoute := got.GetAgentRoutes()[0]
+	if agentRoute.GetProtocol() != route.AgentProtocol_LLM {
+		t.Fatalf("protocol = %v, want LLM", agentRoute.GetProtocol())
+	}
+	if got := agentRoute.GetMatches()[0].GetPath().GetPrefix(); got != "/anthropic" {
+		t.Fatalf("path prefix = %q, want /anthropic", got)
+	}
+	if got := agentRoute.GetRewrite().GetReplacePrefixMatch(); got != "/v1/chat/completions" {
+		t.Fatalf("rewrite prefix = %q, want /v1/chat/completions", got)
+	}
+	if got := got.GetPolicies()[0].GetRetry().GetAttempts(); got != 2 {
+		t.Fatalf("retry attempts = %d, want 2", got)
+	}
+}
+
+func TestBuildAgentConfigExpandsMCPFederationTargets(t *testing.T) {
+	routeConfig := newDxgateHTTPRouteConfig("company-tools", "app", "/mcp", "")
+	serviceConfig := config.Config{
+		Meta: config.Meta{
+			GroupVersionKind: gvk.DxgateService,
+			Name:             "company-tools",
+			Namespace:        "app",
+		},
+		Spec: &networking.DxgateService{
+			Service: &networking.DxgateService_Mcp{Mcp: &networking.MCPService{
+				Targets: []*networking.MCPTarget{
+					{
+						Name: "orders",
+						Static: &networking.StaticBackend{
+							BackendRef: &networking.BackendReference{Name: "orders-tools"},
+							Port:       8080,
+						},
+					},
+					{
+						Name: "tickets",
+						Static: &networking.StaticBackend{
+							BackendRef: &networking.BackendReference{Name: "tickets-tools"},
+							Port:       8081,
+						},
+					},
+				},
+			}},
+		},
+	}
+	push := newRDSTestPushContext(t, []config.Config{routeConfig, serviceConfig}, nil)
+
+	got := buildAgentConfig(push, []config.Config{routeConfig})
+	if got == nil || len(got.GetBackends()) != 2 {
+		t.Fatalf("backends = %v, want 2 MCP targets", got.GetBackends())
+	}
+	if len(got.GetAgentRoutes()) != 1 || len(got.GetAgentRoutes()[0].GetWeightedBackends()) != 2 {
+		t.Fatalf("agent routes = %v, want one federated route", got.GetAgentRoutes())
+	}
+	if endpoint := got.GetBackends()[0].GetMcp().GetEndpoint(); !strings.Contains(endpoint, ".app.svc.cluster.local:") {
+		t.Fatalf("MCP endpoint = %q, want in-cluster Service DNS", endpoint)
+	}
+}
+
 func newRDSTestPushContext(t *testing.T, configs []config.Config, services []*model.Service) *model.PushContext {
 	t.Helper()
 
@@ -358,6 +481,53 @@ func newRDSTestPushContext(t *testing.T, configs []config.Config, services []*mo
 	return push
 }
 
+func newDxgateHTTPRouteConfig(backendName, namespace, path, rewrite string) config.Config {
+	group := gatewayv1.Group(dxgateServiceGroup)
+	kind := gatewayv1.Kind(dxgateServiceKind)
+	pathType := gatewayv1.PathMatchPathPrefix
+	method := gatewayv1.HTTPMethodPost
+	weight := int32(100)
+	rule := gatewayv1.HTTPRouteRule{
+		Matches: []gatewayv1.HTTPRouteMatch{{
+			Path:   &gatewayv1.HTTPPathMatch{Type: &pathType, Value: &path},
+			Method: &method,
+		}},
+		BackendRefs: []gatewayv1.HTTPBackendRef{{
+			BackendRef: gatewayv1.BackendRef{
+				BackendObjectReference: gatewayv1.BackendObjectReference{
+					Group: &group,
+					Kind:  &kind,
+					Name:  gatewayv1.ObjectName(backendName),
+				},
+				Weight: &weight,
+			},
+		}},
+	}
+	if rewrite != "" {
+		modifier := gatewayv1.PrefixMatchHTTPPathModifier
+		rule.Filters = []gatewayv1.HTTPRouteFilter{{
+			Type: gatewayv1.HTTPRouteFilterURLRewrite,
+			URLRewrite: &gatewayv1.HTTPURLRewriteFilter{
+				Path: &gatewayv1.HTTPPathModifier{
+					Type:               modifier,
+					ReplacePrefixMatch: &rewrite,
+				},
+			},
+		}}
+	}
+	return config.Config{
+		Meta: config.Meta{
+			GroupVersionKind: gvk.HTTPRoute,
+			Name:             backendName,
+			Namespace:        namespace,
+			Domain:           "cluster.local",
+		},
+		Spec: &gatewayv1.HTTPRouteSpec{
+			Rules: []gatewayv1.HTTPRouteRule{rule},
+		},
+	}
+}
+
 func newWildcardHTTPRouteConfig(backendName, backendNamespace string, backendPort int32) config.Config {
 	port := backendPort
 	weight := int32(1)
diff --git a/go.mod b/go.mod
index ad044fb3..d44a074f 100644
--- a/go.mod
+++ b/go.mod
@@ -52,9 +52,9 @@ require (
 	github.com/hashicorp/go-multierror v1.1.1
 	github.com/hashicorp/golang-lru/v2 v2.0.7
 	github.com/heroku/color v0.0.6
-	github.com/kdubbo/api v0.0.0-20260808071541-7709b6f3f491
-	github.com/kdubbo/client-go v0.0.0-20260807013041-7abbf3125711
-	github.com/kdubbo/xds-api v0.0.0-20260808063945-44451fbf2da1
+	github.com/kdubbo/api v0.0.0-20260808154749-d91601e19406
+	github.com/kdubbo/client-go v0.0.0-20260808154904-3acbf91635a1
+	github.com/kdubbo/xds-api v0.0.0-20260808155041-b6b8371fcc89
 	github.com/moby/moby/client v0.4.1
 	github.com/moby/term v0.5.2
 	github.com/ory/viper v1.7.5
diff --git a/go.sum b/go.sum
index 1c07d989..5e956c1c 100644
--- a/go.sum
+++ b/go.sum
@@ -368,12 +368,12 @@ github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfV
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
-github.com/kdubbo/api v0.0.0-20260808071541-7709b6f3f491 h1:KukdrR5lEbLUqh45vTPJnGLTusT9dCoro/LkiSvF+dk=
-github.com/kdubbo/api v0.0.0-20260808071541-7709b6f3f491/go.mod h1:8BtJiIovg7QCPsCxXcw3gDf922VcvYq5ihOSvj49Rq8=
-github.com/kdubbo/client-go v0.0.0-20260807013041-7abbf3125711 h1:ofQz1xUC5DZyh+3lVPIFdipjKxe6jQ9uQbNMYoLInaI=
-github.com/kdubbo/client-go v0.0.0-20260807013041-7abbf3125711/go.mod h1:ogHgHroSROD3HwYPBKS8c2dpcfd4cZxpwZrzTSCXjXs=
-github.com/kdubbo/xds-api v0.0.0-20260808063945-44451fbf2da1 h1:s/M95tB0/0Pf0icg9qCiK7zEWFsUt0F+8r3MYP3bUCQ=
-github.com/kdubbo/xds-api v0.0.0-20260808063945-44451fbf2da1/go.mod h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
+github.com/kdubbo/api v0.0.0-20260808154749-d91601e19406 h1:HlD3yE/A+aHwXNIMydXMtdCTJyLukk8Znv+fo2pX8NA=
+github.com/kdubbo/api v0.0.0-20260808154749-d91601e19406/go.mod h1:8BtJiIovg7QCPsCxXcw3gDf922VcvYq5ihOSvj49Rq8=
+github.com/kdubbo/client-go v0.0.0-20260808154904-3acbf91635a1 h1:ykfiDq3lqlBJp8r30xJIJC+E2amT0GQVQep62+lhdzo=
+github.com/kdubbo/client-go v0.0.0-20260808154904-3acbf91635a1/go.mod h1:euRf0fpvc8PfHmIwb3Oj/b42aJDhMVX7PHAYzSZjpU8=
+github.com/kdubbo/xds-api v0.0.0-20260808155041-b6b8371fcc89 h1:HVDXIqVzomm6eHAaitmaB3JGPd7w7BhSg5ns1ObLvzo=
+github.com/kdubbo/xds-api v0.0.0-20260808155041-b6b8371fcc89/go.mod h1:o2HDUgL1ntaDbWomZ4cD2tt8jBamuG2qRtjXOa1zZ0Q=
 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
diff --git a/manifests/charts/base/files/crd-all.gen.yaml b/manifests/charts/base/files/crd-all.gen.yaml
index 34c42e30..21f52ae6 100644
--- a/manifests/charts/base/files/crd-all.gen.yaml
+++ b/manifests/charts/base/files/crd-all.gen.yaml
@@ -152,6 +152,371 @@ spec:
 ---
 apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
+metadata:
+  annotations:
+    "helm.sh/resource-policy": keep
+  labels:
+    app: dubbo
+    chart: dubbo
+    dubbo: networking
+    heritage: Tiller
+    release: dubbo
+  name: dxgateservices.networking.dubbo.apache.org
+spec:
+  group: networking.dubbo.apache.org
+  names:
+    categories:
+    - dubbo
+    - networking
+    kind: DxgateService
+    listKind: DxgateServiceList
+    plural: dxgateservices
+    shortNames:
+    - dxsvc
+    singular: dxgateservice
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - description: CreationTimestamp is a timestamp representing the server time when
+        this object was created.
+      jsonPath: .metadata.creationTimestamp
+      name: Age
+      type: date
+    name: v1alpha3
+    schema:
+      openAPIV3Schema:
+        properties:
+          spec:
+            description: 'Mesh-native LLM, MCP, or A2A backend referenced by Gateway
+              API routes. See more details at: '
+            oneOf:
+            - not:
+                anyOf:
+                - required:
+                  - ai
+                - required:
+                  - mcp
+                - required:
+                  - a2a
+            - required:
+              - ai
+            - required:
+              - mcp
+            - required:
+              - a2a
+            properties:
+              a2a:
+                properties:
+                  agent:
+                    type: string
+                  backendRef:
+                    description: Prefer backend_ref for an in-cluster agent.
+                    properties:
+                      name:
+                        description: Kubernetes Service name.
+                        type: string
+                      namespace:
+                        description: Defaults to the DxgateService namespace.
+                        type: string
+                    required:
+                    - name
+                    type: object
+                  host:
+                    type: string
+                  path:
+                    type: string
+                  port:
+                    maximum: 4294967295
+                    minimum: 0
+                    type: integer
+                type: object
+              ai:
+                properties:
+                  endpoint:
+                    description: Optional backend URL.
+                    type: string
+                  modelRewrites:
+                    additionalProperties:
+                      type: string
+                    description: Client model alias to upstream model name.
+                    type: object
+                  models:
+                    description: Models accepted by this backend.
+                    items:
+                      type: string
+                    type: array
+                  provider:
+                    oneOf:
+                    - not:
+                        anyOf:
+                        - required:
+                          - openai
+                        - required:
+                          - anthropic
+                    - required:
+                      - openai
+                    - required:
+                      - anthropic
+                    properties:
+                      anthropic:
+                        properties:
+                          model:
+                            description: Default upstream model.
+                            type: string
+                        type: object
+                      credential:
+                        description: Provider API key.
+                        properties:
+                          key:
+                            type: string
+                          name:
+                            type: string
+                        required:
+                        - name
+                        - key
+                        type: object
+                      openai:
+                        type: object
+                      requestHeaders:
+                        items:
+                          properties:
+                            name:
+                              type: string
+                            value:
+                              type: string
+                          required:
+                          - name
+                          - value
+                          type: object
+                        type: array
+                    type: object
+                  routes:
+                    additionalProperties:
+                      description: |2-
+
+
+                        Valid Options: COMPLETIONS, RESPONSES, EMBEDDINGS, PASSTHROUGH
+                      enum:
+                      - AI_OPERATION_UNSPECIFIED
+                      - COMPLETIONS
+                      - RESPONSES
+                      - EMBEDDINGS
+                      - PASSTHROUGH
+                      type: string
+                    description: OpenAI-compatible operations explicitly enabled for
+                      this service.
+                    type: object
+                required:
+                - provider
+                type: object
+              mcp:
+                properties:
+                  targets:
+                    items:
+                      properties:
+                        name:
+                          type: string
+                        static:
+                          properties:
+                            backendRef:
+                              properties:
+                                name:
+                                  description: Kubernetes Service name.
+                                  type: string
+                                namespace:
+                                  description: Defaults to the DxgateService namespace.
+                                  type: string
+                              required:
+                              - name
+                              type: object
+                            path:
+                              type: string
+                            port:
+                              maximum: 4294967295
+                              minimum: 0
+                              type: integer
+                          required:
+                          - backendRef
+                          - port
+                          type: object
+                        tools:
+                          items:
+                            type: string
+                          type: array
+                      required:
+                      - name
+                      - static
+                      type: object
+                    type: array
+                required:
+                - targets
+                type: object
+              policies:
+                description: Policies applied to every HTTPRoute reference to this
+                  service.
+                properties:
+                  auth:
+                    description: Client authentication at the gateway.
+                    properties:
+                      header:
+                        type: string
+                      secretRef:
+                        properties:
+                          key:
+                            type: string
+                          name:
+                            type: string
+                        required:
+                        - name
+                        - key
+                        type: object
+                    required:
+                    - secretRef
+                    type: object
+                  maxBodyBytes:
+                    format: int64
+                    type: integer
+                  rateLimit:
+                    properties:
+                      header:
+                        type: string
+                      key:
+                        description: |2-
+
+
+                          Valid Options: ROUTE, BACKEND, HEADER
+                        enum:
+                        - RATE_LIMIT_KEY_UNSPECIFIED
+                        - ROUTE
+                        - BACKEND
+                        - HEADER
+                        type: string
+                      requests:
+                        maximum: 4294967295
+                        minimum: 0
+                        type: integer
+                      window:
+                        type: string
+                        x-kubernetes-validations:
+                        - message: must be a valid duration greater than 1ms
+                          rule: duration(self) >= duration('1ms')
+                    required:
+                    - requests
+                    - window
+                    type: object
+                  requestHeaders:
+                    properties:
+                      add:
+                        items:
+                          properties:
+                            name:
+                              type: string
+                            value:
+                              type: string
+                          required:
+                          - name
+                          - value
+                          type: object
+                        type: array
+                      remove:
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                  responseHeaders:
+                    properties:
+                      add:
+                        items:
+                          properties:
+                            name:
+                              type: string
+                            value:
+                              type: string
+                          required:
+                          - name
+                          - value
+                          type: object
+                        type: array
+                      remove:
+                        items:
+                          type: string
+                        type: array
+                    type: object
+                  retry:
+                    properties:
+                      attempts:
+                        maximum: 4294967295
+                        minimum: 0
+                        type: integer
+                      statusCodes:
+                        items:
+                          maximum: 4294967295
+                          minimum: 0
+                          type: integer
+                        type: array
+                    type: object
+                  timeout:
+                    type: string
+                    x-kubernetes-validations:
+                    - message: must be a valid duration greater than 1ms
+                      rule: duration(self) >= duration('1ms')
+                  tokenLimit:
+                    properties:
+                      header:
+                        type: string
+                      key:
+                        description: |2-
+
+
+                          Valid Options: ROUTE, BACKEND, HEADER
+                        enum:
+                        - RATE_LIMIT_KEY_UNSPECIFIED
+                        - ROUTE
+                        - BACKEND
+                        - HEADER
+                        type: string
+                      tokens:
+                        minimum: 0
+                        type: integer
+                      window:
+                        type: string
+                        x-kubernetes-validations:
+                        - message: must be a valid duration greater than 1ms
+                          rule: duration(self) >= duration('1ms')
+                    required:
+                    - tokens
+                    - window
+                    type: object
+                type: object
+            type: object
+          status:
+            properties:
+              conditions:
+                items:
+                  properties:
+                    observedGeneration:
+                      anyOf:
+                      - type: integer
+                      - type: string
+                      x-kubernetes-int-or-string: true
+                    reason:
+                      type: string
+                    status:
+                      type: string
+                    type:
+                      type: string
+                  type: object
+                type: array
+            type: object
+            x-kubernetes-preserve-unknown-fields: true
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
 metadata:
   annotations:
     "helm.sh/resource-policy": keep
diff --git a/manifests/charts/dubbod/files/kube-gateway.yaml b/manifests/charts/dubbod/files/kube-gateway.yaml
index 3e3ab51c..c2beb796 100644
--- a/manifests/charts/dubbod/files/kube-gateway.yaml
+++ b/manifests/charts/dubbod/files/kube-gateway.yaml
@@ -37,7 +37,41 @@ metadata:
     app.kubernetes.io/instance: {{ .DeploymentName }}
     app.kubernetes.io/managed-by: dubbod
     gateway.networking.k8s.io/gateway-name: {{ .Gateway.Name }}
-automountServiceAccountToken: false
+automountServiceAccountToken: true
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ .ServiceAccount }}-credentials
+  namespace: {{ .Gateway.Namespace }}
+  labels:
+    app.kubernetes.io/name: dxgate
+    app.kubernetes.io/instance: {{ .DeploymentName }}
+    app.kubernetes.io/managed-by: dubbod
+    gateway.networking.k8s.io/gateway-name: {{ .Gateway.Name }}
+rules:
+- apiGroups: [""]
+  resources: ["secrets"]
+  verbs: ["get"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  name: {{ .ServiceAccount }}-credentials
+  namespace: {{ .Gateway.Namespace }}
+  labels:
+    app.kubernetes.io/name: dxgate
+    app.kubernetes.io/instance: {{ .DeploymentName }}
+    app.kubernetes.io/managed-by: dubbod
+    gateway.networking.k8s.io/gateway-name: {{ .Gateway.Name }}
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: {{ .ServiceAccount }}-credentials
+subjects:
+- kind: ServiceAccount
+  name: {{ .ServiceAccount }}
+  namespace: {{ .Gateway.Namespace }}
 ---
 apiVersion: apps/v1
 kind: Deployment
diff --git a/manifests/charts/dubbod/templates/clusterrole.yaml b/manifests/charts/dubbod/templates/clusterrole.yaml
index 751e0f2b..80b23939 100644
--- a/manifests/charts/dubbod/templates/clusterrole.yaml
+++ b/manifests/charts/dubbod/templates/clusterrole.yaml
@@ -113,6 +113,18 @@ rules:
       - update
       - patch
       - delete
+  - apiGroups: ["rbac.authorization.k8s.io"]
+    resources:
+      - roles
+      - rolebindings
+    verbs:
+      - get
+      - list
+      - watch
+      - create
+      - update
+      - patch
+      - delete
   - apiGroups: ["apps"]
     resources:
       - deployments
diff --git a/manifests/charts/dubbod/templates/configmap-injector.yaml b/manifests/charts/dubbod/templates/configmap-injector.yaml
index 80a9b1b3..0d4a9f1a 100644
--- a/manifests/charts/dubbod/templates/configmap-injector.yaml
+++ b/manifests/charts/dubbod/templates/configmap-injector.yaml
@@ -13,6 +13,22 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+{{- $defaults := .Values._internal_default_values_not_set | default dict }}
+{{- $defaultGlobal := $defaults.global | default dict }}
+{{- $defaultProxyless := $defaultGlobal.proxyless | default dict }}
+{{- $defaultCNI := $defaultProxyless.cni | default dict }}
+{{- $global := .Values.global | default dict }}
+{{- $proxyless := $global.proxyless | default dict }}
+{{- $cni := $proxyless.cni | default dict }}
+{{- $image := coalesce $cni.image $defaultCNI.image (printf "ghcr.io/apache/dubbo-kubernetes/dubbod:%s" .Chart.AppVersion) }}
+{{- $injectorValues := deepCopy (omit .Values "_internal_default_values_not_set") }}
+{{- $injectorGlobal := get $injectorValues "global" | default dict }}
+{{- $_ := set $injectorValues "global" $injectorGlobal }}
+{{- $injectorProxyless := get $injectorGlobal "proxyless" | default dict }}
+{{- $_ = set $injectorGlobal "proxyless" $injectorProxyless }}
+{{- $injectorCNI := get $injectorProxyless "cni" | default dict }}
+{{- $_ = set $injectorProxyless "cni" $injectorCNI }}
+{{- $_ = set $injectorCNI "image" $image }}
 apiVersion: v1
 kind: ConfigMap
 metadata:
@@ -31,3 +47,4 @@ data:
       gateway: |
 {{ .Files.Get "files/kube-gateway.yaml" | trim | indent 8 }}
   values: |-
+{{ $injectorValues | toYaml | indent 4 }}
diff --git a/pkg/config/schema/collections/collections.gen.go b/pkg/config/schema/collections/collections.gen.go
index d1a08f35..9d82b78a 100755
--- a/pkg/config/schema/collections/collections.gen.go
+++ b/pkg/config/schema/collections/collections.gen.go
@@ -137,6 +137,21 @@ var (
 		ValidateProto: validation.EmptyValidate,
 	}.MustBuild()
 
+	DxgateService = resource.Builder{
+		Identifier: "DxgateService",
+		Group:      "networking.dubbo.apache.org",
+		Kind:       "DxgateService",
+		Plural:     "dxgateservices",
+		Version:    "v1alpha3",
+		Proto:      "dubbo.networking.v1alpha3.DxgateService", StatusProto: "dubbo.meta.v1alpha1.DubboStatus",
+		ReflectType: reflect.TypeOf(&githubcomkdubboapinetworkingv1alpha3.DxgateService{}).Elem(), StatusType: reflect.TypeOf(&githubcomkdubboapimetav1alpha1.DubboStatus{}).Elem(),
+		ProtoPackage: "github.com/kdubbo/api/networking/v1alpha3", StatusPackage: "github.com/kdubbo/api/meta/v1alpha1",
+		ClusterScoped: false,
+		Synthetic:     false,
+		Builtin:       false,
+		ValidateProto: validation.ValidateDxgateService,
+	}.MustBuild()
+
 	EndpointSlice = resource.Builder{
 		Identifier:    "EndpointSlice",
 		Group:         "discovery.k8s.io",
@@ -548,6 +563,7 @@ var (
 		MustAdd(CustomResourceDefinition).
 		MustAdd(DaemonSet).
 		MustAdd(Deployment).
+		MustAdd(DxgateService).
 		MustAdd(EndpointSlice).
 		MustAdd(Endpoints).
 		MustAdd(FaultInjectionPolicy).
@@ -607,6 +623,7 @@ var (
 	Dubbo = collection.NewSchemasBuilder().
 		MustAdd(AuthorizationPolicy).
 		MustAdd(CircuitBreakerPolicy).
+		MustAdd(DxgateService).
 		MustAdd(FaultInjectionPolicy).
 		MustAdd(PeerAuthentication).
 		MustAdd(RequestAuthentication).
@@ -621,6 +638,7 @@ var (
 			MustAdd(AuthorizationPolicy).
 			MustAdd(BackendTLSPolicy).
 			MustAdd(CircuitBreakerPolicy).
+			MustAdd(DxgateService).
 			MustAdd(FaultInjectionPolicy).
 			MustAdd(GatewayClass).
 			MustAdd(HTTPRoute).
@@ -639,6 +657,7 @@ var (
 				MustAdd(AuthorizationPolicy).
 				MustAdd(BackendTLSPolicy).
 				MustAdd(CircuitBreakerPolicy).
+				MustAdd(DxgateService).
 				MustAdd(FaultInjectionPolicy).
 				MustAdd(GatewayClass).
 				MustAdd(HTTPRoute).
diff --git a/pkg/config/schema/gvk/resources.gen.go b/pkg/config/schema/gvk/resources.gen.go
index 90b93612..b094ac5c 100755
--- a/pkg/config/schema/gvk/resources.gen.go
+++ b/pkg/config/schema/gvk/resources.gen.go
@@ -18,6 +18,7 @@ var (
 	CustomResourceDefinition       = config.GroupVersionKind{Group: "apiextensions.k8s.io", Version: "v1", Kind: "CustomResourceDefinition"}
 	DaemonSet                      = config.GroupVersionKind{Group: "apps", Version: "v1", Kind: "DaemonSet"}
 	Deployment                     = config.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
+	DxgateService                  = config.GroupVersionKind{Group: "networking.dubbo.apache.org", Version: "v1alpha3", Kind: "DxgateService"}
 	EndpointSlice                  = config.GroupVersionKind{Group: "discovery.k8s.io", Version: "v1", Kind: "EndpointSlice"}
 	Endpoints                      = config.GroupVersionKind{Group: "", Version: "v1", Kind: "Endpoints"}
 	FaultInjectionPolicy           = config.GroupVersionKind{Group: "networking.dubbo.apache.org", Version: "v1alpha3", Kind: "FaultInjectionPolicy"}
@@ -69,6 +70,8 @@ func ToGVR(g config.GroupVersionKind) (schema.GroupVersionResource, bool) {
 		return gvr.DaemonSet, true
 	case Deployment:
 		return gvr.Deployment, true
+	case DxgateService:
+		return gvr.DxgateService, true
 	case EndpointSlice:
 		return gvr.EndpointSlice, true
 	case Endpoints:
@@ -150,6 +153,8 @@ func MustToKind(g config.GroupVersionKind) kind.Kind {
 		return kind.DaemonSet
 	case Deployment:
 		return kind.Deployment
+	case DxgateService:
+		return kind.DxgateService
 	case EndpointSlice:
 		return kind.EndpointSlice
 	case Endpoints:
@@ -234,6 +239,8 @@ func FromGVR(g schema.GroupVersionResource) (config.GroupVersionKind, bool) {
 		return DaemonSet, true
 	case gvr.Deployment:
 		return Deployment, true
+	case gvr.DxgateService:
+		return DxgateService, true
 	case gvr.EndpointSlice:
 		return EndpointSlice, true
 	case gvr.Endpoints:
diff --git a/pkg/config/schema/gvr/resources.gen.go b/pkg/config/schema/gvr/resources.gen.go
index 79d7ab4f..b71d600f 100755
--- a/pkg/config/schema/gvr/resources.gen.go
+++ b/pkg/config/schema/gvr/resources.gen.go
@@ -13,6 +13,7 @@ var (
 	CustomResourceDefinition       = schema.GroupVersionResource{Group: "apiextensions.k8s.io", Version: "v1", Resource: "customresourcedefinitions"}
 	DaemonSet                      = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "daemonsets"}
 	Deployment                     = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
+	DxgateService                  = schema.GroupVersionResource{Group: "networking.dubbo.apache.org", Version: "v1alpha3", Resource: "dxgateservices"}
 	EndpointSlice                  = schema.GroupVersionResource{Group: "discovery.k8s.io", Version: "v1", Resource: "endpointslices"}
 	Endpoints                      = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "endpoints"}
 	FaultInjectionPolicy           = schema.GroupVersionResource{Group: "networking.dubbo.apache.org", Version: "v1alpha3", Resource: "faultinjectionpolicies"}
@@ -63,6 +64,8 @@ func IsClusterScoped(g schema.GroupVersionResource) bool {
 		return false
 	case Deployment:
 		return false
+	case DxgateService:
+		return false
 	case EndpointSlice:
 		return false
 	case Endpoints:
diff --git a/pkg/config/schema/kind/resources.gen.go b/pkg/config/schema/kind/resources.gen.go
index 2eeffb3e..01ade2a7 100755
--- a/pkg/config/schema/kind/resources.gen.go
+++ b/pkg/config/schema/kind/resources.gen.go
@@ -13,6 +13,7 @@ const (
 	DNSName
 	DaemonSet
 	Deployment
+	DxgateService
 	EndpointSlice
 	Endpoints
 	FaultInjectionPolicy
@@ -61,6 +62,8 @@ func (k Kind) String() string {
 		return "DaemonSet"
 	case Deployment:
 		return "Deployment"
+	case DxgateService:
+		return "DxgateService"
 	case EndpointSlice:
 		return "EndpointSlice"
 	case Endpoints:
@@ -138,6 +141,8 @@ func FromString(s string) Kind {
 		return DaemonSet
 	case "Deployment":
 		return Deployment
+	case "DxgateService":
+		return DxgateService
 	case "EndpointSlice":
 		return EndpointSlice
 	case "Endpoints":
diff --git a/pkg/config/schema/kubeclient/resources.gen.go b/pkg/config/schema/kubeclient/resources.gen.go
index 217a0ec5..61e5a3ab 100755
--- a/pkg/config/schema/kubeclient/resources.gen.go
+++ b/pkg/config/schema/kubeclient/resources.gen.go
@@ -48,6 +48,8 @@ func GetWriteClient[T runtime.Object](c ClientGetter, namespace string) ktypes.W
 		return c.Kube().AppsV1().DaemonSets(namespace).(ktypes.WriteAPI[T])
 	case *k8sioapiappsv1.Deployment:
 		return c.Kube().AppsV1().Deployments(namespace).(ktypes.WriteAPI[T])
+	case *apigithubcomapachedubbokubernetesapinetworkingv1alpha3.DxgateService:
+		return c.Dubbo().NetworkingV1alpha3().DxgateServices(namespace).(ktypes.WriteAPI[T])
 	case *k8sioapidiscoveryv1.EndpointSlice:
 		return c.Kube().DiscoveryV1().EndpointSlices(namespace).(ktypes.WriteAPI[T])
 	case *k8sioapicorev1.Endpoints:
@@ -119,6 +121,8 @@ func GetClient[T, TL runtime.Object](c ClientGetter, namespace string) ktypes.Re
 		return c.Kube().AppsV1().DaemonSets(namespace).(ktypes.ReadWriteAPI[T, TL])
 	case *k8sioapiappsv1.Deployment:
 		return c.Kube().AppsV1().Deployments(namespace).(ktypes.ReadWriteAPI[T, TL])
+	case *apigithubcomapachedubbokubernetesapinetworkingv1alpha3.DxgateService:
+		return c.Dubbo().NetworkingV1alpha3().DxgateServices(namespace).(ktypes.ReadWriteAPI[T, TL])
 	case *k8sioapidiscoveryv1.EndpointSlice:
 		return c.Kube().DiscoveryV1().EndpointSlices(namespace).(ktypes.ReadWriteAPI[T, TL])
 	case *k8sioapicorev1.Endpoints:
@@ -190,6 +194,8 @@ func gvrToObject(g schema.GroupVersionResource) runtime.Object {
 		return &k8sioapiappsv1.DaemonSet{}
 	case gvr.Deployment:
 		return &k8sioapiappsv1.Deployment{}
+	case gvr.DxgateService:
+		return &apigithubcomapachedubbokubernetesapinetworkingv1alpha3.DxgateService{}
 	case gvr.EndpointSlice:
 		return &k8sioapidiscoveryv1.EndpointSlice{}
 	case gvr.Endpoints:
@@ -299,6 +305,13 @@ func getInformerFiltered(c ClientGetter, opts ktypes.InformerOptions, g schema.G
 		w = func(options metav1.ListOptions) (watch.Interface, error) {
 			return c.Kube().AppsV1().Deployments(opts.Namespace).Watch(context.Background(), options)
 		}
+	case gvr.DxgateService:
+		l = func(options metav1.ListOptions) (runtime.Object, error) {
+			return c.Dubbo().NetworkingV1alpha3().DxgateServices(opts.Namespace).List(context.Background(), options)
+		}
+		w = func(options metav1.ListOptions) (watch.Interface, error) {
+			return c.Dubbo().NetworkingV1alpha3().DxgateServices(opts.Namespace).Watch(context.Background(), options)
+		}
 	case gvr.EndpointSlice:
 		l = func(options metav1.ListOptions) (runtime.Object, error) {
 			return c.Kube().DiscoveryV1().EndpointSlices(opts.Namespace).List(context.Background(), options)
diff --git a/pkg/config/schema/kubetypes/resources.gen.go b/pkg/config/schema/kubetypes/resources.gen.go
index 00efb091..ea73932a 100755
--- a/pkg/config/schema/kubetypes/resources.gen.go
+++ b/pkg/config/schema/kubetypes/resources.gen.go
@@ -44,6 +44,10 @@ func getGvk(obj any) (config.GroupVersionKind, bool) {
 		return gvk.DaemonSet, true
 	case *k8sioapiappsv1.Deployment:
 		return gvk.Deployment, true
+	case *githubcomkdubboapinetworkingv1alpha3.DxgateService:
+		return gvk.DxgateService, true
+	case *apigithubcomapachedubbokubernetesapinetworkingv1alpha3.DxgateService:
+		return gvk.DxgateService, true
 	case *k8sioapidiscoveryv1.EndpointSlice:
 		return gvk.EndpointSlice, true
 	case *k8sioapicorev1.Endpoints:
diff --git a/pkg/config/schema/metadata.yaml b/pkg/config/schema/metadata.yaml
index 7a03f977..c238b616 100644
--- a/pkg/config/schema/metadata.yaml
+++ b/pkg/config/schema/metadata.yaml
@@ -240,6 +240,16 @@ resources:
     statusProto: "dubbo.meta.v1alpha1.DubboStatus"
     statusProtoPackage: "github.com/kdubbo/api/meta/v1alpha1"
 
+  - kind: DxgateService
+    plural: "dxgateservices"
+    group: "networking.dubbo.apache.org"
+    version: "v1alpha3"
+    proto: "dubbo.networking.v1alpha3.DxgateService"
+    protoPackage: "github.com/kdubbo/api/networking/v1alpha3"
+    validate: "validation.ValidateDxgateService"
+    statusProto: "dubbo.meta.v1alpha1.DubboStatus"
+    statusProtoPackage: "github.com/kdubbo/api/meta/v1alpha1"
+
   - kind: ServiceActivationPolicy
     plural: "serviceactivationpolicies"
     group: "networking.dubbo.apache.org"
diff --git a/pkg/config/validation/validators.go b/pkg/config/validation/validators.go
index b5b61739..8d5b050a 100644
--- a/pkg/config/validation/validators.go
+++ b/pkg/config/validation/validators.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"math"
 	"net"
+	"net/url"
 	"strings"
 	"time"
 
@@ -263,6 +264,156 @@ var ValidateFaultInjectionPolicy = RegisterValidateFunc("ValidateFaultInjectionP
 		return v.Unwrap()
 	})
 
+// ValidateDxgateService checks that a mesh-native LLM, MCP, or A2A backend can
+// be compiled into one unambiguous data-plane configuration.
+var ValidateDxgateService = RegisterValidateFunc("ValidateDxgateService",
+	func(cfg config.Config) (Warning, error) {
+		spec, ok := cfg.Spec.(*networking.DxgateService)
+		if !ok {
+			return nil, fmt.Errorf("cannot cast to DxgateService")
+		}
+		v := Validation{}
+		switch {
+		case spec.GetAi() != nil:
+			ai := spec.GetAi()
+			if ai.GetProvider() == nil || ai.GetProvider().GetProvider() == nil {
+				v = appendValidation(v, fmt.Errorf("ai.provider must select openai or anthropic"))
+			}
+			if endpoint := ai.GetEndpoint(); endpoint != "" {
+				parsed, err := url.ParseRequestURI(endpoint)
+				if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
+					v = appendValidation(v, fmt.Errorf("ai.endpoint %q must be an absolute HTTP(S) URL", endpoint))
+				}
+			}
+			for i, model := range ai.GetModels() {
+				if strings.TrimSpace(model) == "" {
+					v = appendValidation(v, fmt.Errorf("ai.models[%d] must not be empty", i))
+				}
+			}
+			for path := range ai.GetRoutes() {
+				if !strings.HasPrefix(path, "/") {
+					v = appendValidation(v, fmt.Errorf("ai.routes key %q must start with /", path))
+				}
+			}
+			if credential := ai.GetProvider().GetCredential(); credential != nil {
+				v = appendValidation(v, validateSecretKeyReference("ai.provider.credential", credential))
+			}
+		case spec.GetMcp() != nil:
+			targets := spec.GetMcp().GetTargets()
+			if len(targets) == 0 {
+				v = appendValidation(v, fmt.Errorf("mcp.targets must not be empty"))
+			}
+			names := make(map[string]struct{}, len(targets))
+			for i, target := range targets {
+				if target == nil {
+					v = appendValidation(v, fmt.Errorf("mcp.targets[%d] must not be null", i))
+					continue
+				}
+				if target.GetName() == "" {
+					v = appendValidation(v, fmt.Errorf("mcp.targets[%d].name must not be empty", i))
+				} else if _, found := names[target.GetName()]; found {
+					v = appendValidation(v, fmt.Errorf("mcp.targets[%d].name %q is duplicated", i, target.GetName()))
+				} else {
+					names[target.GetName()] = struct{}{}
+				}
+				static := target.GetStatic()
+				if static == nil {
+					v = appendValidation(v, fmt.Errorf("mcp.targets[%d].static must be set", i))
+					continue
+				}
+				v = appendValidation(v,
+					validateBackendReference(fmt.Sprintf("mcp.targets[%d].static.backendRef", i), static.GetBackendRef()),
+					validateDxgatePort(fmt.Sprintf("mcp.targets[%d].static.port", i), static.GetPort()),
+					validateOptionalPath(fmt.Sprintf("mcp.targets[%d].static.path", i), static.GetPath()),
+				)
+			}
+		case spec.GetA2A() != nil:
+			a2a := spec.GetA2A()
+			hasRef := a2a.GetBackendRef() != nil
+			hasHost := strings.TrimSpace(a2a.GetHost()) != ""
+			if hasRef == hasHost {
+				v = appendValidation(v, fmt.Errorf("a2a must set exactly one of backendRef or host"))
+			}
+			if hasRef {
+				v = appendValidation(v, validateBackendReference("a2a.backendRef", a2a.GetBackendRef()))
+			}
+			v = appendValidation(v,
+				validateDxgatePort("a2a.port", a2a.GetPort()),
+				validateOptionalPath("a2a.path", a2a.GetPath()),
+			)
+		default:
+			v = appendValidation(v, fmt.Errorf("exactly one of ai, mcp, or a2a must be set"))
+		}
+
+		if policies := spec.GetPolicies(); policies != nil {
+			if auth := policies.GetAuth(); auth != nil {
+				v = appendValidation(v, validateSecretKeyReference("policies.auth.secretRef", auth.GetSecretRef()))
+			}
+			if rate := policies.GetRateLimit(); rate != nil {
+				if rate.GetRequests() == 0 {
+					v = appendValidation(v, fmt.Errorf("policies.rateLimit.requests must be greater than zero"))
+				}
+				v = appendValidation(v, validatePositiveDuration("policies.rateLimit.window", rate.GetWindow()))
+			}
+			if tokens := policies.GetTokenLimit(); tokens != nil {
+				if tokens.GetTokens() == 0 {
+					v = appendValidation(v, fmt.Errorf("policies.tokenLimit.tokens must be greater than zero"))
+				}
+				v = appendValidation(v, validatePositiveDuration("policies.tokenLimit.window", tokens.GetWindow()))
+			}
+			v = appendValidation(v, validatePositiveDuration("policies.timeout", policies.GetTimeout()))
+			if retry := policies.GetRetry(); retry != nil {
+				if retry.GetAttempts() == 0 {
+					v = appendValidation(v, fmt.Errorf("policies.retry.attempts must be greater than zero"))
+				}
+				for i, status := range retry.GetStatusCodes() {
+					if status < 400 || status > 599 {
+						v = appendValidation(v, fmt.Errorf("policies.retry.statusCodes[%d] must be in range [400, 599], got %d", i, status))
+					}
+				}
+			}
+			if policies.GetMaxBodyBytes() < 0 {
+				v = appendValidation(v, fmt.Errorf("policies.maxBodyBytes must not be negative"))
+			}
+		}
+		return v.Unwrap()
+	})
+
+func validateSecretKeyReference(field string, ref *networking.SecretKeyReference) error {
+	if ref == nil {
+		return fmt.Errorf("%s must be set", field)
+	}
+	var errs error
+	if strings.TrimSpace(ref.GetName()) == "" {
+		errs = AppendErrors(errs, fmt.Errorf("%s.name must not be empty", field))
+	}
+	if strings.TrimSpace(ref.GetKey()) == "" {
+		errs = AppendErrors(errs, fmt.Errorf("%s.key must not be empty", field))
+	}
+	return errs
+}
+
+func validateBackendReference(field string, ref *networking.BackendReference) error {
+	if ref == nil || strings.TrimSpace(ref.GetName()) == "" {
+		return fmt.Errorf("%s.name must not be empty", field)
+	}
+	return nil
+}
+
+func validateDxgatePort(field string, port uint32) error {
+	if port == 0 || port > 65535 {
+		return fmt.Errorf("%s must be in range [1, 65535], got %d", field, port)
+	}
+	return nil
+}
+
+func validateOptionalPath(field, path string) error {
+	if path != "" && !strings.HasPrefix(path, "/") {
+		return fmt.Errorf("%s must start with /", field)
+	}
+	return nil
+}
+
 // ValidateServiceEntry checks that a ServiceEntry can be converted into services and endpoints.
 var ValidateServiceEntry = RegisterValidateFunc("ValidateServiceEntry", func(cfg config.Config) (Warning, error) {
 	spec, ok := cfg.Spec.(*networking.ServiceEntry)
diff --git a/pkg/config/validation/validators_test.go b/pkg/config/validation/validators_test.go
index 08dce1af..34cdbd71 100644
--- a/pkg/config/validation/validators_test.go
+++ b/pkg/config/validation/validators_test.go
@@ -382,6 +382,112 @@ func TestValidateFaultInjectionPolicy(t *testing.T) {
 	}
 }
 
+func TestValidateDxgateService(t *testing.T) {
+	openAI := func() *networking.DxgateService {
+		return &networking.DxgateService{
+			Service: &networking.DxgateService_Ai{Ai: &networking.AIService{
+				Provider: &networking.AIProvider{
+					Provider: &networking.AIProvider_Openai{Openai: &networking.OpenAIProvider{}},
+				},
+				Models:   []string{"gpt-test"},
+				Endpoint: "http://llm.default.svc:8080/v1",
+			}},
+		}
+	}
+	mcp := func() *networking.DxgateService {
+		return &networking.DxgateService{
+			Service: &networking.DxgateService_Mcp{Mcp: &networking.MCPService{
+				Targets: []*networking.MCPTarget{{
+					Name: "tools",
+					Static: &networking.StaticBackend{
+						BackendRef: &networking.BackendReference{Name: "tools"},
+						Port:       8080,
+					},
+				}},
+			}},
+		}
+	}
+	a2a := func() *networking.DxgateService {
+		return &networking.DxgateService{
+			Service: &networking.DxgateService_A2A{A2A: &networking.A2AService{
+				BackendRef: &networking.BackendReference{Name: "review-agent"},
+				Port:       9090,
+			}},
+		}
+	}
+	cases := []struct {
+		name    string
+		spec    *networking.DxgateService
+		wantErr bool
+	}{
+		{name: "openai", spec: openAI()},
+		{name: "mcp", spec: mcp()},
+		{name: "a2a", spec: a2a()},
+		{name: "missing service", spec: &networking.DxgateService{}, wantErr: true},
+		{
+			name: "ai provider missing",
+			spec: &networking.DxgateService{
+				Service: &networking.DxgateService_Ai{Ai: &networking.AIService{}},
+			},
+			wantErr: true,
+		},
+		{
+			name: "bad ai endpoint",
+			spec: func() *networking.DxgateService {
+				s := openAI()
+				s.GetAi().Endpoint = "llm:8080"
+				return s
+			}(),
+			wantErr: true,
+		},
+		{
+			name: "empty mcp targets",
+			spec: &networking.DxgateService{
+				Service: &networking.DxgateService_Mcp{Mcp: &networking.MCPService{}},
+			},
+			wantErr: true,
+		},
+		{
+			name: "duplicate mcp target",
+			spec: func() *networking.DxgateService {
+				s := mcp()
+				s.GetMcp().Targets = append(s.GetMcp().Targets, s.GetMcp().Targets[0])
+				return s
+			}(),
+			wantErr: true,
+		},
+		{
+			name: "a2a ambiguous target",
+			spec: func() *networking.DxgateService {
+				s := a2a()
+				s.GetA2A().Host = "agent.example.com"
+				return s
+			}(),
+			wantErr: true,
+		},
+		{
+			name: "bad policy",
+			spec: func() *networking.DxgateService {
+				s := openAI()
+				s.Policies = &networking.DxgateServicePolicies{
+					Retry:   &networking.RetryPolicy{Attempts: 0, StatusCodes: []uint32{200}},
+					Timeout: durationpb.New(-time.Second),
+				}
+				return s
+			}(),
+			wantErr: true,
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := ValidateDxgateService(makeConfig(tc.spec))
+			if (err != nil) != tc.wantErr {
+				t.Fatalf("got err=%v, wantErr=%v", err, tc.wantErr)
+			}
+		})
+	}
+}
+
 func TestValidateServiceEntry(t *testing.T) {
 	valid := func() *networking.ServiceEntry {
 		return &networking.ServiceEntry{
diff --git a/samples/ai-mesh/README.md b/samples/ai-mesh/README.md
new file mode 100644
index 00000000..5fca59e4
--- /dev/null
+++ b/samples/ai-mesh/README.md
@@ -0,0 +1,18 @@
+# Mesh-native AI services
+
+This sample uses one API: `networking.dubbo.apache.org/v1alpha3`
+`DxgateService`. Ordinary HTTP backends remain core Kubernetes `Service`
+objects. `dubbod` compiles both kinds of `HTTPRoute` backend into RDS and
+delivers it to dxgate over xDS.
+
+```bash
+kubectl create namespace ai-mesh
+kubectl -n ai-mesh apply -f gateway.yaml
+kubectl -n ai-mesh apply -f backends.yaml
+kubectl -n ai-mesh apply -f services.yaml
+kubectl -n ai-mesh apply -f routes.yaml
+```
+
+The sample image `kdubbo/agent-mock:latest` is the no-key E2E fixture in
+`tests/e2e/agentmock`; replace its Services and Secret values with production
+backends and credentials.
diff --git a/samples/ai-mesh/backends.yaml b/samples/ai-mesh/backends.yaml
new file mode 100644
index 00000000..ec4e6fb9
--- /dev/null
+++ b/samples/ai-mesh/backends.yaml
@@ -0,0 +1,119 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: agent-mock
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: agent-mock
+  template:
+    metadata:
+      labels:
+        app: agent-mock
+    spec:
+      containers:
+        - name: http
+          image: kdubbo/agent-mock:latest
+          imagePullPolicy: IfNotPresent
+          env:
+            - {name: MOCK_MODE, value: http}
+            - {name: PORT, value: "8080"}
+          ports:
+            - {name: http, containerPort: 8080}
+        - name: openai
+          image: kdubbo/agent-mock:latest
+          imagePullPolicy: IfNotPresent
+          env:
+            - {name: MOCK_MODE, value: openai}
+            - {name: PORT, value: "8081"}
+          ports:
+            - {name: openai, containerPort: 8081}
+        - name: anthropic
+          image: kdubbo/agent-mock:latest
+          imagePullPolicy: IfNotPresent
+          env:
+            - {name: MOCK_MODE, value: anthropic}
+            - {name: PORT, value: "8082"}
+          ports:
+            - {name: anthropic, containerPort: 8082}
+        - name: mcp-search
+          image: kdubbo/agent-mock:latest
+          imagePullPolicy: IfNotPresent
+          env:
+            - {name: MOCK_MODE, value: mcp}
+            - {name: MOCK_NAME, value: search}
+            - {name: PORT, value: "8083"}
+          ports:
+            - {name: mcp-search, containerPort: 8083}
+        - name: mcp-calendar
+          image: kdubbo/agent-mock:latest
+          imagePullPolicy: IfNotPresent
+          env:
+            - {name: MOCK_MODE, value: mcp}
+            - {name: MOCK_NAME, value: calendar}
+            - {name: PORT, value: "8084"}
+          ports:
+            - {name: mcp-calendar, containerPort: 8084}
+        - name: a2a
+          image: kdubbo/agent-mock:latest
+          imagePullPolicy: IfNotPresent
+          env:
+            - {name: MOCK_MODE, value: a2a}
+            - {name: PORT, value: "8085"}
+          ports:
+            - {name: a2a, containerPort: 8085}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: mock-http
+spec:
+  selector: {app: agent-mock}
+  ports:
+    - {name: http, port: 8080, targetPort: http}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: mock-openai
+spec:
+  selector: {app: agent-mock}
+  ports:
+    - {name: http, port: 8081, targetPort: openai}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: mock-anthropic
+spec:
+  selector: {app: agent-mock}
+  ports:
+    - {name: http, port: 8082, targetPort: anthropic}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: mock-mcp-search
+spec:
+  selector: {app: agent-mock}
+  ports:
+    - {name: http, port: 8083, targetPort: mcp-search}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: mock-mcp-calendar
+spec:
+  selector: {app: agent-mock}
+  ports:
+    - {name: http, port: 8084, targetPort: mcp-calendar}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: mock-a2a
+spec:
+  selector: {app: agent-mock}
+  ports:
+    - {name: http, port: 8085, targetPort: a2a}
diff --git a/samples/ai-mesh/gateway.yaml b/samples/ai-mesh/gateway.yaml
new file mode 100644
index 00000000..86ea738f
--- /dev/null
+++ b/samples/ai-mesh/gateway.yaml
@@ -0,0 +1,13 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: public
+spec:
+  gatewayClassName: dubbo
+  listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: Same
diff --git a/samples/ai-mesh/routes.yaml b/samples/ai-mesh/routes.yaml
new file mode 100644
index 00000000..ad2dd293
--- /dev/null
+++ b/samples/ai-mesh/routes.yaml
@@ -0,0 +1,86 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: ordinary-http
+spec:
+  parentRefs:
+    - {name: public, sectionName: http}
+  rules:
+    - matches:
+        - path: {type: PathPrefix, value: /users}
+        - path: {type: PathPrefix, value: /orders}
+      backendRefs:
+        - {name: mock-http, port: 8080}
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: openai
+spec:
+  parentRefs:
+    - {name: public, sectionName: http}
+  rules:
+    - matches:
+        - path: {type: PathPrefix, value: /openai}
+      filters:
+        - type: URLRewrite
+          urlRewrite:
+            path:
+              type: ReplacePrefixMatch
+              replacePrefixMatch: /v1
+      backendRefs:
+        - group: networking.dubbo.apache.org
+          kind: DxgateService
+          name: openai
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: anthropic
+spec:
+  parentRefs:
+    - {name: public, sectionName: http}
+  rules:
+    - matches:
+        - path: {type: PathPrefix, value: /anthropic}
+      filters:
+        - type: URLRewrite
+          urlRewrite:
+            path:
+              type: ReplacePrefixMatch
+              replacePrefixMatch: /v1
+      backendRefs:
+        - group: networking.dubbo.apache.org
+          kind: DxgateService
+          name: anthropic
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: tools
+spec:
+  parentRefs:
+    - {name: public, sectionName: http}
+  rules:
+    - matches:
+        - path: {type: Exact, value: /mcp}
+      backendRefs:
+        - group: networking.dubbo.apache.org
+          kind: DxgateService
+          name: tools
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: planner
+spec:
+  parentRefs:
+    - {name: public, sectionName: http}
+  rules:
+    - matches:
+        - path: {type: Exact, value: /a2a}
+        - path: {type: Exact, value: /.well-known/agent-card.json}
+      backendRefs:
+        - group: networking.dubbo.apache.org
+          kind: DxgateService
+          name: planner
diff --git a/samples/ai-mesh/services.yaml b/samples/ai-mesh/services.yaml
new file mode 100644
index 00000000..48aabb9b
--- /dev/null
+++ b/samples/ai-mesh/services.yaml
@@ -0,0 +1,80 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: agent-credentials
+type: Opaque
+stringData:
+  openai: mock-openai-key
+  anthropic: mock-anthropic-key
+  client: mock-client-key
+---
+apiVersion: networking.dubbo.apache.org/v1alpha3
+kind: DxgateService
+metadata:
+  name: openai
+spec:
+  ai:
+    endpoint: http://mock-openai:8081/v1
+    provider:
+      openai: {}
+      credential:
+        name: agent-credentials
+        key: openai
+    models: [gpt-mock]
+    routes:
+      /v1/chat/completions: COMPLETIONS
+  policies:
+    auth:
+      header: x-client-key
+      secretRef:
+        name: agent-credentials
+        key: client
+    timeout: 5s
+    retry:
+      attempts: 2
+      statusCodes: [502, 503, 504]
+---
+apiVersion: networking.dubbo.apache.org/v1alpha3
+kind: DxgateService
+metadata:
+  name: anthropic
+spec:
+  ai:
+    endpoint: http://mock-anthropic:8082
+    provider:
+      anthropic:
+        model: claude-mock
+      credential:
+        name: agent-credentials
+        key: anthropic
+    models: [claude-mock]
+    routes:
+      /v1/chat/completions: COMPLETIONS
+---
+apiVersion: networking.dubbo.apache.org/v1alpha3
+kind: DxgateService
+metadata:
+  name: tools
+spec:
+  mcp:
+    targets:
+      - name: search
+        static:
+          backendRef: {name: mock-mcp-search}
+          port: 8083
+        tools: [search]
+      - name: calendar
+        static:
+          backendRef: {name: mock-mcp-calendar}
+          port: 8084
+        tools: [calendar]
+---
+apiVersion: networking.dubbo.apache.org/v1alpha3
+kind: DxgateService
+metadata:
+  name: planner
+spec:
+  a2a:
+    backendRef: {name: mock-a2a}
+    port: 8085
+    agent: planner
diff --git a/tests/e2e/agentmock/Dockerfile b/tests/e2e/agentmock/Dockerfile
new file mode 100644
index 00000000..d9c11d3b
--- /dev/null
+++ b/tests/e2e/agentmock/Dockerfile
@@ -0,0 +1,9 @@
+FROM golang:1.25-alpine AS builder
+WORKDIR /src
+COPY main.go .
+RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /agentmock main.go
+
+FROM scratch
+COPY --from=builder /agentmock /agentmock
+USER 65532:65532
+ENTRYPOINT ["/agentmock"]
diff --git a/tests/e2e/agentmock/main.go b/tests/e2e/agentmock/main.go
new file mode 100644
index 00000000..c7a91265
--- /dev/null
+++ b/tests/e2e/agentmock/main.go
@@ -0,0 +1,94 @@
+// 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.
+
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"time"
+)
+
+type request struct {
+	ID     any            `json:"id"`
+	Method string         `json:"method"`
+	Model  string         `json:"model"`
+	Params map[string]any `json:"params"`
+}
+
+func main() {
+	mode := env("MOCK_MODE", "http")
+	name := env("MOCK_NAME", mode)
+	port := env("PORT", "8080")
+	mux := http.NewServeMux()
+	mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
+		w.WriteHeader(http.StatusOK)
+	})
+	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		switch mode {
+		case "http":
+			writeJSON(w, map[string]any{"service": "http", "path": r.URL.Path})
+		case "openai":
+			var in request
+			_ = json.NewDecoder(r.Body).Decode(&in)
+			writeJSON(w, map[string]any{
+				"id": "chatcmpl-mock", "object": "chat.completion", "model": in.Model,
+				"provider_authorization": r.Header.Get("Authorization"),
+				"choices":                []any{map[string]any{"index": 0, "message": map[string]any{"role": "assistant", "content": "openai-mock"}, "finish_reason": "stop"}},
+				"usage":                  map[string]any{"prompt_tokens": 2, "completion_tokens": 3, "total_tokens": 5},
+			})
+		case "anthropic":
+			var in request
+			_ = json.NewDecoder(r.Body).Decode(&in)
+			writeJSON(w, map[string]any{
+				"id": "msg-mock", "type": "message", "role": "assistant", "model": in.Model,
+				"content":     []any{map[string]any{"type": "text", "text": "anthropic-mock"}},
+				"stop_reason": "end_turn",
+				"usage":       map[string]any{"input_tokens": 4, "output_tokens": 6},
+			})
+		case "mcp":
+			var in request
+			_ = json.NewDecoder(r.Body).Decode(&in)
+			result := map[string]any{}
+			switch in.Method {
+			case "initialize":
+				result = map[string]any{"protocolVersion": "2025-03-26", "serverInfo": map[string]any{"name": name, "version": "1.0.0"}, "capabilities": map[string]any{"tools": map[string]any{}}}
+			case "tools/list":
+				result = map[string]any{"tools": []any{map[string]any{"name": name, "description": name + " mock tool", "inputSchema": map[string]any{"type": "object"}}}}
+			case "tools/call":
+				result = map[string]any{"content": []any{map[string]any{"type": "text", "text": name + "-ok"}}}
+			}
+			writeJSON(w, map[string]any{"jsonrpc": "2.0", "id": in.ID, "result": result})
+		case "a2a":
+			if r.Method == http.MethodGet {
+				writeJSON(w, map[string]any{"name": "planner", "version": "1.0.0", "url": "http://mock-a2a:8080/a2a"})
+				return
+			}
+			var in request
+			_ = json.NewDecoder(r.Body).Decode(&in)
+			writeJSON(w, map[string]any{"jsonrpc": "2.0", "id": in.ID, "result": map[string]any{"id": "task-mock", "status": map[string]any{"state": "completed"}}})
+		default:
+			http.Error(w, fmt.Sprintf("unknown MOCK_MODE %q", mode), http.StatusInternalServerError)
+		}
+	})
+	server := &http.Server{Addr: ":" + port, Handler: mux, ReadHeaderTimeout: 5 * time.Second}
+	log.Printf("agent mock mode=%s name=%s addr=%s", mode, name, server.Addr)
+	log.Fatal(server.ListenAndServe())
+}
+
+func env(name, fallback string) string {
+	if value := os.Getenv(name); value != "" {
+		return value
+	}
+	return fallback
+}
+
+func writeJSON(w http.ResponseWriter, value any) {
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(value)
+}
diff --git a/tests/e2e/run.sh b/tests/e2e/run.sh
index 042a7de9..efb0d795 100755
--- a/tests/e2e/run.sh
+++ b/tests/e2e/run.sh
@@ -32,6 +32,7 @@
 #   KIND            path to the kind binary      (default: kind)
 #   KIND_NODE_IMAGE kind node image override     (default: kind release default)
 #   ACTIVATION_E2E  install KEDA and run real scale-to-zero E2E (default: 0)
+#   AI_MESH_E2E     run no-key HTTP/LLM/MCP/A2A E2E (default: 0)
 #   DXGATE_IMAGE    prebuilt dxgate image used by managed Gateways
 #   KEDA_VERSION    pinned KEDA chart/app version (default: 2.20.2)
 
@@ -52,7 +53,9 @@ PREVIOUS_CHART=""
 KIND="${KIND:-kind}"
 KIND_NODE_IMAGE="${KIND_NODE_IMAGE:-}"
 ACTIVATION_E2E="${ACTIVATION_E2E:-0}"
+AI_MESH_E2E="${AI_MESH_E2E:-0}"
 DXGATE_IMAGE="${DXGATE_IMAGE:-kdubbo/dxgate:latest}"
+AGENT_MOCK_IMAGE="${AGENT_MOCK_IMAGE:-kdubbo/agent-mock:latest}"
 ACTIVATION_APP_IMAGE="${ACTIVATION_APP_IMAGE:-kdubbo/activation-e2e:latest}"
 ACTIVATION_CLIENT_IMAGE="${ACTIVATION_CLIENT_IMAGE:-kdubbo/activation-client:latest}"
 KEDA_VERSION="${KEDA_VERSION:-2.20.2}"
@@ -81,6 +84,7 @@ fail() {
 
 cleanup() {
   if [[ -n "${PF_PID:-}" ]]; then kill "${PF_PID}" 2>/dev/null || true; fi
+  if [[ -n "${AI_PF_PID:-}" ]]; then kill "${AI_PF_PID}" 2>/dev/null || true; fi
   if [[ "${KEEP_CLUSTER:-0}" != "1" ]]; then
     "${KIND}" delete cluster --name "${CLUSTER_NAME}" || true
   fi
@@ -90,6 +94,14 @@ cleanup() {
 }
 trap cleanup EXIT
 
+apply_activation_fixture() {
+  sed \
+    -e "s#kdubbo/activation-e2e:latest#${ACTIVATION_APP_IMAGE}#g" \
+    -e "s#kdubbo/activation-client:latest#${ACTIVATION_CLIENT_IMAGE}#g" \
+    "$1" \
+    | "${KUBECTL[@]}" apply -f -
+}
+
 prepare_previous_chart() {
   if [[ -n "${UPGRADE_FROM_CHART}" ]]; then
     [[ -e "${UPGRADE_FROM_CHART}" ]] || fail "previous chart not found: ${UPGRADE_FROM_CHART}"
@@ -152,11 +164,18 @@ log "loading ${IMAGE} into kind"
 if [[ "${IMAGE}" != "${UPGRADE_FROM_IMAGE}" ]]; then
   "${KIND}" load docker-image "${UPGRADE_FROM_IMAGE}" --name "${CLUSTER_NAME}"
 fi
-if [[ "${ACTIVATION_E2E}" == "1" ]]; then
+if [[ "${ACTIVATION_E2E}" == "1" || "${AI_MESH_E2E}" == "1" ]]; then
   docker image inspect "${DXGATE_IMAGE}" >/dev/null 2>&1 \
-    || fail "ACTIVATION_E2E requires prebuilt ${DXGATE_IMAGE}"
-  log "building ${ACTIVATION_APP_IMAGE}"
-  docker build -t "${ACTIVATION_APP_IMAGE}" "${ROOT}/tests/e2e/activationapp"
+    || fail "data-plane E2E requires prebuilt ${DXGATE_IMAGE}"
+fi
+if [[ "${ACTIVATION_E2E}" == "1" ]]; then
+  if [[ "${SKIP_BUILD:-0}" != "1" ]]; then
+    log "building ${ACTIVATION_APP_IMAGE}"
+    docker build -t "${ACTIVATION_APP_IMAGE}" "${ROOT}/tests/e2e/activationapp"
+  else
+    docker image inspect "${ACTIVATION_APP_IMAGE}" >/dev/null 2>&1 \
+      || fail "SKIP_BUILD=1 requires prebuilt ${ACTIVATION_APP_IMAGE}"
+  fi
   docker tag "${IMAGE}" "${ACTIVATION_CLIENT_IMAGE}"
   log "loading activation data-plane images into kind"
   "${KIND}" load docker-image \
@@ -165,6 +184,20 @@ if [[ "${ACTIVATION_E2E}" == "1" ]]; then
     "${ACTIVATION_CLIENT_IMAGE}" \
     --name "${CLUSTER_NAME}"
 fi
+if [[ "${AI_MESH_E2E}" == "1" ]]; then
+  if [[ "${SKIP_BUILD:-0}" != "1" ]]; then
+    log "building ${AGENT_MOCK_IMAGE}"
+    docker build -t "${AGENT_MOCK_IMAGE}" "${ROOT}/tests/e2e/agentmock"
+  else
+    docker image inspect "${AGENT_MOCK_IMAGE}" >/dev/null 2>&1 \
+      || fail "SKIP_BUILD=1 requires prebuilt ${AGENT_MOCK_IMAGE}"
+  fi
+  log "loading AI mesh data-plane images into kind"
+  "${KIND}" load docker-image \
+    "${DXGATE_IMAGE}" \
+    "${AGENT_MOCK_IMAGE}" \
+    --name "${CLUSTER_NAME}"
+fi
 
 log "installing Gateway API CRDs"
 # Pin to the sigs.k8s.io/gateway-api version in go.mod. HTTPRoute retry is an
@@ -354,6 +387,93 @@ GATEWAY_ENV="$("${KUBECTL[@]}" -n "${APP_NS}" get deploy public-dubbo \
   | grep -qx metadata.name \
   || fail "gateway does not inject POD_NAME; demand reports would not be attributable to a replica"
 
+if [[ "${AI_MESH_E2E}" == "1" ]]; then
+  log "applying mesh-native HTTP, OpenAI, Anthropic, MCP, and A2A sample"
+  sed "s#kdubbo/agent-mock:latest#${AGENT_MOCK_IMAGE}#g" \
+    "${ROOT}/samples/ai-mesh/backends.yaml" \
+    | "${KUBECTL[@]}" -n "${APP_NS}" apply -f -
+  "${KUBECTL[@]}" -n "${APP_NS}" apply -f "${ROOT}/samples/ai-mesh/services.yaml"
+  "${KUBECTL[@]}" -n "${APP_NS}" apply -f "${ROOT}/samples/ai-mesh/routes.yaml"
+  "${KUBECTL[@]}" -n "${APP_NS}" rollout status deploy/agent-mock --timeout=300s \
+    || fail "agent mock deployment did not become ready"
+  "${KUBECTL[@]}" -n "${APP_NS}" rollout status deploy/public-dubbo --timeout=300s \
+    || fail "mesh gateway deployment did not become ready"
+
+  "${KUBECTL[@]}" get crd dxgateservices.networking.dubbo.apache.org >/dev/null \
+    || fail "DxgateService CRD is missing"
+  "${KUBECTL[@]}" -n "${APP_NS}" get role public-dubbo-credentials >/dev/null \
+    || fail "dxgate credential Role is missing"
+  "${KUBECTL[@]}" -n "${APP_NS}" get rolebinding public-dubbo-credentials >/dev/null \
+    || fail "dxgate credential RoleBinding is missing"
+  "${KUBECTL[@]}" auth can-i get secret/agent-credentials \
+    --as="system:serviceaccount:${APP_NS}:public-dubbo" -n "${APP_NS}" \
+    | grep -qx yes || fail "dxgate ServiceAccount cannot resolve referenced Secret"
+
+  log "port-forwarding mesh gateway"
+  "${KUBECTL[@]}" -n "${APP_NS}" port-forward svc/public-dubbo 18081:80 >/dev/null 2>&1 &
+  AI_PF_PID=$!
+  ai_get() { curl -sf --max-time 10 "http://127.0.0.1:18081$1"; }
+  ai_post() {
+    local path="$1" body="$2"
+    curl -sf --max-time 10 -H 'content-type: application/json' \
+      -d "${body}" "http://127.0.0.1:18081${path}"
+  }
+  ai_openai() {
+    curl -sf --max-time 10 -H 'content-type: application/json' \
+      -H 'x-client-key: mock-client-key' \
+      -d '{"model":"gpt-mock","messages":[{"role":"user","content":"ping"}]}' \
+      http://127.0.0.1:18081/openai/chat/completions
+  }
+
+  retry "ordinary /users Service route" ai_get /users
+  [[ "$(ai_get /users | jq -r .path)" == "/users" ]] \
+    || fail "ordinary /users route returned the wrong backend response"
+  [[ "$(ai_get /orders | jq -r .path)" == "/orders" ]] \
+    || fail "ordinary /orders route returned the wrong backend response"
+  retry "OpenAI DxgateService route and Secret resolution" ai_openai
+  OPENAI_RESPONSE="$(ai_openai)"
+  [[ "$(jq -r .choices[0].message.content <<<"${OPENAI_RESPONSE}")" == "openai-mock" ]] \
+    || fail "OpenAI mock response was not proxied"
+  [[ "$(jq -r .provider_authorization <<<"${OPENAI_RESPONSE}")" == "Bearer mock-openai-key" ]] \
+    || fail "OpenAI provider credential was not resolved from the Secret"
+
+  ANTHROPIC_RESPONSE="$(ai_post /anthropic/chat/completions \
+    '{"model":"claude-mock","messages":[{"role":"user","content":"ping"}]}')"
+  [[ "$(jq -r .choices[0].message.content <<<"${ANTHROPIC_RESPONSE}")" == "anthropic-mock" ]] \
+    || fail "Anthropic native response was not translated to OpenAI"
+
+  MCP_RESPONSE="$(ai_post /mcp '{"jsonrpc":"2.0","id":1,"method":"tools/list"}')"
+  [[ "$(jq -r '[.result.tools[].name] | sort | join(",")' <<<"${MCP_RESPONSE}")" == "calendar,search" ]] \
+    || fail "MCP tools/list was not federated across both targets"
+  [[ "$(ai_post /mcp '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search","arguments":{}}}' \
+      | jq -r .result.content[0].text)" == "search-ok" ]] \
+    || fail "MCP tools/call did not reach the selected target"
+
+  [[ "$(ai_get /.well-known/agent-card.json | jq -r .name)" == "planner" ]] \
+    || fail "A2A Agent Card was not proxied"
+  [[ "$(ai_post /a2a '{"jsonrpc":"2.0","id":3,"method":"message/send","params":{}}' \
+      | jq -r .result.status.state)" == "completed" ]] \
+    || fail "A2A task request did not complete"
+
+  check_agent_config_programmed() {
+    local pod
+    pod="$("${KUBECTL[@]}" -n "${APP_NS}" get pods \
+      -l gateway.networking.k8s.io/gateway-name=public \
+      -o jsonpath='{.items[0].metadata.name}')"
+    "${KUBECTL[@]}" get --raw \
+      "/api/v1/namespaces/${APP_NS}/pods/${pod}:26021/proxy/debug/config" \
+      | jq -e '
+          ([.providers[].name] | length) == 2 and
+          ([.backends[] | select(.type == "llm")] | length) == 2 and
+          ([.backends[] | select(.type == "mcp")] | length) == 2 and
+          ([.backends[] | select(.type == "a2a")] | length) == 1 and
+          ([.routes[] | .protocol] | sort | join(",")) == "a2a,llm,llm,mcp"
+        ' >/dev/null
+  }
+  retry "compiled AgentConfig visible in dxgate /debug/config" check_agent_config_programmed
+  log "mesh-native HTTP, LLM, MCP, and A2A E2E passed"
+fi
+
 if [[ "${ACTIVATION_E2E}" == "1" ]]; then
   log "asserting multiple Gateways have isolated resources"
   "${KUBECTL[@]}" -n "${APP_NS}" get deploy dxgate-gateway public-dubbo >/dev/null \
@@ -364,7 +484,7 @@ if [[ "${ACTIVATION_E2E}" == "1" ]]; then
     || fail "second managed gateway did not become ready"
 
   log "deploying the proxyless activation target and KEDA ScaledObject"
-  "${KUBECTL[@]}" apply -f "${ROOT}/tests/e2e/testdata/eastwest-activation.yaml"
+  apply_activation_fixture "${ROOT}/tests/e2e/testdata/eastwest-activation.yaml"
   "${KUBECTL[@]}" apply -f "${ROOT}/tests/e2e/testdata/eastwest-activation-scaledobject.yaml"
   "${KUBECTL[@]}" -n "${APP_NS}" wait --for=condition=Ready scaledobject/payment --timeout=180s \
     || fail "KEDA ScaledObject did not become ready"
@@ -426,7 +546,7 @@ if [[ "${ACTIVATION_E2E}" == "1" ]]; then
 
   log "sending one proxyless request while payment is at zero"
   "${KUBECTL[@]}" -n "${APP_NS}" delete pod payment-client --ignore-not-found
-  "${KUBECTL[@]}" apply -f "${ROOT}/tests/e2e/testdata/eastwest-activation-client.yaml"
+  apply_activation_fixture "${ROOT}/tests/e2e/testdata/eastwest-activation-client.yaml"
   activation_metrics() {
     local pod
     while read -r pod; do
@@ -475,7 +595,7 @@ if [[ "${ACTIVATION_E2E}" == "1" ]]; then
     local before after
     before="$(activation_payment_requests_total)"
     "${KUBECTL[@]}" -n "${APP_NS}" delete pod payment-client --ignore-not-found --wait=true >/dev/null
-    "${KUBECTL[@]}" apply -f "${ROOT}/tests/e2e/testdata/eastwest-activation-client.yaml" >/dev/null
+    apply_activation_fixture "${ROOT}/tests/e2e/testdata/eastwest-activation-client.yaml" >/dev/null
     "${KUBECTL[@]}" -n "${APP_NS}" wait \
       --for=jsonpath='{.status.containerStatuses[?(@.name=="client")].state.terminated.exitCode}'=0 \
       pod/payment-client --timeout=30s >/dev/null || return 1