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

wu-sheng pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/skywalking-cli.git


The following commit(s) were added to refs/heads/master by this push:
     new 612a2df  Support the OAP admin-server REST API (`swctl admin ...`) and 
adapt to OAP 11.0.0 (#228)
612a2df is described below

commit 612a2dfe27843f33e2a8bb274eae69affaf674a6
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Wed Jun 3 15:01:50 2026 +0800

    Support the OAP admin-server REST API (`swctl admin ...`) and adapt to OAP 
11.0.0 (#228)
    
    * Support the OAP admin-server REST API and adapt to OAP 11.0.0
    
    Add a first-class admin-host (REST) client and a `swctl admin ...` command
    tree, alongside the existing GraphQL surface, and adapt to the OAP 11.0.0
    breaking changes.
    
    Admin REST surface (default port 17128):
    - New global `--admin-url` flag (env SW_ADMIN_URL / config key `admin-url`),
      derived from `--base-url`'s host with port 17128 when unset.
    - New pkg/transport (shared TLS/basic-auth) and pkg/admin/client REST client
      with a typed error envelope and admin-module preflight detection.
    - `swctl admin ...` covering every feature module: preflight; cluster nodes,
      config dump/ttl, alarm rules/rule (status); inspect metrics/entities;
      ui-template list/get/create/update/disable; runtime-rule
      list/bundled/get/add/inactivate/delete/dump; dsl-debug status/sessions/
      session start|get|stop and oal files/file/rules/rule.
    
    OAP 11.0.0 adaptations:
    - alarm list: migrate getAlarm -> queryAlarms, add --layer/--rules filters.
    - menu get: detect the retired getMenuItems and report a clear message
      instead of a raw GraphQL error.
    
    E2E:
    - Bump OAP to 11.0.0+, switch storage from Elasticsearch to BanyanDB.
    - basic: layer list normalized via `yq sort`; trace cases migrated to
      trace-v2 (BanyanDB rejects the v1 trace API).
    - New `admin` case (static admin REST) and `live-debugging` case (OAL live
      capture, asserting the captured pipeline is exactly the bound metric).
    - New admin-command-tests and live-debugging-tests CI jobs.
---
 .github/workflows/CI.yaml                          |  58 ++++-
 CHANGES.md                                         |   3 +
 README.md                                          |   1 +
 assets/graphqls/alarm/alarms.graphql               |   4 +-
 cmd/swctl/main.go                                  |  11 +
 examples/.skywalking.yaml                          |   3 +
 internal/commands/admin/admin.go                   |  59 +++++
 internal/commands/admin/alarm/alarm.go             |  98 ++++++++
 .../{menu/get.go => admin/cluster/cluster.go}      |  35 +--
 internal/commands/admin/config/config.go           |  71 ++++++
 internal/commands/admin/dsldebug/dsldebug.go       | 185 +++++++++++++++
 internal/commands/admin/inspect/inspect.go         | 132 +++++++++++
 internal/commands/admin/oal/oal.go                 | 105 +++++++++
 .../commands/{menu/get.go => admin/preflight.go}   |  32 +--
 internal/commands/admin/runtimerule/runtimerule.go | 251 +++++++++++++++++++++
 internal/commands/admin/uitemplate/uitemplate.go   | 192 ++++++++++++++++
 internal/commands/alarm/list.go                    |  40 +++-
 internal/commands/interceptor/interceptor.go       |   3 +
 internal/commands/menu/get.go                      |  30 +++
 pkg/admin/client/client.go                         | 238 +++++++++++++++++++
 pkg/admin/client/client_test.go                    |  79 +++++++
 pkg/admin/dsldebug/dsldebug.go                     | 110 +++++++++
 pkg/admin/inspect/inspect.go                       | 123 ++++++++++
 pkg/admin/oal/oal.go                               |  67 ++++++
 pkg/admin/preflight/preflight.go                   | 121 ++++++++++
 pkg/admin/runtimerule/runtimerule.go               | 211 +++++++++++++++++
 pkg/admin/status/status.go                         | 115 ++++++++++
 pkg/admin/uitemplate/uitemplate.go                 |  92 ++++++++
 pkg/contextkey/contextkey.go                       |   1 +
 pkg/graphql/alarm/alarm.go                         |  47 +++-
 pkg/graphql/client/client.go                       |  32 +--
 pkg/transport/transport.go                         |  71 ++++++
 test/base/docker-compose.yml                       |  22 +-
 test/cases/{basic => admin}/docker-compose.yml     |  33 +--
 .../traces-list.yml => admin/expected/count.yml}   |  13 +-
 .../traces-list.yml => admin/expected/ok.yml}      |  13 +-
 .../traces-list.yml => admin/expected/true.yml}    |  13 +-
 test/cases/admin/test.yaml                         |  72 ++++++
 test/cases/basic/docker-compose.yml                |   8 +-
 test/cases/basic/expected/layer-list.yml           |  79 ++++---
 test/cases/basic/expected/trace-users-detail.yml   | 103 ---------
 test/cases/basic/test.yaml                         |  18 +-
 .../{basic => live-debugging}/docker-compose.yml   |  16 +-
 .../expected/ok.yml}                               |  13 +-
 test/cases/live-debugging/oal-debug-flow.sh        | 116 ++++++++++
 test/cases/live-debugging/test.yaml                |  46 ++++
 46 files changed, 2865 insertions(+), 320 deletions(-)

diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml
index b8960e4..f768792 100644
--- a/.github/workflows/CI.yaml
+++ b/.github/workflows/CI.yaml
@@ -30,6 +30,12 @@ concurrency:
   group: skywalking-cli-${{ github.event.pull_request.number || github.ref }}
   cancel-in-progress: true
 
+# OAP image tag used by the E2E suites. Must be an admin-capable build: the 
admin-server
+# REST host (port 17128), queryAlarms and the menu retirement landed in OAP 
11.0.0; the
+# OAL live-debugging case additionally needs the demo traffic to drive the 
captured pipeline.
+env:
+  OAP_TAG: 768d0693aac34ed49ce4d1c89e4a56df353e4140
+
 jobs:
   check-license:
     name: License header
@@ -85,10 +91,6 @@ jobs:
     name: Command Tests
     runs-on: ubuntu-latest
     if: github.repository == 'apache/skywalking-cli'
-    strategy:
-      matrix:
-        oap:
-          - 42c613bea94999a6cc8e805ed4c8c7659f3a735c
     steps:
       - uses: actions/checkout@v4
       - name: Set up Go
@@ -101,12 +103,50 @@ jobs:
 
       - name: Test commands
         uses: 
apache/skywalking-infra-e2e@cf589b4a0b9f8e6f436f78e9cfd94a1ee5494180
-        env:
-          OAP_TAG: ${{ matrix.oap }}
         with:
           e2e-file: test/cases/basic/test.yaml
 
 
+  admin-command-tests:
+    name: Admin Command Tests
+    runs-on: ubuntu-latest
+    if: github.repository == 'apache/skywalking-cli'
+    steps:
+      - uses: actions/checkout@v4
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: "1.26"
+
+      - name: Install swctl
+        run: make install DESTDIR=/usr/local/bin
+
+      - name: Test admin commands
+        uses: 
apache/skywalking-infra-e2e@cf589b4a0b9f8e6f436f78e9cfd94a1ee5494180
+        with:
+          e2e-file: test/cases/admin/test.yaml
+
+
+  live-debugging-tests:
+    name: Live Debugging Tests
+    runs-on: ubuntu-latest
+    if: github.repository == 'apache/skywalking-cli'
+    steps:
+      - uses: actions/checkout@v4
+      - name: Set up Go
+        uses: actions/setup-go@v5
+        with:
+          go-version: "1.26"
+
+      - name: Install swctl
+        run: make install DESTDIR=/usr/local/bin
+
+      - name: Test OAL live debugging
+        uses: 
apache/skywalking-infra-e2e@cf589b4a0b9f8e6f436f78e9cfd94a1ee5494180
+        with:
+          e2e-file: test/cases/live-debugging/test.yaml
+
+
   unit-tests:
     name: Unit Tests
     runs-on: ubuntu-latest
@@ -130,6 +170,8 @@ jobs:
       - check-license
       - build
       - command-tests
+      - admin-command-tests
+      - live-debugging-tests
       - unit-tests
     runs-on: ubuntu-latest
     timeout-minutes: 10
@@ -140,8 +182,12 @@ jobs:
           [[ ${checkLicense} == 'success' ]] || exit 1;
           build=${{ needs.build.result }};
           commandTests=${{ needs.command-tests.result }};
+          adminCommandTests=${{ needs.admin-command-tests.result }};
+          liveDebuggingTests=${{ needs.live-debugging-tests.result }};
           unitTests=${{ needs.unit-tests.result }};
           [[ ${build} == 'success' ]] || [[ ${build} == 'skipped' ]] || exit 3;
           [[ ${commandTests} == 'success' ]] || [[ ${commandTests} == 
'skipped' ]] || exit 4;
+          [[ ${adminCommandTests} == 'success' ]] || [[ ${adminCommandTests} 
== 'skipped' ]] || exit 6;
+          [[ ${liveDebuggingTests} == 'success' ]] || [[ ${liveDebuggingTests} 
== 'skipped' ]] || exit 7;
           [[ ${unitTests} == 'success' ]] || [[ ${unitTests} == 'skipped' ]] 
|| exit 5;
           exit 0;
diff --git a/CHANGES.md b/CHANGES.md
index 3af9896..6de747b 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -16,10 +16,13 @@ Release Notes.
 * Add the duration field in the `trace list` command by @mrproliu in 
https://github.com/apache/skywalking-cli/pull/225
 * Remove the oldest `queryTraceFromColdStage` query call in the `trace list` 
command by @mrproliu in https://github.com/apache/skywalking-cli/pull/225
 * Add the sub-command `profiling pprof` for pprof query API by @JophieQu in 
https://github.com/apache/skywalking-cli/pull/226
+* Add the `admin` command group for the OAP admin-server REST host (default 
port `17128`), with a new global `--admin-url` flag (derived from `--base-url` 
when unset). Covers every admin feature module: `admin preflight`; `admin 
cluster nodes`, `admin config dump|ttl`, `admin alarm rules|rule` (status); 
`admin inspect metrics|entities` (inspect); `admin ui-template 
list|get|create|update|disable` (ui-management); `admin runtime-rule 
list|bundled|get|add|inactivate|delete|dump` (runtime-r [...]
 
 ### Bug Fixes
 
 * Fix wrong process id format by @mrproliu in 
https://github.com/apache/skywalking-cli/pull/215
+* Migrate `alarm list` from the deprecated `getAlarm` GraphQL query to 
`queryAlarms`, adding `--layer` and `--rules` filters (OAP 11.0.0).
+* `menu get` now reports a clear message when the OAP backend no longer serves 
the UI menu (retired in OAP 11.0.0) instead of a raw GraphQL error.
 
 0.14.0
 ------------------
diff --git a/README.md b/README.md
index f52b887..06afeb7 100644
--- a/README.md
+++ b/README.md
@@ -178,6 +178,7 @@ The compatibility table here only lists fully compatible 
OAP versions, which mea
 | \> = 0.12.0    | \> = 9.5.0    |
 | \> = 0.13.0    | \> = 9.6.0    |
 | \> = 0.14.0    | \> = 10.2.0   |
+| \> = 0.15.0    | \> = 11.0.0   |
 
 # Contributing
 
diff --git a/assets/graphqls/alarm/alarms.graphql 
b/assets/graphqls/alarm/alarms.graphql
index da2cbbe..1e4b477 100644
--- a/assets/graphqls/alarm/alarms.graphql
+++ b/assets/graphqls/alarm/alarms.graphql
@@ -15,8 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 
-query ($duration: Duration!, $scope: Scope, $keyword: String, $paging: 
Pagination!, $tags: [AlarmTag]) {
-    result: getAlarm(duration: $duration, scope: $scope, keyword: $keyword, 
paging: $paging, tags: $tags) {
+query ($condition: AlarmQueryCondition!) {
+    result: queryAlarms(condition: $condition) {
         msgs {
             startTime
             scope
diff --git a/cmd/swctl/main.go b/cmd/swctl/main.go
index 6a29883..01ee2c9 100644
--- a/cmd/swctl/main.go
+++ b/cmd/swctl/main.go
@@ -22,6 +22,7 @@ import (
        "os"
        "runtime"
 
+       "github.com/apache/skywalking-cli/internal/commands/admin"
        "github.com/apache/skywalking-cli/internal/commands/alarm"
        "github.com/apache/skywalking-cli/internal/commands/browser"
        "github.com/apache/skywalking-cli/internal/commands/completion"
@@ -115,6 +116,7 @@ services, service instances, etc.`
                records.Command,
                menu.Command,
                hierarchy.Command,
+               admin.Command,
        }
 
        app.Before = interceptor.BeforeChain(
@@ -150,6 +152,15 @@ func flags() []cli.Flag {
                        Usage:    "base `url` of the OAP backend graphql 
service",
                        Value:    "http://127.0.0.1:12800/graphql";,
                }),
+               altsrc.NewStringFlag(&cli.StringFlag{
+                       Name:     "admin-url",
+                       Required: false,
+                       EnvVars:  []string{"SW_ADMIN_URL"},
+                       Usage: "base `url` of the OAP admin-server REST service 
(default port 17128), " +
+                               "used by `swctl admin ...` sub-commands. If 
empty, it is derived from `--base-url` " +
+                               "by reusing its host with port 17128.",
+                       Value: "",
+               }),
                altsrc.NewStringFlag(&cli.StringFlag{
                        Name:     "grpc-addr",
                        Usage:    "backend gRPC service address `<host:port>`",
diff --git a/examples/.skywalking.yaml b/examples/.skywalking.yaml
index 55125f6..4adaf92 100644
--- a/examples/.skywalking.yaml
+++ b/examples/.skywalking.yaml
@@ -16,6 +16,9 @@
 # under the License.
 
 base-url: http://demo.skywalking.apache.org/graphql
+# admin-url is the OAP admin-server REST host used by `swctl admin ...` 
sub-commands.
+# When omitted, it is derived from base-url's host with port 17128.
+admin-url: http://demo.skywalking.apache.org:17128
 grpc-addr: 127.0.0.1:11800
 username: basic-auth-username
 password: basic-auth-password
diff --git a/internal/commands/admin/admin.go b/internal/commands/admin/admin.go
new file mode 100644
index 0000000..e77b042
--- /dev/null
+++ b/internal/commands/admin/admin.go
@@ -0,0 +1,59 @@
+// Licensed to 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. Apache Software Foundation (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 admin
+
+import (
+       "github.com/urfave/cli/v2"
+
+       "github.com/apache/skywalking-cli/internal/commands/admin/alarm"
+       "github.com/apache/skywalking-cli/internal/commands/admin/cluster"
+       "github.com/apache/skywalking-cli/internal/commands/admin/config"
+       "github.com/apache/skywalking-cli/internal/commands/admin/dsldebug"
+       "github.com/apache/skywalking-cli/internal/commands/admin/inspect"
+       "github.com/apache/skywalking-cli/internal/commands/admin/oal"
+       "github.com/apache/skywalking-cli/internal/commands/admin/runtimerule"
+       "github.com/apache/skywalking-cli/internal/commands/admin/uitemplate"
+)
+
+// Command is the parent of every sub-command that talks to the OAP 
admin-server
+// REST host (default port 17128), as opposed to the public GraphQL surface on
+// `--base-url` (default port 12800). The admin host bundles the status, 
inspect,
+// ui-management, dsl-debugging and runtime-rule feature modules. Its address 
comes
+// from `--admin-url` (or is derived from `--base-url` with port 17128).
+var Command = &cli.Command{
+       Name:  "admin",
+       Usage: "Admin (REST) sub-commands that talk to the OAP admin-server 
(default port 17128)",
+       UsageText: `Admin sub-commands call the OAP admin-server REST host, a 
separate surface from the
+public GraphQL endpoint used by the other commands.
+
+The admin host address defaults to the "--base-url" host with port 17128; 
override it
+with the global "--admin-url" flag (or the SW_ADMIN_URL env var / "admin-url" 
config key).
+The admin host has no built-in authentication and is expected to sit behind a 
gateway;
+"--username"/"--password"/"--authorization" and "--insecure" apply to it the 
same way.`,
+       Subcommands: []*cli.Command{
+               preflightCommand,
+               cluster.Command,
+               config.Command,
+               alarm.Command,
+               inspect.Command,
+               uitemplate.Command,
+               runtimerule.Command,
+               dsldebug.Command,
+               oal.Command,
+       },
+}
diff --git a/internal/commands/admin/alarm/alarm.go 
b/internal/commands/admin/alarm/alarm.go
new file mode 100644
index 0000000..fdf3ead
--- /dev/null
+++ b/internal/commands/admin/alarm/alarm.go
@@ -0,0 +1,98 @@
+// Licensed to 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. Apache Software Foundation (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 alarm exposes the admin-server alarm runtime status (loaded rule
+// definitions and per-entity evaluation/window state). This is distinct from 
the
+// top-level `swctl alarm` command, which reads fired alarm records via 
GraphQL.
+package alarm
+
+import (
+       "fmt"
+
+       "github.com/urfave/cli/v2"
+
+       "github.com/apache/skywalking-cli/pkg/admin/preflight"
+       "github.com/apache/skywalking-cli/pkg/admin/status"
+       "github.com/apache/skywalking-cli/pkg/display"
+       "github.com/apache/skywalking-cli/pkg/display/displayable"
+)
+
+var Command = &cli.Command{
+       Name:  "alarm",
+       Usage: "Inspect alarm runtime status from the admin-server `status` 
module",
+       UsageText: `Inspect the alarm-running kernel: loaded rule definitions 
and per-entity
+evaluation/window state. This differs from "swctl alarm list", which returns 
fired
+alarm records from the GraphQL surface.`,
+       Subcommands: []*cli.Command{
+               rulesCommand,
+               ruleCommand,
+       },
+}
+
+var rulesCommand = &cli.Command{
+       Name:  "rules",
+       Usage: "List the loaded alarm rules per OAP node (GET 
/status/alarm/rules)",
+       UsageText: `List the loaded alarm rules, fanned out across every OAP 
node.
+
+Examples:
+1. List alarm rules:
+$ swctl admin alarm rules`,
+       Action: func(ctx *cli.Context) error {
+               rules, err := status.AlarmRules(ctx.Context)
+               if err != nil {
+                       return preflight.Explain(ctx.Context, err, 
preflight.ModuleStatus, "SW_STATUS")
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: rules})
+       },
+}
+
+var ruleCommand = &cli.Command{
+       Name:      "rule",
+       Usage:     "Show one alarm rule's definition and running state (GET 
/status/alarm/{ruleId}[/{entityName}])",
+       ArgsUsage: "<ruleId> [<entityName>]",
+       UsageText: `Show the definition and running state of a single alarm 
rule. When an
+entity name is given, the per-entity evaluation/window state is returned 
instead.
+
+Examples:
+1. Show a rule's running state:
+$ swctl admin alarm rule service_resp_time_rule
+
+2. Show the per-entity state of a rule:
+$ swctl admin alarm rule service_resp_time_rule mock_b_service`,
+       Action: func(ctx *cli.Context) error {
+               args := ctx.Args()
+               ruleID := args.Get(0)
+               if ruleID == "" {
+                       return fmt.Errorf("a <ruleId> argument is required")
+               }
+               entityName := args.Get(1)
+
+               var (
+                       result *status.ClusterAlarmStatus
+                       err    error
+               )
+               if entityName == "" {
+                       result, err = status.AlarmRule(ctx.Context, ruleID)
+               } else {
+                       result, err = status.AlarmRuleEntity(ctx.Context, 
ruleID, entityName)
+               }
+               if err != nil {
+                       return preflight.Explain(ctx.Context, err, 
preflight.ModuleStatus, "SW_STATUS")
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
diff --git a/internal/commands/menu/get.go 
b/internal/commands/admin/cluster/cluster.go
similarity index 61%
copy from internal/commands/menu/get.go
copy to internal/commands/admin/cluster/cluster.go
index c7b9aac..26229fa 100644
--- a/internal/commands/menu/get.go
+++ b/internal/commands/admin/cluster/cluster.go
@@ -15,29 +15,38 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package menu
+package cluster
 
 import (
+       "github.com/urfave/cli/v2"
+
+       "github.com/apache/skywalking-cli/pkg/admin/preflight"
+       "github.com/apache/skywalking-cli/pkg/admin/status"
        "github.com/apache/skywalking-cli/pkg/display"
        "github.com/apache/skywalking-cli/pkg/display/displayable"
-       "github.com/apache/skywalking-cli/pkg/graphql/menu"
-
-       "github.com/urfave/cli/v2"
 )
 
-var Get = &cli.Command{
-       Name:  "get",
-       Usage: "Get the UI menu items",
-       UsageText: `Get the UI menu items.
+var Command = &cli.Command{
+       Name:  "cluster",
+       Usage: "Inspect the OAP cluster from the admin-server `status` module",
+       Subcommands: []*cli.Command{
+               nodesCommand,
+       },
+}
+
+var nodesCommand = &cli.Command{
+       Name:  "nodes",
+       Usage: "List the OAP cluster peer nodes (GET /status/cluster/nodes)",
+       UsageText: `List the OAP cluster peer nodes as seen by the cluster 
coordinator.
 
 Examples:
-1. Get the UI menu items:
-$swctl menu get`,
+1. List cluster nodes:
+$ swctl admin cluster nodes`,
        Action: func(ctx *cli.Context) error {
-               menuItems, err := menu.GetItems(ctx.Context)
+               nodes, err := status.ClusterNodesQuery(ctx.Context)
                if err != nil {
-                       return err
+                       return preflight.Explain(ctx.Context, err, 
preflight.ModuleStatus, "SW_STATUS")
                }
-               return display.Display(ctx.Context, 
&displayable.Displayable{Data: menuItems})
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: nodes})
        },
 }
diff --git a/internal/commands/admin/config/config.go 
b/internal/commands/admin/config/config.go
new file mode 100644
index 0000000..f41468f
--- /dev/null
+++ b/internal/commands/admin/config/config.go
@@ -0,0 +1,71 @@
+// Licensed to 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. Apache Software Foundation (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 config
+
+import (
+       "github.com/urfave/cli/v2"
+
+       "github.com/apache/skywalking-cli/pkg/admin/preflight"
+       "github.com/apache/skywalking-cli/pkg/admin/status"
+       "github.com/apache/skywalking-cli/pkg/display"
+       "github.com/apache/skywalking-cli/pkg/display/displayable"
+)
+
+var Command = &cli.Command{
+       Name:  "config",
+       Usage: "Inspect the OAP effective configuration and TTL from the 
admin-server",
+       Subcommands: []*cli.Command{
+               dumpCommand,
+               ttlCommand,
+       },
+}
+
+var dumpCommand = &cli.Command{
+       Name:  "dump",
+       Usage: "Dump the OAP node's effective, secrets-redacted configuration 
(GET /debugging/config/dump)",
+       UsageText: `Dump the effective configuration of the OAP node as a flat 
map of
+"<module>.<provider>.<property>" keys. Secrets are redacted by OAP.
+
+Examples:
+1. Dump the effective configuration:
+$ swctl admin config dump`,
+       Action: func(ctx *cli.Context) error {
+               dump, err := status.ConfigDump(ctx.Context)
+               if err != nil {
+                       return preflight.Explain(ctx.Context, err, 
preflight.ModuleStatus, "SW_STATUS")
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: dump})
+       },
+}
+
+var ttlCommand = &cli.Command{
+       Name:  "ttl",
+       Usage: "Show the effective TTL configuration (GET /status/config/ttl)",
+       UsageText: `Show the effective metric / record / trace / log TTL bounds.
+
+Examples:
+1. Show the effective TTL configuration:
+$ swctl admin config ttl`,
+       Action: func(ctx *cli.Context) error {
+               ttl, err := status.ConfigTTL(ctx.Context)
+               if err != nil {
+                       return preflight.Explain(ctx.Context, err, 
preflight.ModuleStatus, "SW_STATUS")
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: ttl})
+       },
+}
diff --git a/internal/commands/admin/dsldebug/dsldebug.go 
b/internal/commands/admin/dsldebug/dsldebug.go
new file mode 100644
index 0000000..2d04953
--- /dev/null
+++ b/internal/commands/admin/dsldebug/dsldebug.go
@@ -0,0 +1,185 @@
+// Licensed to 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. Apache Software Foundation (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 dsldebug
+
+import (
+       "fmt"
+
+       "github.com/google/uuid"
+       "github.com/urfave/cli/v2"
+
+       "github.com/apache/skywalking-cli/pkg/admin/dsldebug"
+       "github.com/apache/skywalking-cli/pkg/admin/preflight"
+       "github.com/apache/skywalking-cli/pkg/display"
+       "github.com/apache/skywalking-cli/pkg/display/displayable"
+)
+
+var Command = &cli.Command{
+       Name:  "dsl-debug",
+       Usage: "Live MAL / LAL / OAL debugger via the admin-server 
`dsl-debugging` module",
+       UsageText: `Run sample-based debug sessions that capture how MAL / LAL 
/ OAL rules transform
+live ingest, with per-stage captured records.`,
+       Subcommands: []*cli.Command{
+               statusCommand,
+               sessionsCommand,
+               sessionCommand,
+       },
+}
+
+var statusCommand = &cli.Command{
+       Name:  "status",
+       Usage: "Show the dsl-debugging module health snapshot (GET 
/dsl-debugging/status)",
+       Action: func(ctx *cli.Context) error {
+               result, err := dsldebug.Status(ctx.Context)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+var sessionsCommand = &cli.Command{
+       Name:  "sessions",
+       Usage: "List the active debug sessions (GET /dsl-debugging/sessions)",
+       Action: func(ctx *cli.Context) error {
+               result, err := dsldebug.ListSessions(ctx.Context)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+var sessionCommand = &cli.Command{
+       Name:  "session",
+       Usage: "Start / poll / stop a single debug session",
+       Subcommands: []*cli.Command{
+               sessionStartCommand,
+               sessionGetCommand,
+               sessionStopCommand,
+       },
+}
+
+var sessionStartCommand = &cli.Command{
+       Name:  "start",
+       Usage: "Start a debug capture session (POST /dsl-debugging/session)",
+       UsageText: `Start a sample-based debug capture session.
+
+Examples:
+1. Debug a MAL metric:
+$ swctl admin dsl-debug session start --catalog otel-rules --name vm 
--rule-name vm_memory_used`,
+       Flags: []cli.Flag{
+               &cli.StringFlag{
+                       Name:     "catalog",
+                       Usage:    "session `catalog`: otel-rules / 
log-mal-rules / telegraf-rules / lal / oal",
+                       Required: true,
+               },
+               &cli.StringFlag{
+                       Name:     "name",
+                       Usage:    "rule file name (MAL/LAL) or OAL source class 
`name`",
+                       Required: true,
+               },
+               &cli.StringFlag{
+                       Name:     "rule-name",
+                       Usage:    "the metric / rule `name` within the file 
(OAL: same as --name)",
+                       Required: true,
+               },
+               &cli.StringFlag{
+                       Name:  "client-id",
+                       Usage: "stable per-debug-context `id`; a random UUID is 
generated when omitted",
+               },
+               &cli.StringFlag{
+                       Name:  "granularity",
+                       Usage: "LAL capture `granularity`: block (default) or 
statement",
+               },
+               &cli.IntFlag{
+                       Name:  "record-cap",
+                       Usage: fmt.Sprintf("max records to capture before the 
session is full (1-%d)", dsldebug.MaxRecordCap),
+               },
+               &cli.IntFlag{
+                       Name:  "retention-millis",
+                       Usage: fmt.Sprintf("session wall-clock retention in ms 
(max %d = 1h)", dsldebug.MaxRetentionMillis),
+               },
+       },
+       Action: func(ctx *cli.Context) error {
+               recordCap := ctx.Int("record-cap")
+               if recordCap < 0 || recordCap > dsldebug.MaxRecordCap {
+                       return fmt.Errorf("--record-cap must be between 1 and 
%d", dsldebug.MaxRecordCap)
+               }
+               retention := ctx.Int("retention-millis")
+               if retention < 0 || retention > dsldebug.MaxRetentionMillis {
+                       return fmt.Errorf("--retention-millis must be between 1 
and %d", dsldebug.MaxRetentionMillis)
+               }
+               clientID := ctx.String("client-id")
+               if clientID == "" {
+                       clientID = uuid.New().String()
+               }
+
+               result, err := dsldebug.StartSession(ctx.Context, 
&dsldebug.StartArgs{
+                       ClientID:        clientID,
+                       Catalog:         ctx.String("catalog"),
+                       Name:            ctx.String("name"),
+                       RuleName:        ctx.String("rule-name"),
+                       Granularity:     ctx.String("granularity"),
+                       RecordCap:       recordCap,
+                       RetentionMillis: retention,
+               })
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+var sessionGetCommand = &cli.Command{
+       Name:      "get",
+       Usage:     "Poll a session's captured records (GET 
/dsl-debugging/session/{id})",
+       ArgsUsage: "<sessionId>",
+       Action: func(ctx *cli.Context) error {
+               id := ctx.Args().Get(0)
+               if id == "" {
+                       return fmt.Errorf("a <sessionId> argument is required")
+               }
+               result, err := dsldebug.GetSession(ctx.Context, id)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+var sessionStopCommand = &cli.Command{
+       Name:      "stop",
+       Usage:     "Stop a session (POST /dsl-debugging/session/{id}/stop)",
+       ArgsUsage: "<sessionId>",
+       Action: func(ctx *cli.Context) error {
+               id := ctx.Args().Get(0)
+               if id == "" {
+                       return fmt.Errorf("a <sessionId> argument is required")
+               }
+               result, err := dsldebug.StopSession(ctx.Context, id)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+func explain(ctx *cli.Context, err error) error {
+       return preflight.Explain(ctx.Context, err, preflight.ModuleDSLDebug, 
"SW_DSL_DEBUGGING")
+}
diff --git a/internal/commands/admin/inspect/inspect.go 
b/internal/commands/admin/inspect/inspect.go
new file mode 100644
index 0000000..528d6c6
--- /dev/null
+++ b/internal/commands/admin/inspect/inspect.go
@@ -0,0 +1,132 @@
+// Licensed to 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. Apache Software Foundation (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 inspect
+
+import (
+       "fmt"
+
+       "github.com/urfave/cli/v2"
+
+       "github.com/apache/skywalking-cli/internal/commands/interceptor"
+       "github.com/apache/skywalking-cli/internal/flags"
+       "github.com/apache/skywalking-cli/internal/model"
+       "github.com/apache/skywalking-cli/pkg/admin/inspect"
+       "github.com/apache/skywalking-cli/pkg/admin/preflight"
+       "github.com/apache/skywalking-cli/pkg/display"
+       "github.com/apache/skywalking-cli/pkg/display/displayable"
+)
+
+var Command = &cli.Command{
+       Name:  "inspect",
+       Usage: "Browse the metric catalog and entities from the admin-server 
`inspect` module",
+       Subcommands: []*cli.Command{
+               metricsCommand,
+               entitiesCommand,
+       },
+}
+
+var metricsCommand = &cli.Command{
+       Name:  "metrics",
+       Usage: "List the registered metric catalog (GET /inspect/metrics)",
+       UsageText: `List the registered metrics with their type, scope and 
supported downsamplings.
+
+Examples:
+1. List every metric:
+$ swctl admin inspect metrics
+
+2. List service metrics that /inspect/entities accepts:
+$ swctl admin inspect metrics --catalog SERVICE --mqe-queryable`,
+       Flags: []cli.Flag{
+               &cli.StringFlag{
+                       Name:  "regex",
+                       Usage: "filter metric names by a Java `regex` (default 
matches all)",
+               },
+               &cli.StringSliceFlag{
+                       Name:  "type",
+                       Usage: "filter by metric `type` (REGULAR_VALUE / 
LABELED_VALUE / HEATMAP / SAMPLED_RECORD); repeatable",
+               },
+               &cli.StringSliceFlag{
+                       Name:  "catalog",
+                       Usage: "filter by `catalog` (SERVICE / SERVICE_INSTANCE 
/ ENDPOINT / *_RELATION); repeatable",
+               },
+               &cli.BoolFlag{
+                       Name:  "mqe-queryable",
+                       Usage: "return only metrics that /inspect/entities 
accepts (REGULAR_VALUE, LABELED_VALUE)",
+               },
+       },
+       Action: func(ctx *cli.Context) error {
+               metrics, err := inspect.ListMetrics(ctx.Context, 
inspect.MetricsOptions{
+                       Regex:        ctx.String("regex"),
+                       Types:        ctx.StringSlice("type"),
+                       Catalogs:     ctx.StringSlice("catalog"),
+                       MQEQueryable: ctx.Bool("mqe-queryable"),
+               })
+               if err != nil {
+                       return preflight.Explain(ctx.Context, err, 
preflight.ModuleInspect, "SW_INSPECT")
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: metrics})
+       },
+}
+
+var entitiesCommand = &cli.Command{
+       Name:  "entities",
+       Usage: "Enumerate the entities holding values for a metric (GET 
/inspect/entities)",
+       UsageText: `Enumerate the entities currently holding values for a 
metric over a time range.
+Each row carries an MQE-ready entity to paste into a follow-up "swctl metrics 
exec"
+(execExpression) query. Only REGULAR_VALUE / LABELED_VALUE metrics are 
accepted.
+
+Examples:
+1. Entities reporting service_cpm in the last 30 minutes:
+$ swctl admin inspect entities --metric service_cpm`,
+       Flags: flags.Flags(
+               flags.DurationFlags,
+               []cli.Flag{
+                       &cli.StringFlag{
+                               Name:     "metric",
+                               Usage:    "the `metric` name to enumerate 
entities for",
+                               Required: true,
+                       },
+                       &cli.IntFlag{
+                               Name:  "limit",
+                               Usage: fmt.Sprintf("max rows scanned at the 
storage layer (1-%d, server default 300)", inspect.MaxLimit),
+                       },
+               },
+       ),
+       Before: interceptor.BeforeChain(
+               interceptor.DurationInterceptor,
+       ),
+       Action: func(ctx *cli.Context) error {
+               limit := ctx.Int("limit")
+               if limit < 0 || limit > inspect.MaxLimit {
+                       return fmt.Errorf("--limit must be between 1 and %d", 
inspect.MaxLimit)
+               }
+               step := ctx.Generic("step").(*model.StepEnumValue).Selected
+
+               entities, err := inspect.ListEntities(ctx.Context, 
inspect.EntitiesOptions{
+                       Metric: ctx.String("metric"),
+                       Start:  ctx.String("start"),
+                       End:    ctx.String("end"),
+                       Step:   string(step),
+                       Limit:  limit,
+               })
+               if err != nil {
+                       return preflight.Explain(ctx.Context, err, 
preflight.ModuleInspect, "SW_INSPECT")
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: entities})
+       },
+}
diff --git a/internal/commands/admin/oal/oal.go 
b/internal/commands/admin/oal/oal.go
new file mode 100644
index 0000000..45805fb
--- /dev/null
+++ b/internal/commands/admin/oal/oal.go
@@ -0,0 +1,105 @@
+// Licensed to 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. Apache Software Foundation (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 oal
+
+import (
+       "fmt"
+
+       "github.com/urfave/cli/v2"
+
+       "github.com/apache/skywalking-cli/pkg/admin/oal"
+       "github.com/apache/skywalking-cli/pkg/admin/preflight"
+       "github.com/apache/skywalking-cli/pkg/display"
+       "github.com/apache/skywalking-cli/pkg/display/displayable"
+)
+
+var Command = &cli.Command{
+       Name:  "oal",
+       Usage: "Browse the read-only OAL catalog (the OAL debugger's rule 
picker)",
+       UsageText: `Read-only listing of loaded OAL files and the 
per-dispatcher source catalog used
+by the OAL live debugger. Hosted by the admin-server dsl-debugging module.`,
+       Subcommands: []*cli.Command{
+               filesCommand,
+               fileCommand,
+               rulesCommand,
+               ruleCommand,
+       },
+}
+
+var filesCommand = &cli.Command{
+       Name:  "files",
+       Usage: "List the loaded .oal file names (GET /runtime/oal/files)",
+       Action: func(ctx *cli.Context) error {
+               result, err := oal.ListFiles(ctx.Context)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+var fileCommand = &cli.Command{
+       Name:      "file",
+       Usage:     "Print the raw .oal text of one file (GET 
/runtime/oal/files/{name})",
+       ArgsUsage: "<name>",
+       Action: func(ctx *cli.Context) error {
+               name := ctx.Args().Get(0)
+               if name == "" {
+                       return fmt.Errorf("a <name> argument is required")
+               }
+               content, err := oal.GetFile(ctx.Context, name)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               fmt.Println(content)
+               return nil
+       },
+}
+
+var rulesCommand = &cli.Command{
+       Name:  "rules",
+       Usage: "List the per-dispatcher OAL source catalog (GET 
/runtime/oal/rules)",
+       Action: func(ctx *cli.Context) error {
+               result, err := oal.ListSources(ctx.Context)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+var ruleCommand = &cli.Command{
+       Name:      "rule",
+       Usage:     "Show one OAL source's per-metric holder status (GET 
/runtime/oal/rules/{source})",
+       ArgsUsage: "<source>",
+       Action: func(ctx *cli.Context) error {
+               source := ctx.Args().Get(0)
+               if source == "" {
+                       return fmt.Errorf("a <source> argument is required")
+               }
+               result, err := oal.GetSource(ctx.Context, source)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+func explain(ctx *cli.Context, err error) error {
+       return preflight.Explain(ctx.Context, err, preflight.ModuleDSLDebug, 
"SW_DSL_DEBUGGING")
+}
diff --git a/internal/commands/menu/get.go 
b/internal/commands/admin/preflight.go
similarity index 56%
copy from internal/commands/menu/get.go
copy to internal/commands/admin/preflight.go
index c7b9aac..1eea7f1 100644
--- a/internal/commands/menu/get.go
+++ b/internal/commands/admin/preflight.go
@@ -15,29 +15,33 @@
 // specific language governing permissions and limitations
 // under the License.
 
-package menu
+package admin
 
 import (
+       "github.com/urfave/cli/v2"
+
+       "github.com/apache/skywalking-cli/pkg/admin/preflight"
        "github.com/apache/skywalking-cli/pkg/display"
        "github.com/apache/skywalking-cli/pkg/display/displayable"
-       "github.com/apache/skywalking-cli/pkg/graphql/menu"
-
-       "github.com/urfave/cli/v2"
 )
 
-var Get = &cli.Command{
-       Name:  "get",
-       Usage: "Get the UI menu items",
-       UsageText: `Get the UI menu items.
+var preflightCommand = &cli.Command{
+       Name:  "preflight",
+       Usage: "Detect which admin feature modules are enabled on the OAP 
admin-server",
+       UsageText: `Reads the effective configuration from the admin host and 
reports which feature
+modules (status, inspect, ui-management, dsl-debugging, runtime-rule) are 
enabled.
 
 Examples:
-1. Get the UI menu items:
-$swctl menu get`,
+1. Check admin feature availability:
+$ swctl admin preflight`,
        Action: func(ctx *cli.Context) error {
-               menuItems, err := menu.GetItems(ctx.Context)
-               if err != nil {
-                       return err
+               // Run reports per-module enablement; a transport error means 
the admin host
+               // is unreachable, in which case we still print the 
(all-disabled) result so
+               // the user sees which admin URL was probed.
+               result, err := preflight.Run(ctx.Context)
+               if err != nil && !result.AdminReachable {
+                       return preflight.Explain(ctx.Context, err, 
preflight.ModuleAdminServer, "SW_ADMIN_SERVER")
                }
-               return display.Display(ctx.Context, 
&displayable.Displayable{Data: menuItems})
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
        },
 }
diff --git a/internal/commands/admin/runtimerule/runtimerule.go 
b/internal/commands/admin/runtimerule/runtimerule.go
new file mode 100644
index 0000000..fc93803
--- /dev/null
+++ b/internal/commands/admin/runtimerule/runtimerule.go
@@ -0,0 +1,251 @@
+// Licensed to 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. Apache Software Foundation (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 runtimerule
+
+import (
+       "fmt"
+       "os"
+
+       "github.com/urfave/cli/v2"
+
+       "github.com/apache/skywalking-cli/pkg/admin/preflight"
+       "github.com/apache/skywalking-cli/pkg/admin/runtimerule"
+       "github.com/apache/skywalking-cli/pkg/display"
+       "github.com/apache/skywalking-cli/pkg/display/displayable"
+       "github.com/apache/skywalking-cli/pkg/util"
+)
+
+var Command = &cli.Command{
+       Name:  "runtime-rule",
+       Usage: "Hot-update MAL / LAL rules via the admin-server 
`receiver-runtime-rule` module",
+       UsageText: `Add, override, inactivate and delete MAL / LAL rule files 
at runtime without
+restarting OAP, and inspect the live and bundled rule state.
+
+Catalogs: otel-rules, log-mal-rules, telegraf-rules, lal.`,
+       Subcommands: []*cli.Command{
+               listCommand,
+               bundledCommand,
+               getCommand,
+               addCommand,
+               inactivateCommand,
+               deleteCommand,
+               dumpCommand,
+       },
+}
+
+func catalogFlag(required bool) cli.Flag {
+       return &cli.StringFlag{
+               Name:     "catalog",
+               Usage:    "rule `catalog`: otel-rules / log-mal-rules / 
telegraf-rules / lal",
+               Required: required,
+       }
+}
+
+func nameFlag() cli.Flag {
+       return &cli.StringFlag{
+               Name:     "name",
+               Usage:    "rule `name`",
+               Required: true,
+       }
+}
+
+var listCommand = &cli.Command{
+       Name:  "list",
+       Usage: "List the live rule state per node (GET /runtime/rule/list)",
+       Flags: []cli.Flag{catalogFlag(false)},
+       Action: func(ctx *cli.Context) error {
+               result, err := runtimerule.List(ctx.Context, 
ctx.String("catalog"))
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+var bundledCommand = &cli.Command{
+       Name:  "bundled",
+       Usage: "List the static (bundled) rule twins for a catalog (GET 
/runtime/rule/bundled)",
+       Flags: []cli.Flag{
+               catalogFlag(true),
+               &cli.BoolFlag{
+                       Name:  "with-content",
+                       Usage: "include the bundled rule content",
+                       Value: true,
+               },
+       },
+       Action: func(ctx *cli.Context) error {
+               result, err := runtimerule.ListBundled(ctx.Context, 
ctx.String("catalog"), ctx.Bool("with-content"))
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+var getCommand = &cli.Command{
+       Name:  "get",
+       Usage: "Fetch a single rule's content + metadata (GET /runtime/rule)",
+       Flags: []cli.Flag{
+               catalogFlag(true),
+               nameFlag(),
+               &cli.StringFlag{
+                       Name:  "source",
+                       Usage: "`source` to read: runtime (default, DAO first) 
or bundled (static twin only)",
+               },
+       },
+       Action: func(ctx *cli.Context) error {
+               rule, err := runtimerule.Get(ctx.Context, 
ctx.String("catalog"), ctx.String("name"), ctx.String("source"), "")
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: rule})
+       },
+}
+
+var addCommand = &cli.Command{
+       Name:    "add",
+       Aliases: []string{"add-or-update"},
+       Usage:   "Push a new or updated rule from a YAML file (POST 
/runtime/rule/addOrUpdate)",
+       UsageText: `Push a new or updated MAL / LAL rule. The file holds the 
raw rule YAML.
+
+Examples:
+1. Apply a MAL rule:
+$ swctl admin runtime-rule add --catalog otel-rules --name vm -f vm.yaml`,
+       Flags: []cli.Flag{
+               catalogFlag(true),
+               nameFlag(),
+               &cli.StringFlag{
+                       Name:     "file",
+                       Aliases:  []string{"f"},
+                       Usage:    "`path` to the rule YAML",
+                       Required: true,
+               },
+               &cli.BoolFlag{
+                       Name:  "allow-storage-change",
+                       Usage: "approve an edit that moves the rule's storage 
identity (structural change)",
+               },
+               &cli.BoolFlag{
+                       Name:  "force",
+                       Usage: "re-run apply even when the content is 
byte-identical",
+               },
+       },
+       Action: func(ctx *cli.Context) error {
+               body, err := readFile(ctx.String("file"))
+               if err != nil {
+                       return err
+               }
+               result, err := runtimerule.AddOrUpdate(ctx.Context, 
ctx.String("catalog"), ctx.String("name"),
+                       body, ctx.Bool("allow-storage-change"), 
ctx.Bool("force"))
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+var inactivateCommand = &cli.Command{
+       Name:  "inactivate",
+       Usage: "Turn a rule off (POST /runtime/rule/inactivate)",
+       Flags: []cli.Flag{catalogFlag(true), nameFlag()},
+       Action: func(ctx *cli.Context) error {
+               result, err := runtimerule.Inactivate(ctx.Context, 
ctx.String("catalog"), ctx.String("name"))
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+var deleteCommand = &cli.Command{
+       Name:  "delete",
+       Usage: "Remove a rule (POST /runtime/rule/delete)",
+       UsageText: `Remove a rule. An active rule must be inactivated first 
(the server returns a
+409 requires_inactivate_first otherwise).
+
+Examples:
+1. Delete an inactivated rule:
+$ swctl admin runtime-rule delete --catalog otel-rules --name vm
+
+2. Revert a rule to its bundled twin:
+$ swctl admin runtime-rule delete --catalog otel-rules --name vm --mode 
revertToBundled`,
+       Flags: []cli.Flag{
+               catalogFlag(true),
+               nameFlag(),
+               &cli.StringFlag{
+                       Name:  "mode",
+                       Usage: "deletion `mode`; pass revertToBundled to 
restore the static twin",
+               },
+       },
+       Action: func(ctx *cli.Context) error {
+               result, err := runtimerule.Delete(ctx.Context, 
ctx.String("catalog"), ctx.String("name"), ctx.String("mode"))
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: result})
+       },
+}
+
+var dumpCommand = &cli.Command{
+       Name:  "dump",
+       Usage: "Download a tar.gz snapshot of all rules (GET 
/runtime/rule/dump)",
+       UsageText: `Download a tar.gz snapshot of all rules, or one catalog's.
+
+Examples:
+1. Dump every rule to a file:
+$ swctl admin runtime-rule dump -o rules.tar.gz
+
+2. Dump one catalog:
+$ swctl admin runtime-rule dump --catalog otel-rules -o otel.tar.gz`,
+       Flags: []cli.Flag{
+               catalogFlag(false),
+               &cli.StringFlag{
+                       Name:     "output",
+                       Aliases:  []string{"o"},
+                       Usage:    "`path` to write the tar.gz to (default: 
stdout)",
+                       Required: true,
+               },
+       },
+       Action: func(ctx *cli.Context) error {
+               data, err := runtimerule.Dump(ctx.Context, 
ctx.String("catalog"))
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               out := util.ExpandFilePath(ctx.String("output"))
+               if err := os.WriteFile(out, data, 0o600); err != nil {
+                       return fmt.Errorf("failed to write dump to %q: %w", 
out, err)
+               }
+               fmt.Printf("wrote %d bytes to %s\n", len(data), out)
+               return nil
+       },
+}
+
+func readFile(path string) (string, error) {
+       content, err := os.ReadFile(util.ExpandFilePath(path))
+       if err != nil {
+               return "", fmt.Errorf("failed to read rule file %q: %w", path, 
err)
+       }
+       // Send the rule bytes verbatim. The runtime-rule API hashes the raw 
body for its
+       // contentHash / no-change detection, so the CLI must not normalize 
whitespace —
+       // trimming or re-adding a trailing newline would make a byte-identical 
rule look
+       // like a change.
+       return string(content), nil
+}
+
+func explain(ctx *cli.Context, err error) error {
+       return preflight.Explain(ctx.Context, err, preflight.ModuleRuntimeRule, 
"SW_RECEIVER_RUNTIME_RULE")
+}
diff --git a/internal/commands/admin/uitemplate/uitemplate.go 
b/internal/commands/admin/uitemplate/uitemplate.go
new file mode 100644
index 0000000..9db9fc1
--- /dev/null
+++ b/internal/commands/admin/uitemplate/uitemplate.go
@@ -0,0 +1,192 @@
+// Licensed to 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. Apache Software Foundation (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 uitemplate
+
+import (
+       "fmt"
+       "os"
+       "strings"
+
+       "github.com/google/uuid"
+       "github.com/urfave/cli/v2"
+
+       "github.com/apache/skywalking-cli/pkg/admin/preflight"
+       "github.com/apache/skywalking-cli/pkg/admin/uitemplate"
+       "github.com/apache/skywalking-cli/pkg/display"
+       "github.com/apache/skywalking-cli/pkg/display/displayable"
+       "github.com/apache/skywalking-cli/pkg/util"
+)
+
+var Command = &cli.Command{
+       Name:  "ui-template",
+       Usage: "Manage dashboard templates via the admin-server `ui-management` 
module",
+       UsageText: `Manage OAP dashboard templates over REST. This replaces the 
GraphQL
+UIConfigurationManagement template mutations retired in SkyWalking 11.0.0. 
There is no
+delete; templates are soft-disabled.`,
+       Subcommands: []*cli.Command{
+               listCommand,
+               getCommand,
+               createCommand,
+               updateCommand,
+               disableCommand,
+       },
+}
+
+var listCommand = &cli.Command{
+       Name:  "list",
+       Usage: "List all dashboard templates (GET /ui-management/templates)",
+       UsageText: `List all dashboard templates.
+
+Examples:
+1. List enabled templates:
+$ swctl admin ui-template list
+
+2. Include soft-disabled templates:
+$ swctl admin ui-template list --include-disabled`,
+       Flags: []cli.Flag{
+               &cli.BoolFlag{
+                       Name:  "include-disabled",
+                       Usage: "also return soft-disabled templates",
+               },
+       },
+       Action: func(ctx *cli.Context) error {
+               templates, err := uitemplate.List(ctx.Context, 
ctx.Bool("include-disabled"))
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: templates})
+       },
+}
+
+var getCommand = &cli.Command{
+       Name:      "get",
+       Usage:     "Get a single dashboard template by ID (GET 
/ui-management/templates/{id})",
+       ArgsUsage: "<id>",
+       Action: func(ctx *cli.Context) error {
+               id := ctx.Args().Get(0)
+               if id == "" {
+                       return fmt.Errorf("an <id> argument is required")
+               }
+               template, err := uitemplate.Get(ctx.Context, id)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: template})
+       },
+}
+
+var createCommand = &cli.Command{
+       Name:  "create",
+       Usage: "Add a new dashboard template (POST /ui-management/templates)",
+       UsageText: `Add a new dashboard template. The file holds the 
JSON-encoded template
+configuration. An id is required by OAP; a random UUID is generated when --id 
is omitted.
+
+Examples:
+1. Create a template from a file:
+$ swctl admin ui-template create -f my-dashboard.json`,
+       Flags: []cli.Flag{
+               &cli.StringFlag{
+                       Name:     "file",
+                       Aliases:  []string{"f"},
+                       Usage:    "`path` to the JSON-encoded template 
configuration",
+                       Required: true,
+               },
+               &cli.StringFlag{
+                       Name:  "id",
+                       Usage: "template `id`; a random UUID is generated when 
omitted",
+               },
+       },
+       Action: func(ctx *cli.Context) error {
+               configuration, err := readFile(ctx.String("file"))
+               if err != nil {
+                       return err
+               }
+               id := ctx.String("id")
+               if id == "" {
+                       id = uuid.New().String()
+               }
+               status, err := uitemplate.Create(ctx.Context, id, configuration)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: status})
+       },
+}
+
+var updateCommand = &cli.Command{
+       Name:  "update",
+       Usage: "Update an existing dashboard template (PUT 
/ui-management/templates)",
+       UsageText: `Update an existing dashboard template by ID with a new 
configuration.
+
+Examples:
+1. Update a template:
+$ swctl admin ui-template update --id <uuid> -f my-dashboard.json`,
+       Flags: []cli.Flag{
+               &cli.StringFlag{
+                       Name:     "id",
+                       Usage:    "`id` of the template to update",
+                       Required: true,
+               },
+               &cli.StringFlag{
+                       Name:     "file",
+                       Aliases:  []string{"f"},
+                       Usage:    "`path` to the JSON-encoded template 
configuration",
+                       Required: true,
+               },
+       },
+       Action: func(ctx *cli.Context) error {
+               configuration, err := readFile(ctx.String("file"))
+               if err != nil {
+                       return err
+               }
+               status, err := uitemplate.Update(ctx.Context, ctx.String("id"), 
configuration)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: status})
+       },
+}
+
+var disableCommand = &cli.Command{
+       Name:      "disable",
+       Usage:     "Soft-disable a dashboard template (POST 
/ui-management/templates/{id}/disable)",
+       ArgsUsage: "<id>",
+       Action: func(ctx *cli.Context) error {
+               id := ctx.Args().Get(0)
+               if id == "" {
+                       return fmt.Errorf("an <id> argument is required")
+               }
+               status, err := uitemplate.Disable(ctx.Context, id)
+               if err != nil {
+                       return explain(ctx, err)
+               }
+               return display.Display(ctx.Context, 
&displayable.Displayable{Data: status})
+       },
+}
+
+func readFile(path string) (string, error) {
+       content, err := os.ReadFile(util.ExpandFilePath(path))
+       if err != nil {
+               return "", fmt.Errorf("failed to read template file %q: %w", 
path, err)
+       }
+       return strings.TrimSpace(string(content)), nil
+}
+
+func explain(ctx *cli.Context, err error) error {
+       return preflight.Explain(ctx.Context, err, preflight.ModuleUIManage, 
"SW_UI_MANAGEMENT")
+}
diff --git a/internal/commands/alarm/list.go b/internal/commands/alarm/list.go
index beae908..5430630 100644
--- a/internal/commands/alarm/list.go
+++ b/internal/commands/alarm/list.go
@@ -29,6 +29,7 @@ import (
        "github.com/apache/skywalking-cli/pkg/display"
        "github.com/apache/skywalking-cli/pkg/display/displayable"
        "github.com/apache/skywalking-cli/pkg/graphql/alarm"
+       "github.com/apache/skywalking-cli/pkg/logger"
 
        api "skywalking.apache.org/repo/goapi/query"
 )
@@ -58,9 +59,19 @@ $ swctl alarm list
                                Usage:    "`tags` of the alarm, in form of 
`key=value,key=value`",
                                Required: false,
                        },
+                       &cli.StringFlag{
+                               Name:     "layer",
+                               Usage:    "filter alarms by the underlying 
entity `layer`, e.g. GENERAL, MESH",
+                               Required: false,
+                       },
+                       &cli.StringFlag{
+                               Name:     "rules",
+                               Usage:    "filter alarms by the alarm `rule` 
name(s) that fired them, comma-separated",
+                               Required: false,
+                       },
                        &cli.GenericFlag{
                                Name:  "scope",
-                               Usage: "the `scope` of the alarm entity",
+                               Usage: "(deprecated) the `scope` of the alarm 
entity; ignored, queryAlarms filters by entity/layer/rule instead",
                                Value: &model.ScopeEnumValue{
                                        Enum:     api.AllScope,
                                        Default:  "",
@@ -80,7 +91,21 @@ $ swctl alarm list
 
                keyword := ctx.String("keyword")
                tagStr := ctx.String("tags")
-               scope := ctx.Generic("scope").(*model.ScopeEnumValue).Selected
+               layer := ctx.String("layer")
+
+               if ctx.IsSet("scope") {
+                       logger.Log.Warn("--scope is deprecated and ignored: the 
queryAlarms API filters by entity/layer/rule, " +
+                               "not by a bare scope. Use --layer and --rules 
instead.")
+               }
+
+               var ruleNames []string
+               if rules := ctx.String("rules"); rules != "" {
+                       for rule := range strings.SplitSeq(rules, ",") {
+                               if r := strings.TrimSpace(rule); r != "" {
+                                       ruleNames = append(ruleNames, r)
+                               }
+                       }
+               }
 
                duration := api.Duration{
                        Start:     start,
@@ -108,11 +133,12 @@ $ swctl alarm list
                }
 
                condition := &alarm.ListAlarmCondition{
-                       Duration: &duration,
-                       Keyword:  keyword,
-                       Scope:    scope,
-                       Tags:     tags,
-                       Paging:   &paging,
+                       Duration:  &duration,
+                       Keyword:   keyword,
+                       Tags:      tags,
+                       Paging:    &paging,
+                       Layer:     layer,
+                       RuleNames: ruleNames,
                }
                alarms, err := alarm.Alarms(ctx.Context, condition)
                if err != nil {
diff --git a/internal/commands/interceptor/interceptor.go 
b/internal/commands/interceptor/interceptor.go
index 9cb873e..32bbf0b 100644
--- a/internal/commands/interceptor/interceptor.go
+++ b/internal/commands/interceptor/interceptor.go
@@ -20,6 +20,7 @@ package interceptor
 import (
        "context"
 
+       adminclient "github.com/apache/skywalking-cli/pkg/admin/client"
        "github.com/apache/skywalking-cli/pkg/contextkey"
 
        "github.com/urfave/cli/v2"
@@ -30,6 +31,8 @@ func BeforeChain(beforeFunctions ...cli.BeforeFunc) 
cli.BeforeFunc {
        return func(cliCtx *cli.Context) error {
                ctx := cliCtx.Context
                ctx = context.WithValue(ctx, contextkey.BaseURL{}, 
cliCtx.String("base-url"))
+               ctx = context.WithValue(ctx, contextkey.AdminURL{},
+                       adminclient.DeriveAdminURL(cliCtx.String("base-url"), 
cliCtx.String("admin-url")))
                ctx = context.WithValue(ctx, contextkey.Insecure{}, 
cliCtx.Bool("insecure"))
                ctx = context.WithValue(ctx, contextkey.Username{}, 
cliCtx.String("username"))
                ctx = context.WithValue(ctx, contextkey.Password{}, 
cliCtx.String("password"))
diff --git a/internal/commands/menu/get.go b/internal/commands/menu/get.go
index c7b9aac..d41ae1f 100644
--- a/internal/commands/menu/get.go
+++ b/internal/commands/menu/get.go
@@ -18,6 +18,9 @@
 package menu
 
 import (
+       "fmt"
+       "strings"
+
        "github.com/apache/skywalking-cli/pkg/display"
        "github.com/apache/skywalking-cli/pkg/display/displayable"
        "github.com/apache/skywalking-cli/pkg/graphql/menu"
@@ -36,8 +39,35 @@ $swctl menu get`,
        Action: func(ctx *cli.Context) error {
                menuItems, err := menu.GetItems(ctx.Context)
                if err != nil {
+                       if isMenuUnsupported(err) {
+                               return fmt.Errorf("this OAP version no longer 
serves the UI menu: the `getMenuItems` " +
+                                       "GraphQL query was retired in 
SkyWalking 11.0.0, where the OAP backend stopped storing " +
+                                       "and serving the sidebar menu. The menu 
is now owned client-side by Horizon UI " +
+                                       
"(https://github.com/apache/skywalking-horizon-ui). Upgrade/downgrade swctl to 
match your " +
+                                       "OAP version, or stop using `swctl menu 
get` against 11.0.0+ backends")
+                       }
                        return err
                }
                return display.Display(ctx.Context, 
&displayable.Displayable{Data: menuItems})
        },
 }
+
+// isMenuUnsupported reports whether err is the GraphQL schema-validation 
error raised
+// by an OAP backend that no longer defines the retired `getMenuItems` query 
(11.0.0+),
+// as opposed to a transport or other runtime error. graphql-java phrases this 
as a
+// "FieldUndefined" validation error; we match defensively on the field name 
plus a
+// validation marker so wording changes do not break detection.
+func isMenuUnsupported(err error) bool {
+       if err == nil {
+               return false
+       }
+       msg := err.Error()
+       if !strings.Contains(msg, "getMenuItems") {
+               return false
+       }
+       lower := strings.ToLower(msg)
+       return strings.Contains(lower, "fieldundefined") ||
+               strings.Contains(lower, "undefined") ||
+               strings.Contains(lower, "cannot query field") ||
+               strings.Contains(lower, "validation error")
+}
diff --git a/pkg/admin/client/client.go b/pkg/admin/client/client.go
new file mode 100644
index 0000000..ec431ac
--- /dev/null
+++ b/pkg/admin/client/client.go
@@ -0,0 +1,238 @@
+// Licensed to 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. Apache Software Foundation (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 client is the REST client for the OAP admin-server (default port 
17128),
+// the HTTP host that bundles the status, inspect, ui-management, 
dsl-debugging and
+// runtime-rule feature modules. It is the REST counterpart of 
pkg/graphql/client
+// and shares its TLS / basic-auth handling via pkg/transport.
+package client
+
+import (
+       "context"
+       "encoding/json"
+       "fmt"
+       "io"
+       "net"
+       "net/http"
+       "net/url"
+       "strings"
+       "time"
+
+       "github.com/apache/skywalking-cli/pkg/contextkey"
+       "github.com/apache/skywalking-cli/pkg/transport"
+)
+
+const (
+       // DefaultAdminURL is the fallback admin-server REST base URL.
+       DefaultAdminURL = "http://127.0.0.1:17128";
+       // DefaultAdminPort is the admin-server REST port (admin-server `port`).
+       DefaultAdminPort = "17128"
+
+       defaultTimeout = 30 * time.Second
+
+       contentTypeJSON = "application/json"
+)
+
+// DeriveAdminURL resolves the admin REST base URL. When adminURL is non-empty 
it is
+// used verbatim (whitespace and trailing slash trimmed). Otherwise the admin 
URL is
+// derived from the GraphQL base URL by reusing its scheme and host and 
swapping in the
+// admin port, dropping any path such as `/graphql`. A non-parseable base URL 
falls
+// back to DefaultAdminURL.
+func DeriveAdminURL(baseURL, adminURL string) string {
+       if s := strings.TrimRight(strings.TrimSpace(adminURL), "/"); s != "" {
+               return s
+       }
+       u, err := url.Parse(strings.TrimSpace(baseURL))
+       if err != nil || u.Hostname() == "" {
+               return DefaultAdminURL
+       }
+       scheme := u.Scheme
+       if scheme == "" {
+               scheme = "http"
+       }
+       // net.JoinHostPort brackets IPv6 hosts, e.g. http://[::1]:17128.
+       return scheme + "://" + net.JoinHostPort(u.Hostname(), DefaultAdminPort)
+}
+
+// BaseURL returns the admin REST base URL stored in the context (set by the
+// interceptor), trailing slash trimmed, falling back to DefaultAdminURL.
+func BaseURL(ctx context.Context) string {
+       return strings.TrimRight(transport.GetValue(ctx, contextkey.AdminURL{}, 
DefaultAdminURL), "/")
+}
+
+// Request describes a single admin REST call.
+type Request struct {
+       Method      string
+       Path        string // appended to the admin base URL, e.g. 
"/status/cluster/nodes"
+       Query       url.Values
+       Body        io.Reader
+       ContentType string            // request Content-Type, when a body is 
sent
+       Accept      string            // Accept header; defaults to 
application/json
+       Headers     map[string]string // extra request headers (e.g. 
If-None-Match)
+}
+
+// Response is the raw result of an admin REST call, exposed so callers can 
read
+// status-dependent headers (e.g. runtime-rule X-Sw-* / ETag) and bodies 
(tar.gz).
+type Response struct {
+       StatusCode int
+       Header     http.Header
+       Body       []byte
+       URL        string
+}
+
+// IsSuccess reports whether the response carries a 2xx status code.
+func (r *Response) IsSuccess() bool {
+       return r.StatusCode >= 200 && r.StatusCode < 300
+}
+
+// APIError is returned for non-2xx admin responses. It decodes the common 
admin
+// error envelopes so callers can switch on the semantic Status code (e.g.
+// requires_inactivate_first, session_not_found, cluster_view_split).
+type APIError struct {
+       StatusCode int
+       URL        string
+       Status     string // applyStatus / code / status from the JSON envelope
+       Message    string
+       Body       string
+}
+
+func (e *APIError) Error() string {
+       msg := e.Message
+       if msg == "" {
+               msg = e.Body
+       }
+       if e.Status != "" {
+               return fmt.Sprintf("admin API %s: HTTP %d (%s): %s", e.URL, 
e.StatusCode, e.Status, msg)
+       }
+       return fmt.Sprintf("admin API %s: HTTP %d: %s", e.URL, e.StatusCode, 
msg)
+}
+
+// ParseError builds an APIError from a non-2xx response, decoding the admin 
error
+// envelopes: {applyStatus,message} (runtime-rule), {status,code,message}
+// (dsl-debugging / oal) and {error} (inspect). A non-JSON body is kept as raw 
text.
+func ParseError(resp *Response) *APIError {
+       e := &APIError{StatusCode: resp.StatusCode, URL: resp.URL, Body: 
strings.TrimSpace(string(resp.Body))}
+       var env struct {
+               ApplyStatus string `json:"applyStatus"`
+               Status      string `json:"status"`
+               Code        string `json:"code"`
+               Message     string `json:"message"`
+               Error       string `json:"error"`
+       }
+       if json.Unmarshal(resp.Body, &env) == nil {
+               switch {
+               case env.ApplyStatus != "":
+                       e.Status = env.ApplyStatus
+               case env.Code != "":
+                       e.Status = env.Code
+               case env.Status != "":
+                       e.Status = env.Status
+               }
+               switch {
+               case env.Message != "":
+                       e.Message = env.Message
+               case env.Error != "":
+                       e.Message = env.Error
+               }
+       }
+       return e
+}
+
+// Do performs req against the admin base URL and returns the raw Response for 
any
+// HTTP status. Only transport-level failures are returned as an error; callers
+// decide how to treat non-2xx (see Response.IsSuccess / ParseError). It 
reuses the
+// shared TLS and basic-auth handling from pkg/transport.
+func Do(ctx context.Context, req *Request) (*Response, error) {
+       full := BaseURL(ctx) + req.Path
+       if len(req.Query) > 0 {
+               full += "?" + req.Query.Encode()
+       }
+
+       httpReq, err := http.NewRequestWithContext(ctx, req.Method, full, 
req.Body)
+       if err != nil {
+               return nil, err
+       }
+       accept := req.Accept
+       if accept == "" {
+               accept = contentTypeJSON
+       }
+       httpReq.Header.Set("Accept", accept)
+       if req.ContentType != "" {
+               httpReq.Header.Set("Content-Type", req.ContentType)
+       }
+       if auth := transport.AuthHeader(ctx); auth != "" {
+               httpReq.Header.Set("Authorization", auth)
+       }
+       for k, v := range req.Headers {
+               httpReq.Header.Set(k, v)
+       }
+
+       httpClient := transport.HTTPClient(ctx)
+       httpClient.Timeout = defaultTimeout
+       resp, err := httpClient.Do(httpReq)
+       if err != nil {
+               return nil, err
+       }
+       defer resp.Body.Close()
+
+       body, err := io.ReadAll(resp.Body)
+       if err != nil {
+               return nil, err
+       }
+       return &Response{StatusCode: resp.StatusCode, Header: resp.Header, 
Body: body, URL: full}, nil
+}
+
+// GetJSON issues a GET to path and decodes a JSON 2xx response into out 
(which may be
+// nil to discard the body). A non-2xx response is returned as an *APIError.
+func GetJSON(ctx context.Context, path string, query url.Values, out any) 
error {
+       resp, err := Do(ctx, &Request{Method: http.MethodGet, Path: path, 
Query: query})
+       if err != nil {
+               return err
+       }
+       if !resp.IsSuccess() {
+               return ParseError(resp)
+       }
+       if out == nil || len(resp.Body) == 0 {
+               return nil
+       }
+       return json.Unmarshal(resp.Body, out)
+}
+
+// SendJSON issues method to path with an optional JSON body and decodes a 
JSON 2xx
+// response into out (which may be nil). A non-2xx response is returned as an 
*APIError.
+func SendJSON(ctx context.Context, method, path string, query url.Values, in, 
out any) error {
+       req := &Request{Method: method, Path: path, Query: query}
+       if in != nil {
+               data, err := json.Marshal(in)
+               if err != nil {
+                       return err
+               }
+               req.Body = strings.NewReader(string(data))
+               req.ContentType = contentTypeJSON
+       }
+       resp, err := Do(ctx, req)
+       if err != nil {
+               return err
+       }
+       if !resp.IsSuccess() {
+               return ParseError(resp)
+       }
+       if out == nil || len(resp.Body) == 0 {
+               return nil
+       }
+       return json.Unmarshal(resp.Body, out)
+}
diff --git a/pkg/admin/client/client_test.go b/pkg/admin/client/client_test.go
new file mode 100644
index 0000000..006b93f
--- /dev/null
+++ b/pkg/admin/client/client_test.go
@@ -0,0 +1,79 @@
+// Licensed to 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. Apache Software Foundation (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 client
+
+import "testing"
+
+func TestDeriveAdminURL(t *testing.T) {
+       tests := []struct {
+               name     string
+               baseURL  string
+               adminURL string
+               want     string
+       }{
+               {
+                       name:    "derive from default graphql base-url",
+                       baseURL: "http://127.0.0.1:12800/graphql";,
+                       want:    "http://127.0.0.1:17128";,
+               },
+               {
+                       name:    "derive keeps the base host",
+                       baseURL: "http://1.2.3.4:12800/graphql";,
+                       want:    "http://1.2.3.4:17128";,
+               },
+               {
+                       name:    "derive preserves https scheme",
+                       baseURL: "https://oap.example.com:12800/graphql";,
+                       want:    "https://oap.example.com:17128";,
+               },
+               {
+                       name:    "derive brackets an IPv6 host",
+                       baseURL: "http://[::1]:12800/graphql";,
+                       want:    "http://[::1]:17128";,
+               },
+               {
+                       name:     "explicit admin-url wins and trailing slash 
trimmed",
+                       baseURL:  "http://1.2.3.4:12800/graphql";,
+                       adminURL: "http://admin.example.com:17128/";,
+                       want:     "http://admin.example.com:17128";,
+               },
+               {
+                       name:     "explicit admin-url with whitespace",
+                       baseURL:  "http://1.2.3.4:12800/graphql";,
+                       adminURL: "  http://admin:17128  ",
+                       want:     "http://admin:17128";,
+               },
+               {
+                       name:    "empty base-url falls back to default admin 
url",
+                       baseURL: "",
+                       want:    DefaultAdminURL,
+               },
+               {
+                       name:    "host without scheme defaults to http",
+                       baseURL: "//demo.skywalking.apache.org:12800/graphql",
+                       want:    "http://demo.skywalking.apache.org:17128";,
+               },
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       if got := DeriveAdminURL(tt.baseURL, tt.adminURL); got 
!= tt.want {
+                               t.Errorf("DeriveAdminURL(%q, %q) = %q, want 
%q", tt.baseURL, tt.adminURL, got, tt.want)
+                       }
+               })
+       }
+}
diff --git a/pkg/admin/dsldebug/dsldebug.go b/pkg/admin/dsldebug/dsldebug.go
new file mode 100644
index 0000000..a87fb52
--- /dev/null
+++ b/pkg/admin/dsldebug/dsldebug.go
@@ -0,0 +1,110 @@
+// Licensed to 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. Apache Software Foundation (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 dsldebug wraps the OAP admin-server `dsl-debugging` feature module: 
a
+// sample-based live debugger for MAL / LAL / OAL rules. The deeply-nested 
per-stage
+// capture payloads are passed through as generic JSON for display; only the 
request
+// parameters and client-side limits are modeled.
+package dsldebug
+
+import (
+       "context"
+       "net/http"
+       "net/url"
+
+       "github.com/apache/skywalking-cli/pkg/admin/client"
+)
+
+const (
+       // MaxRecordCap mirrors OAP's SessionLimits.MAX_RECORD_CAP.
+       MaxRecordCap = 100
+       // MaxRetentionMillis mirrors OAP's SessionLimits.MAX_RETENTION_MILLIS 
(1 hour).
+       MaxRetentionMillis = 60 * 60 * 1000
+)
+
+// Catalogs accepted by a debug session.
+var Catalogs = []string{"otel-rules", "log-mal-rules", "telegraf-rules", 
"lal", "oal"}
+
+// StartArgs holds the inputs of POST /dsl-debugging/session. Catalog, Name, 
RuleName
+// and ClientID are mandatory query params; RecordCap / RetentionMillis are 
optional and
+// sent as a JSON body only when set. Granularity (LAL only) is sent as a 
query param.
+type StartArgs struct {
+       ClientID        string
+       Catalog         string
+       Name            string
+       RuleName        string
+       Granularity     string
+       RecordCap       int
+       RetentionMillis int
+}
+
+// StartSession opens a debug capture session.
+func StartSession(ctx context.Context, a *StartArgs) (any, error) {
+       query := url.Values{
+               "catalog":  []string{a.Catalog},
+               "name":     []string{a.Name},
+               "ruleName": []string{a.RuleName},
+               "clientId": []string{a.ClientID},
+       }
+       if a.Granularity != "" {
+               query.Set("granularity", a.Granularity)
+       }
+
+       body := map[string]any{}
+       if a.RecordCap > 0 {
+               body["recordCap"] = a.RecordCap
+       }
+       if a.RetentionMillis > 0 {
+               body["retentionMillis"] = a.RetentionMillis
+       }
+       var in any
+       if len(body) > 0 {
+               in = body
+       }
+
+       var out any
+       err := client.SendJSON(ctx, http.MethodPost, "/dsl-debugging/session", 
query, in, &out)
+       return out, err
+}
+
+// GetSession polls a session's captured records (GET 
/dsl-debugging/session/{id}).
+func GetSession(ctx context.Context, id string) (any, error) {
+       var out any
+       err := client.GetJSON(ctx, 
"/dsl-debugging/session/"+url.PathEscape(id), nil, &out)
+       return out, err
+}
+
+// StopSession stops a session (POST /dsl-debugging/session/{id}/stop). 
Idempotent.
+func StopSession(ctx context.Context, id string) (any, error) {
+       var out any
+       err := client.SendJSON(ctx, http.MethodPost, 
"/dsl-debugging/session/"+url.PathEscape(id)+"/stop", nil, nil, &out)
+       return out, err
+}
+
+// ListSessions lists the active sessions (GET /dsl-debugging/sessions).
+func ListSessions(ctx context.Context) (any, error) {
+       var out any
+       err := client.GetJSON(ctx, "/dsl-debugging/sessions", nil, &out)
+       return out, err
+}
+
+// Status returns the module health snapshot (GET /dsl-debugging/status).
+func Status(ctx context.Context) (any, error) {
+       var out any
+       err := client.GetJSON(ctx, "/dsl-debugging/status", nil, &out)
+       return out, err
+}
diff --git a/pkg/admin/inspect/inspect.go b/pkg/admin/inspect/inspect.go
new file mode 100644
index 0000000..6ff9807
--- /dev/null
+++ b/pkg/admin/inspect/inspect.go
@@ -0,0 +1,123 @@
+// Licensed to 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. Apache Software Foundation (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 inspect wraps the OAP admin-server `inspect` feature module: browse 
the
+// metric catalog and enumerate the entities currently holding values for a 
metric.
+// The entity rows carry an MQE-ready payload that pastes into a follow-up
+// execExpression query on the public GraphQL surface.
+package inspect
+
+import (
+       "context"
+       "net/url"
+       "strconv"
+
+       "github.com/apache/skywalking-cli/pkg/admin/client"
+)
+
+// MaxLimit is the server-side hard cap on /inspect/entities rows.
+const MaxLimit = 300
+
+// Metric is a single entry of the metric catalog.
+type Metric struct {
+       Name            string   `json:"name"`
+       Type            string   `json:"type"`
+       Catalog         string   `json:"catalog"`
+       ScopeID         int      `json:"scopeId"`
+       Scope           string   `json:"scope"`
+       ValueColumnName string   `json:"valueColumnName"`
+       Downsamplings   []string `json:"downsamplings"`
+}
+
+// Metrics is the response of GET /inspect/metrics.
+type Metrics struct {
+       Metrics []Metric `json:"metrics"`
+}
+
+// MetricsOptions holds the optional filters of GET /inspect/metrics.
+type MetricsOptions struct {
+       Regex        string
+       Types        []string
+       Catalogs     []string
+       MQEQueryable bool
+}
+
+// Entity is one row of GET /inspect/entities: the decoded entity plus an 
MQE-ready
+// payload to feed back into execExpression.
+type Entity struct {
+       EntityID  string `json:"entityId"`
+       Decoded   any    `json:"decoded"`
+       Layer     string `json:"layer"`
+       MqeEntity any    `json:"mqeEntity"`
+}
+
+// Entities is the response of GET /inspect/entities.
+type Entities struct {
+       Metric string   `json:"metric"`
+       Scope  string   `json:"scope"`
+       Step   string   `json:"step"`
+       Start  string   `json:"start"`
+       End    string   `json:"end"`
+       Rows   []Entity `json:"rows"`
+}
+
+// EntitiesOptions holds the parameters of GET /inspect/entities.
+type EntitiesOptions struct {
+       Metric string
+       Start  string
+       End    string
+       Step   string
+       Limit  int
+}
+
+// ListMetrics lists the registered metric catalog (GET /inspect/metrics).
+func ListMetrics(ctx context.Context, opts MetricsOptions) (*Metrics, error) {
+       query := url.Values{}
+       if opts.Regex != "" {
+               query.Set("regex", opts.Regex)
+       }
+       for _, t := range opts.Types {
+               query.Add("type", t)
+       }
+       for _, c := range opts.Catalogs {
+               query.Add("catalog", c)
+       }
+       if opts.MQEQueryable {
+               query.Set("mqeQueryable", "true")
+       }
+
+       var out Metrics
+       err := client.GetJSON(ctx, "/inspect/metrics", query, &out)
+       return &out, err
+}
+
+// ListEntities enumerates the entities holding values for a metric over a 
time range
+// (GET /inspect/entities). Only REGULAR_VALUE / LABELED_VALUE metrics are 
accepted.
+func ListEntities(ctx context.Context, opts EntitiesOptions) (*Entities, 
error) {
+       query := url.Values{}
+       query.Set("metric", opts.Metric)
+       query.Set("start", opts.Start)
+       query.Set("end", opts.End)
+       query.Set("step", opts.Step)
+       if opts.Limit > 0 {
+               query.Set("limit", strconv.Itoa(opts.Limit))
+       }
+
+       var out Entities
+       err := client.GetJSON(ctx, "/inspect/entities", query, &out)
+       return &out, err
+}
diff --git a/pkg/admin/oal/oal.go b/pkg/admin/oal/oal.go
new file mode 100644
index 0000000..ad9c3e9
--- /dev/null
+++ b/pkg/admin/oal/oal.go
@@ -0,0 +1,67 @@
+// Licensed to 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. Apache Software Foundation (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 oal wraps the read-only OAL listing endpoints (`/runtime/oal/*`) 
hosted by
+// the OAP admin-server `dsl-debugging` feature module: the rule-picker source 
for the
+// OAL live debugger. OAL hot-update is upstream-deferred; these endpoints are 
read-only.
+package oal
+
+import (
+       "context"
+       "net/http"
+       "net/url"
+
+       "github.com/apache/skywalking-cli/pkg/admin/client"
+)
+
+// ListFiles lists the loaded .oal file names (GET /runtime/oal/files).
+func ListFiles(ctx context.Context) (any, error) {
+       var out any
+       err := client.GetJSON(ctx, "/runtime/oal/files", nil, &out)
+       return out, err
+}
+
+// GetFile returns the raw .oal text of a single file (GET 
/runtime/oal/files/{name}).
+func GetFile(ctx context.Context, name string) (string, error) {
+       resp, err := client.Do(ctx, &client.Request{
+               Method: http.MethodGet,
+               Path:   "/runtime/oal/files/" + url.PathEscape(name),
+               Accept: "application/json, text/plain",
+       })
+       if err != nil {
+               return "", err
+       }
+       if !resp.IsSuccess() {
+               return "", client.ParseError(resp)
+       }
+       return string(resp.Body), nil
+}
+
+// ListSources lists the per-dispatcher OAL source catalog (GET 
/runtime/oal/rules).
+func ListSources(ctx context.Context) (any, error) {
+       var out any
+       err := client.GetJSON(ctx, "/runtime/oal/rules", nil, &out)
+       return out, err
+}
+
+// GetSource returns one source's per-metric holder status
+// (GET /runtime/oal/rules/{source}).
+func GetSource(ctx context.Context, source string) (any, error) {
+       var out any
+       err := client.GetJSON(ctx, 
"/runtime/oal/rules/"+url.PathEscape(source), nil, &out)
+       return out, err
+}
diff --git a/pkg/admin/preflight/preflight.go b/pkg/admin/preflight/preflight.go
new file mode 100644
index 0000000..963d7db
--- /dev/null
+++ b/pkg/admin/preflight/preflight.go
@@ -0,0 +1,121 @@
+// Licensed to 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. Apache Software Foundation (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 preflight performs admin-server feature detection by reading the 
effective
+// configuration dump, mirroring how Horizon UI degrades gracefully when an 
admin
+// feature module is disabled or the admin host is unreachable.
+package preflight
+
+import (
+       "context"
+       "errors"
+       "fmt"
+       "net/http"
+       "strings"
+
+       "github.com/apache/skywalking-cli/pkg/admin/client"
+)
+
+// Known admin feature-module selector keys (the first dotted segment of a 
config-dump
+// key) and the environment variables that enable them.
+const (
+       ModuleAdminServer = "admin-server"
+       ModuleStatus      = "status"
+       ModuleInspect     = "inspect"
+       ModuleUIManage    = "ui-management"
+       ModuleDSLDebug    = "dsl-debugging"
+       ModuleRuntimeRule = "receiver-runtime-rule"
+)
+
+// Module describes one admin feature module and how to enable it.
+type Module struct {
+       Name    string `json:"name"`
+       EnvVar  string `json:"envVar"`
+       Enabled bool   `json:"enabled"`
+}
+
+// Result is the outcome of a preflight check against the admin host.
+type Result struct {
+       AdminURL       string   `json:"adminURL"`
+       AdminReachable bool     `json:"adminReachable"`
+       Modules        []Module `json:"modules"`
+
+       enabled map[string]bool
+}
+
+// IsEnabled reports whether a feature module (by selector key) is enabled.
+func (r *Result) IsEnabled(module string) bool { return r.enabled[module] }
+
+var knownModules = []struct{ name, env string }{
+       {ModuleAdminServer, "SW_ADMIN_SERVER"},
+       {ModuleStatus, "SW_STATUS"},
+       {ModuleInspect, "SW_INSPECT"},
+       {ModuleUIManage, "SW_UI_MANAGEMENT"},
+       {ModuleDSLDebug, "SW_DSL_DEBUGGING"},
+       {ModuleRuntimeRule, "SW_RECEIVER_RUNTIME_RULE"},
+}
+
+// Run reads GET /debugging/config/dump from the admin host and reports which 
feature
+// modules are enabled. A module is considered enabled when any dotted config 
key
+// starts with `<module>.`. When the dump cannot be fetched, AdminReachable is 
false,
+// every module reports disabled, and the transport error is returned 
alongside the
+// (still useful) Result so callers can surface the admin URL.
+func Run(ctx context.Context) (*Result, error) {
+       r := &Result{AdminURL: client.BaseURL(ctx), enabled: map[string]bool{}}
+
+       var dump map[string]any
+       err := client.GetJSON(ctx, "/debugging/config/dump", nil, &dump)
+       if err == nil {
+               r.AdminReachable = true
+               for k := range dump {
+                       prefix := k
+                       if i := strings.IndexByte(k, '.'); i >= 0 {
+                               prefix = k[:i]
+                       }
+                       r.enabled[prefix] = true
+               }
+       }
+       for _, m := range knownModules {
+               r.Modules = append(r.Modules, Module{Name: m.name, EnvVar: 
m.env, Enabled: r.enabled[m.name]})
+       }
+       return r, err
+}
+
+// Explain enriches an admin-call error with operator-actionable context. A 
transport
+// failure is reported as an unreachable admin host. A 404 with no 
recognizable JSON
+// error envelope is reported as a likely-disabled module (the route is not 
registered),
+// whereas a 404 that carries an error body (e.g. {"error":"not_found"}) is a 
real
+// resource miss from an enabled module and is returned unchanged — as are all 
other API
+// errors (400/409/421/...), which are already specific.
+func Explain(ctx context.Context, err error, module, envVar string) error {
+       if err == nil {
+               return nil
+       }
+       adminURL := client.BaseURL(ctx)
+       var apiErr *client.APIError
+       if errors.As(err, &apiErr) {
+               if apiErr.StatusCode == http.StatusNotFound && apiErr.Message 
== "" && apiErr.Status == "" {
+                       return fmt.Errorf("the `%s` admin feature module 
appears disabled on OAP "+
+                               "(HTTP 404 with no error body at %s); enable it 
with %s=default. original error: %w",
+                               module, adminURL, envVar, err)
+               }
+               return err
+       }
+       return fmt.Errorf("could not reach the OAP admin-server at %s; "+
+               "verify --admin-url and that admin-server is enabled 
(SW_ADMIN_SERVER=default). "+
+               "original error: %w", adminURL, err)
+}
diff --git a/pkg/admin/runtimerule/runtimerule.go 
b/pkg/admin/runtimerule/runtimerule.go
new file mode 100644
index 0000000..c98b236
--- /dev/null
+++ b/pkg/admin/runtimerule/runtimerule.go
@@ -0,0 +1,211 @@
+// Licensed to 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. Apache Software Foundation (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 runtimerule wraps the OAP admin-server `receiver-runtime-rule` 
feature
+// module: hot-update of MAL / LAL rule files without restarting OAP, plus 
read access
+// to the live and bundled rule state.
+package runtimerule
+
+import (
+       "context"
+       "encoding/json"
+       "net/http"
+       "net/url"
+       "strconv"
+       "strings"
+
+       "github.com/apache/skywalking-cli/pkg/admin/client"
+)
+
+// Catalogs accepted by the runtime-rule endpoints.
+var Catalogs = []string{"otel-rules", "log-mal-rules", "telegraf-rules", "lal"}
+
+// ApplyResult is the JSON envelope returned by addOrUpdate / inactivate / 
delete.
+type ApplyResult struct {
+       ApplyStatus string `json:"applyStatus"`
+       Catalog     string `json:"catalog"`
+       Name        string `json:"name"`
+       Message     string `json:"message"`
+}
+
+// Rule is the result of a single-rule GET. Metadata comes from X-Sw-* response
+// headers; Content is the raw YAML body. NotModified is set when a 
conditional GET
+// (If-None-Match) returns 304.
+type Rule struct {
+       Status      string `json:"status"`
+       Source      string `json:"source"`
+       ContentHash string `json:"contentHash"`
+       UpdateTime  int64  `json:"updateTime"`
+       ETag        string `json:"etag"`
+       Content     string `json:"content,omitempty"`
+       NotModified bool   `json:"notModified,omitempty"`
+}
+
+// List returns the live rule state per node (GET 
/runtime/rule/list[?catalog=]).
+func List(ctx context.Context, catalog string) (any, error) {
+       var query url.Values
+       if catalog != "" {
+               query = url.Values{"catalog": []string{catalog}}
+       }
+       var out any
+       err := client.GetJSON(ctx, "/runtime/rule/list", query, &out)
+       return out, err
+}
+
+// ListBundled returns the static (bundled) rule twins for a catalog
+// (GET /runtime/rule/bundled?catalog=&withContent=).
+func ListBundled(ctx context.Context, catalog string, withContent bool) (any, 
error) {
+       query := url.Values{
+               "catalog":     []string{catalog},
+               "withContent": []string{strconv.FormatBool(withContent)},
+       }
+       var out any
+       err := client.GetJSON(ctx, "/runtime/rule/bundled", query, &out)
+       return out, err
+}
+
+// Get fetches a single rule (GET /runtime/rule?catalog=&name=[&source=]). It 
negotiates
+// raw YAML and reads metadata from the X-Sw-* headers. When ifNoneMatch is 
set and the
+// server replies 304, the returned Rule has NotModified=true and an empty 
Content.
+func Get(ctx context.Context, catalog, name, source, ifNoneMatch string) 
(*Rule, error) {
+       query := url.Values{"catalog": []string{catalog}, "name": 
[]string{name}}
+       if source != "" {
+               query.Set("source", source)
+       }
+       headers := map[string]string{}
+       if ifNoneMatch != "" {
+               headers["If-None-Match"] = ifNoneMatch
+       }
+
+       resp, err := client.Do(ctx, &client.Request{
+               Method:  http.MethodGet,
+               Path:    "/runtime/rule",
+               Query:   query,
+               Accept:  "application/x-yaml",
+               Headers: headers,
+       })
+       if err != nil {
+               return nil, err
+       }
+       if resp.StatusCode == http.StatusNotModified {
+               return &Rule{
+                       NotModified: true,
+                       ETag:        resp.Header.Get("ETag"),
+                       ContentHash: resp.Header.Get("X-Sw-Content-Hash"),
+                       Status:      headerOr(resp, "X-Sw-Status", "n/a"),
+               }, nil
+       }
+       if !resp.IsSuccess() {
+               return nil, client.ParseError(resp)
+       }
+       updateTime, _ := strconv.ParseInt(resp.Header.Get("X-Sw-Update-Time"), 
10, 64)
+       return &Rule{
+               Status:      headerOr(resp, "X-Sw-Status", "n/a"),
+               Source:      headerOr(resp, "X-Sw-Source", "runtime"),
+               ContentHash: resp.Header.Get("X-Sw-Content-Hash"),
+               UpdateTime:  updateTime,
+               ETag:        resp.Header.Get("ETag"),
+               Content:     string(resp.Body),
+       }, nil
+}
+
+// AddOrUpdate pushes a new or updated rule as raw YAML
+// (POST 
/runtime/rule/addOrUpdate?catalog=&name=[&allowStorageChange=][&force=]).
+func AddOrUpdate(ctx context.Context, catalog, name, body string, 
allowStorageChange, force bool) (*ApplyResult, error) {
+       query := url.Values{"catalog": []string{catalog}, "name": 
[]string{name}}
+       if allowStorageChange {
+               query.Set("allowStorageChange", "true")
+       }
+       if force {
+               query.Set("force", "true")
+       }
+       resp, err := client.Do(ctx, &client.Request{
+               Method:      http.MethodPost,
+               Path:        "/runtime/rule/addOrUpdate",
+               Query:       query,
+               Body:        strings.NewReader(body),
+               ContentType: "text/plain",
+       })
+       if err != nil {
+               return nil, err
+       }
+       return applyResult(resp)
+}
+
+// Inactivate turns a rule off (POST /runtime/rule/inactivate?catalog=&name=).
+func Inactivate(ctx context.Context, catalog, name string) (*ApplyResult, 
error) {
+       query := url.Values{"catalog": []string{catalog}, "name": 
[]string{name}}
+       resp, err := client.Do(ctx, &client.Request{Method: http.MethodPost, 
Path: "/runtime/rule/inactivate", Query: query})
+       if err != nil {
+               return nil, err
+       }
+       return applyResult(resp)
+}
+
+// Delete removes a rule (POST 
/runtime/rule/delete?catalog=&name=[&mode=revertToBundled]).
+// The server enforces a two-step gate: deleting an active rule returns a 409
+// requires_inactivate_first.
+func Delete(ctx context.Context, catalog, name, mode string) (*ApplyResult, 
error) {
+       query := url.Values{"catalog": []string{catalog}, "name": 
[]string{name}}
+       if mode != "" {
+               query.Set("mode", mode)
+       }
+       resp, err := client.Do(ctx, &client.Request{Method: http.MethodPost, 
Path: "/runtime/rule/delete", Query: query})
+       if err != nil {
+               return nil, err
+       }
+       return applyResult(resp)
+}
+
+// Dump returns a tar.gz snapshot of all rules, or one catalog's
+// (GET /runtime/rule/dump[/{catalog}]).
+func Dump(ctx context.Context, catalog string) ([]byte, error) {
+       path := "/runtime/rule/dump"
+       if catalog != "" {
+               path += "/" + url.PathEscape(catalog)
+       }
+       resp, err := client.Do(ctx, &client.Request{Method: http.MethodGet, 
Path: path, Accept: "application/gzip"})
+       if err != nil {
+               return nil, err
+       }
+       if !resp.IsSuccess() {
+               return nil, client.ParseError(resp)
+       }
+       return resp.Body, nil
+}
+
+func applyResult(resp *client.Response) (*ApplyResult, error) {
+       if !resp.IsSuccess() {
+               // Non-2xx still carries the ApplyResult envelope; ParseError 
lifts its
+               // applyStatus/message so callers can switch on the semantic 
code.
+               return nil, client.ParseError(resp)
+       }
+       var out ApplyResult
+       if len(resp.Body) > 0 {
+               if err := json.Unmarshal(resp.Body, &out); err != nil {
+                       return nil, err
+               }
+       }
+       return &out, nil
+}
+
+func headerOr(resp *client.Response, key, fallback string) string {
+       if v := resp.Header.Get(key); v != "" {
+               return v
+       }
+       return fallback
+}
diff --git a/pkg/admin/status/status.go b/pkg/admin/status/status.go
new file mode 100644
index 0000000..2b7d661
--- /dev/null
+++ b/pkg/admin/status/status.go
@@ -0,0 +1,115 @@
+// Licensed to 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. Apache Software Foundation (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 status wraps the OAP admin-server `status` feature module: cluster
+// membership, effective configuration / TTL, and alarm runtime status. These
+// endpoints were served on the public REST port before OAP 11.0.0 and now 
live on
+// the admin host (default 17128); only /status/config/ttl is also mirrored on 
12800.
+package status
+
+import (
+       "context"
+       "net/url"
+
+       "github.com/apache/skywalking-cli/pkg/admin/client"
+)
+
+// ClusterNode is a single OAP node as seen by the cluster coordinator.
+type ClusterNode struct {
+       Host string `json:"host"`
+       Port int    `json:"port"`
+       Self bool   `json:"self"`
+}
+
+// ClusterNodes is the response of GET /status/cluster/nodes.
+type ClusterNodes struct {
+       Nodes []ClusterNode `json:"nodes"`
+}
+
+// ClusterAlarmStatus is the per-node envelope returned by every 
/status/alarm/* call.
+// OAP fans the query out across cluster members; non-evaluating nodes return 
a stub
+// status and errorMsg is omitted on success.
+type ClusterAlarmStatus struct {
+       OapInstances []OapInstanceStatus `json:"oapInstances"`
+}
+
+// OapInstanceStatus is one cluster member's slice of an alarm status response.
+type OapInstanceStatus struct {
+       Address  string `json:"address"`
+       ErrorMsg string `json:"errorMsg,omitempty"`
+       Status   any    `json:"status"`
+}
+
+// ClusterNodesQuery returns the OAP cluster peer list (GET 
/status/cluster/nodes).
+// The self flag is normalized from either `self` or `isSelf` on the wire.
+func ClusterNodesQuery(ctx context.Context) (*ClusterNodes, error) {
+       var raw struct {
+               Nodes []struct {
+                       Host   string `json:"host"`
+                       Port   int    `json:"port"`
+                       Self   bool   `json:"self"`
+                       IsSelf bool   `json:"isSelf"`
+               } `json:"nodes"`
+       }
+       if err := client.GetJSON(ctx, "/status/cluster/nodes", nil, &raw); err 
!= nil {
+               return nil, err
+       }
+       out := &ClusterNodes{Nodes: make([]ClusterNode, 0, len(raw.Nodes))}
+       for _, n := range raw.Nodes {
+               out.Nodes = append(out.Nodes, ClusterNode{Host: n.Host, Port: 
n.Port, Self: n.Self || n.IsSelf})
+       }
+       return out, nil
+}
+
+// ConfigTTL returns the effective TTL configuration (GET /status/config/ttl).
+func ConfigTTL(ctx context.Context) (any, error) {
+       var out any
+       err := client.GetJSON(ctx, "/status/config/ttl", nil, &out)
+       return out, err
+}
+
+// ConfigDump returns the effective, secrets-redacted configuration as a flat 
map of
+// `<module>.<provider>.<property>` keys (GET /debugging/config/dump).
+func ConfigDump(ctx context.Context) (any, error) {
+       var out any
+       err := client.GetJSON(ctx, "/debugging/config/dump", nil, &out)
+       return out, err
+}
+
+// AlarmRules returns the loaded alarm rules per OAP node (GET 
/status/alarm/rules).
+func AlarmRules(ctx context.Context) (*ClusterAlarmStatus, error) {
+       var out ClusterAlarmStatus
+       err := client.GetJSON(ctx, "/status/alarm/rules", nil, &out)
+       return &out, err
+}
+
+// AlarmRule returns the definition + running state of a single alarm rule
+// (GET /status/alarm/{ruleId}).
+func AlarmRule(ctx context.Context, ruleID string) (*ClusterAlarmStatus, 
error) {
+       var out ClusterAlarmStatus
+       err := client.GetJSON(ctx, "/status/alarm/"+url.PathEscape(ruleID), 
nil, &out)
+       return &out, err
+}
+
+// AlarmRuleEntity returns the per-entity alarm window/evaluation state for a 
rule
+// (GET /status/alarm/{ruleId}/{entityName}).
+func AlarmRuleEntity(ctx context.Context, ruleID, entityName string) 
(*ClusterAlarmStatus, error) {
+       var out ClusterAlarmStatus
+       path := "/status/alarm/" + url.PathEscape(ruleID) + "/" + 
url.PathEscape(entityName)
+       err := client.GetJSON(ctx, path, nil, &out)
+       return &out, err
+}
diff --git a/pkg/admin/uitemplate/uitemplate.go 
b/pkg/admin/uitemplate/uitemplate.go
new file mode 100644
index 0000000..aac98cd
--- /dev/null
+++ b/pkg/admin/uitemplate/uitemplate.go
@@ -0,0 +1,92 @@
+// Licensed to 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. Apache Software Foundation (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 uitemplate wraps the OAP admin-server `ui-management` feature 
module: REST
+// CRUD over dashboard templates. It is the replacement for the GraphQL
+// UIConfigurationManagement template mutations retired in SkyWalking 11.0.0. 
There is
+// no DELETE; templates are soft-disabled.
+package uitemplate
+
+import (
+       "context"
+       "net/http"
+       "net/url"
+
+       "github.com/apache/skywalking-cli/pkg/admin/client"
+)
+
+const basePath = "/ui-management/templates"
+
+// Template is a dashboard template. Configuration is an opaque JSON-encoded 
string.
+type Template struct {
+       ID            string `json:"id"`
+       Configuration string `json:"configuration"`
+       Disabled      bool   `json:"disabled"`
+}
+
+// ChangeStatus is the acknowledgement of a create / update / disable write.
+type ChangeStatus struct {
+       ID      string `json:"id"`
+       Status  bool   `json:"status"`
+       Message string `json:"message"`
+}
+
+// List returns all templates (GET /ui-management/templates). When 
includingDisabled
+// is true, soft-disabled templates are included as well.
+func List(ctx context.Context, includingDisabled bool) ([]Template, error) {
+       var query url.Values
+       if includingDisabled {
+               query = url.Values{"includingDisabled": []string{"true"}}
+       }
+       var out []Template
+       err := client.GetJSON(ctx, basePath, query, &out)
+       return out, err
+}
+
+// Get returns a single template by ID (GET /ui-management/templates/{id}).
+func Get(ctx context.Context, id string) (*Template, error) {
+       var out Template
+       err := client.GetJSON(ctx, basePath+"/"+url.PathEscape(id), nil, &out)
+       return &out, err
+}
+
+// Create adds a new template (POST /ui-management/templates). configuration 
is the
+// JSON-encoded template body. Since OAP 11.0.0 (skywalking#13884) the id is 
required
+// in the request body, so callers pass a client-generated id.
+func Create(ctx context.Context, id, configuration string) (*ChangeStatus, 
error) {
+       var out ChangeStatus
+       body := map[string]string{"id": id, "configuration": configuration}
+       err := client.SendJSON(ctx, http.MethodPost, basePath, nil, body, &out)
+       return &out, err
+}
+
+// Update replaces an existing template (PUT /ui-management/templates).
+func Update(ctx context.Context, id, configuration string) (*ChangeStatus, 
error) {
+       var out ChangeStatus
+       body := map[string]string{"id": id, "configuration": configuration}
+       err := client.SendJSON(ctx, http.MethodPut, basePath, nil, body, &out)
+       return &out, err
+}
+
+// Disable soft-disables a template (POST 
/ui-management/templates/{id}/disable). It is
+// idempotent. The template row is preserved; only its disabled flag flips.
+func Disable(ctx context.Context, id string) (*ChangeStatus, error) {
+       out := ChangeStatus{ID: id, Status: true}
+       path := basePath + "/" + url.PathEscape(id) + "/disable"
+       err := client.SendJSON(ctx, http.MethodPost, path, nil, nil, &out)
+       return &out, err
+}
diff --git a/pkg/contextkey/contextkey.go b/pkg/contextkey/contextkey.go
index d687506..1071d03 100644
--- a/pkg/contextkey/contextkey.go
+++ b/pkg/contextkey/contextkey.go
@@ -19,6 +19,7 @@ package contextkey
 
 type (
        BaseURL       struct{}
+       AdminURL      struct{}
        Insecure      struct{}
        Username      struct{}
        Password      struct{}
diff --git a/pkg/graphql/alarm/alarm.go b/pkg/graphql/alarm/alarm.go
index 1e3a93a..bcafc76 100644
--- a/pkg/graphql/alarm/alarm.go
+++ b/pkg/graphql/alarm/alarm.go
@@ -29,24 +29,47 @@ import (
 )
 
 type ListAlarmCondition struct {
-       Duration *api.Duration
-       Keyword  string
-       Scope    api.Scope
-       Tags     []*api.AlarmTag
-       Paging   *api.Pagination
+       Duration  *api.Duration
+       Keyword   string
+       Tags      []*api.AlarmTag
+       Paging    *api.Pagination
+       Layer     string
+       RuleNames []string
+       Entities  []*api.Entity
+}
+
+// alarmQueryCondition mirrors the GraphQL `AlarmQueryCondition` input of 
`queryAlarms`.
+// It is JSON-encoded as the `condition` variable, so the field tags must 
match the
+// schema field names exactly.
+type alarmQueryCondition struct {
+       Duration  *api.Duration   `json:"duration"`
+       Paging    *api.Pagination `json:"paging"`
+       Entities  []*api.Entity   `json:"entities,omitempty"`
+       Layer     *string         `json:"layer,omitempty"`
+       RuleNames []string        `json:"ruleNames,omitempty"`
+       Keyword   *string         `json:"keyword,omitempty"`
+       Tags      []*api.AlarmTag `json:"tags,omitempty"`
 }
 
 func Alarms(ctx context.Context, condition *ListAlarmCondition) (api.Alarms, 
error) {
        var response map[string]api.Alarms
 
-       request := 
graphql.NewRequest(assets.Read("graphqls/alarm/alarms.graphql"))
-       request.Var("paging", condition.Paging)
-       request.Var("tags", condition.Tags)
-       request.Var("duration", condition.Duration)
-       request.Var("keyword", condition.Keyword)
-       if condition.Scope != "" {
-               request.Var("scope", condition.Scope)
+       queryCondition := alarmQueryCondition{
+               Duration:  condition.Duration,
+               Paging:    condition.Paging,
+               Entities:  condition.Entities,
+               RuleNames: condition.RuleNames,
+               Tags:      condition.Tags,
+       }
+       if condition.Keyword != "" {
+               queryCondition.Keyword = &condition.Keyword
        }
+       if condition.Layer != "" {
+               queryCondition.Layer = &condition.Layer
+       }
+
+       request := 
graphql.NewRequest(assets.Read("graphqls/alarm/alarms.graphql"))
+       request.Var("condition", queryCondition)
 
        err := client.ExecuteQuery(ctx, request, &response)
 
diff --git a/pkg/graphql/client/client.go b/pkg/graphql/client/client.go
index a2ad60a..67f4d9b 100644
--- a/pkg/graphql/client/client.go
+++ b/pkg/graphql/client/client.go
@@ -19,29 +19,23 @@ package client
 
 import (
        "context"
-       "crypto/tls"
-       "encoding/base64"
-       "net/http"
 
        "github.com/machinebox/graphql"
 
        "github.com/apache/skywalking-cli/pkg/contextkey"
        "github.com/apache/skywalking-cli/pkg/logger"
+       "github.com/apache/skywalking-cli/pkg/transport"
 )
 
 // newClient creates a new GraphQL client with configuration from context.
 func newClient(ctx context.Context) *graphql.Client {
        options := []graphql.ClientOption{}
 
-       insecure := ctx.Value(contextkey.Insecure{}).(bool)
-       if insecure {
-               customTransport := 
http.DefaultTransport.(*http.Transport).Clone()
-               customTransport.TLSClientConfig = 
&tls.Config{InsecureSkipVerify: insecure} // #nosec G402
-               httpClient := &http.Client{Transport: customTransport}
-               options = append(options, graphql.WithHTTPClient(httpClient))
+       if transport.Insecure(ctx) {
+               options = append(options, 
graphql.WithHTTPClient(transport.HTTPClient(ctx)))
        }
 
-       baseURL := getValue(ctx, contextkey.BaseURL{}, 
"http://127.0.0.1:12800/graphql";)
+       baseURL := transport.GetValue(ctx, contextkey.BaseURL{}, 
"http://127.0.0.1:12800/graphql";)
        client := graphql.NewClient(baseURL, options...)
        client.Log = func(msg string) {
                logger.Log.Debugln(msg)
@@ -51,14 +45,7 @@ func newClient(ctx context.Context) *graphql.Client {
 
 // ExecuteQuery executes the `request` and parse to the `response`, returning 
`error` if there is any.
 func ExecuteQuery(ctx context.Context, request *graphql.Request, response any) 
error {
-       username := getValue(ctx, contextkey.Username{}, "")
-       password := getValue(ctx, contextkey.Password{}, "")
-       authorization := getValue(ctx, contextkey.Authorization{}, "")
-
-       if authorization == "" && username != "" && password != "" {
-               authorization = "Basic " + 
base64.StdEncoding.EncodeToString([]byte(username+":"+password))
-       }
-       if authorization != "" {
+       if authorization := transport.AuthHeader(ctx); authorization != "" {
                request.Header.Set("Authorization", authorization)
        }
 
@@ -66,12 +53,3 @@ func ExecuteQuery(ctx context.Context, request 
*graphql.Request, response any) e
        err := client.Run(ctx, request, response)
        return err
 }
-
-// getValue safely extracts a value from the context.
-func getValue[T any](ctx context.Context, key any, defaultValue T) T {
-       val := ctx.Value(key)
-       if v, ok := val.(T); ok {
-               return v
-       }
-       return defaultValue
-}
diff --git a/pkg/transport/transport.go b/pkg/transport/transport.go
new file mode 100644
index 0000000..ae8066a
--- /dev/null
+++ b/pkg/transport/transport.go
@@ -0,0 +1,71 @@
+// Licensed to 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. Apache Software Foundation (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 transport holds the connection bits shared by the GraphQL client and
+// the admin REST client: TLS handling, basic-auth resolution, and context 
helpers.
+package transport
+
+import (
+       "context"
+       "crypto/tls"
+       "encoding/base64"
+       "net/http"
+
+       "github.com/apache/skywalking-cli/pkg/contextkey"
+)
+
+// Insecure reports whether TLS certificate verification should be skipped,
+// according to the `--insecure` global option stored in the context.
+func Insecure(ctx context.Context) bool {
+       return GetValue(ctx, contextkey.Insecure{}, false)
+}
+
+// HTTPClient builds an *http.Client honoring the `--insecure` option from 
context.
+// When insecure is not set it returns a plain client, equivalent to 
http.DefaultClient.
+func HTTPClient(ctx context.Context) *http.Client {
+       if Insecure(ctx) {
+               customTransport := 
http.DefaultTransport.(*http.Transport).Clone()
+               customTransport.TLSClientConfig = 
&tls.Config{InsecureSkipVerify: true} // #nosec G402
+               return &http.Client{Transport: customTransport}
+       }
+       return &http.Client{}
+}
+
+// AuthHeader resolves the value of the `Authorization` header from the global
+// `--authorization`, `--username` and `--password` options stored in the 
context,
+// returning an empty string when no credentials are configured. A raw
+// `--authorization` takes precedence over `--username`/`--password`.
+func AuthHeader(ctx context.Context) string {
+       username := GetValue(ctx, contextkey.Username{}, "")
+       password := GetValue(ctx, contextkey.Password{}, "")
+       authorization := GetValue(ctx, contextkey.Authorization{}, "")
+
+       if authorization == "" && username != "" && password != "" {
+               authorization = "Basic " + 
base64.StdEncoding.EncodeToString([]byte(username+":"+password))
+       }
+       return authorization
+}
+
+// GetValue safely extracts a typed value from the context, falling back to
+// defaultValue when the key is absent or holds a different type.
+func GetValue[T any](ctx context.Context, key any, defaultValue T) T {
+       val := ctx.Value(key)
+       if v, ok := val.(T); ok {
+               return v
+       }
+       return defaultValue
+}
diff --git a/test/base/docker-compose.yml b/test/base/docker-compose.yml
index ba5a944..aa84832 100644
--- a/test/base/docker-compose.yml
+++ b/test/base/docker-compose.yml
@@ -16,17 +16,20 @@
 version: "2.1"
 
 services:
-  es:
-    image: docker.elastic.co/elasticsearch/elasticsearch:7.10.1
+  # BanyanDB is SkyWalking's native, lightweight storage — far cheaper to spin 
up in
+  # CI than Elasticsearch. The image tag is pinned to the same BanyanDB build 
the
+  # upstream skywalking master e2e validates against (test/e2e-v2/script/env:
+  # SW_BANYANDB_COMMIT); keep it in sync when bumping OAP_TAG.
+  banyandb:
+    image: 
ghcr.io/apache/skywalking-banyandb:84b919efca3fee3d51df9e97a734a9f10ae6f1d2
     expose:
-      - 9200
+      - 17912
+      - 17913
     networks:
       - test
-    environment:
-      - discovery.type=single-node
-      - xpack.security.enabled=false
+    command: standalone --measure-metadata-cache-wait-duration 1m 
--stream-metadata-cache-wait-duration 1m
     healthcheck:
-      test: ["CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/9200"]
+      test: ["CMD", "sh", "-c", "nc -nz 127.0.0.1 17912"]
       interval: 5s
       timeout: 60s
       retries: 120
@@ -39,8 +42,8 @@ services:
     networks:
       - test
     environment:
-      - SW_STORAGE=elasticsearch
-      - SW_STORAGE_ES_CLUSTER_NODES=es:9200
+      - SW_STORAGE=banyandb
+      - SW_STORAGE_BANYANDB_TARGETS=banyandb:17912
       - SW_HEALTH_CHECKER=default
       - SW_TELEMETRY=prometheus
     healthcheck:
@@ -48,6 +51,7 @@ services:
       interval: 5s
       timeout: 60s
       retries: 120
+      start_period: 30s
 
   provider:
     image: apache/skywalking-python:0.8.0-grpc-py3.9
diff --git a/test/cases/basic/docker-compose.yml 
b/test/cases/admin/docker-compose.yml
similarity index 68%
copy from test/cases/basic/docker-compose.yml
copy to test/cases/admin/docker-compose.yml
index 30fe99f..1fe22d4 100644
--- a/test/cases/basic/docker-compose.yml
+++ b/test/cases/admin/docker-compose.yml
@@ -13,41 +13,28 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# Minimal topology for the admin-server REST commands: just BanyanDB + OAP. 
The admin
+# feature modules (status / inspect / ui-management / dsl-debugging / 
runtime-rule)
+# are enabled by default in OAP 11.0.0+, so no extra environment is required — 
only
+# the admin REST port (17128) needs to be published alongside the GraphQL port 
(12800).
+# Requires an OAP build that includes the admin host (OAP_TAG >= 11.0.0).
+
 version: "2.1"
 
 services:
-  es:
+  banyandb:
     extends:
       file: ../../base/docker-compose.yml
-      service: es
-    ports:
-      - 9200
+      service: banyandb
   oap:
     extends:
       file: ../../base/docker-compose.yml
       service: oap
     ports:
       - 12800
+      - 17128
     depends_on:
-      es:
-        condition: service_healthy
-
-  provider:
-    extends:
-      file: ../../base/docker-compose.yml
-      service: provider
-    depends_on:
-      oap:
-        condition: service_healthy
-
-  consumer:
-    extends:
-      file: ../../base/docker-compose.yml
-      service: consumer
-    ports:
-      - 9090
-    depends_on:
-      provider:
+      banyandb:
         condition: service_healthy
 
 networks:
diff --git a/test/cases/basic/expected/traces-list.yml 
b/test/cases/admin/expected/count.yml
similarity index 75%
copy from test/cases/basic/expected/traces-list.yml
copy to test/cases/admin/expected/count.yml
index a51cb7f..56181bf 100644
--- a/test/cases/basic/expected/traces-list.yml
+++ b/test/cases/admin/expected/count.yml
@@ -13,15 +13,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-traces:
-  {{- contains .traces }}
-- segmentid: {{ notEmpty .segmentid }}
-  endpointnames:
-    - /users
-  duration: {{ ge .duration 0 }}
-  start: "{{ notEmpty .start}}"
-  iserror: false
-  traceids:
-    - {{ index .traceids 0 }}
-  {{- end }}
-debuggingtrace: null
\ No newline at end of file
+{{ gt . 0 }}
diff --git a/test/cases/basic/expected/traces-list.yml 
b/test/cases/admin/expected/ok.yml
similarity index 75%
copy from test/cases/basic/expected/traces-list.yml
copy to test/cases/admin/expected/ok.yml
index a51cb7f..e33f055 100644
--- a/test/cases/basic/expected/traces-list.yml
+++ b/test/cases/admin/expected/ok.yml
@@ -13,15 +13,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-traces:
-  {{- contains .traces }}
-- segmentid: {{ notEmpty .segmentid }}
-  endpointnames:
-    - /users
-  duration: {{ ge .duration 0 }}
-  start: "{{ notEmpty .start}}"
-  iserror: false
-  traceids:
-    - {{ index .traceids 0 }}
-  {{- end }}
-debuggingtrace: null
\ No newline at end of file
+ok
diff --git a/test/cases/basic/expected/traces-list.yml 
b/test/cases/admin/expected/true.yml
similarity index 75%
copy from test/cases/basic/expected/traces-list.yml
copy to test/cases/admin/expected/true.yml
index a51cb7f..1549354 100644
--- a/test/cases/basic/expected/traces-list.yml
+++ b/test/cases/admin/expected/true.yml
@@ -13,15 +13,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-traces:
-  {{- contains .traces }}
-- segmentid: {{ notEmpty .segmentid }}
-  endpointnames:
-    - /users
-  duration: {{ ge .duration 0 }}
-  start: "{{ notEmpty .start}}"
-  iserror: false
-  traceids:
-    - {{ index .traceids 0 }}
-  {{- end }}
-debuggingtrace: null
\ No newline at end of file
+true
diff --git a/test/cases/admin/test.yaml b/test/cases/admin/test.yaml
new file mode 100644
index 0000000..f521f09
--- /dev/null
+++ b/test/cases/admin/test.yaml
@@ -0,0 +1,72 @@
+# 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.
+
+# E2E coverage for the OAP admin-server REST commands (`swctl admin ...`) and 
the
+# OAP 11.0.0 regression fixes (alarm -> queryAlarms, menu retirement 
detection).
+# Requires an admin-capable OAP (OAP_TAG >= 11.0.0). The admin host needs no 
traffic,
+# so this suite does not run the provider/consumer demo apps or a trigger.
+
+setup:
+  env: compose
+  file: docker-compose.yml
+  timeout: 20m
+  steps:
+    - name: install yq
+      command: yq > /dev/null 2>&1 || go install 
github.com/mikefarah/yq/v4@latest
+
+verify:
+  retry:
+    count: 20
+    interval: 10s
+  cases:
+    # admin-server reachability + feature detection (GET 
/debugging/config/dump).
+    - query: swctl --display json --admin-url=http://${oap_host}:${oap_17128} 
admin preflight | yq e '.adminReachable' -
+      expected: expected/true.yml
+
+    # status module — cluster membership, effective config, alarm runtime 
status.
+    - query: swctl --display json --admin-url=http://${oap_host}:${oap_17128} 
admin cluster nodes | yq e 'has("nodes")' -
+      expected: expected/true.yml
+    - query: swctl --display json --admin-url=http://${oap_host}:${oap_17128} 
admin config dump | yq e 'length' -
+      expected: expected/count.yml
+    - query: swctl --display json --admin-url=http://${oap_host}:${oap_17128} 
admin alarm rules | yq e 'has("oapInstances")' -
+      expected: expected/true.yml
+
+    # inspect module — metric catalog.
+    - query: swctl --display json --admin-url=http://${oap_host}:${oap_17128} 
admin inspect metrics --mqe-queryable | yq e '.metrics | length' -
+      expected: expected/count.yml
+
+    # ui-management module — dashboard templates (a sequence, possibly empty).
+    - query: swctl --display json --admin-url=http://${oap_host}:${oap_17128} 
admin ui-template list | yq e 'length > -1' -
+      expected: expected/true.yml
+
+    # runtime-rule module — live rule state.
+    - query: swctl --display json --admin-url=http://${oap_host}:${oap_17128} 
admin runtime-rule list | yq e 'has("rules")' -
+      expected: expected/true.yml
+
+    # dsl-debugging module + OAL picker.
+    - query: swctl --display json --admin-url=http://${oap_host}:${oap_17128} 
admin dsl-debug status | yq e 'has("module")' -
+      expected: expected/true.yml
+    - query: swctl --display json --admin-url=http://${oap_host}:${oap_17128} 
admin oal files | yq e '.files | length' -
+      expected: expected/count.yml
+
+    # regression — `alarm list` migrated from getAlarm to queryAlarms (GraphQL 
on 12800).
+    - query: swctl --display json 
--base-url=http://${oap_host}:${oap_12800}/graphql alarm list | yq e 
'has("msgs")' -
+      expected: expected/true.yml
+
+    # regression — `menu get` reports the 11.0.0 retirement gracefully instead 
of a raw error.
+    - query: |
+        out=$(swctl --base-url=http://${oap_host}:${oap_12800}/graphql menu 
get 2>&1 || true)
+        echo "$out" | grep -q "no longer serves the UI menu" && echo ok
+      expected: expected/ok.yml
diff --git a/test/cases/basic/docker-compose.yml 
b/test/cases/basic/docker-compose.yml
index 30fe99f..2622828 100644
--- a/test/cases/basic/docker-compose.yml
+++ b/test/cases/basic/docker-compose.yml
@@ -16,12 +16,10 @@
 version: "2.1"
 
 services:
-  es:
+  banyandb:
     extends:
       file: ../../base/docker-compose.yml
-      service: es
-    ports:
-      - 9200
+      service: banyandb
   oap:
     extends:
       file: ../../base/docker-compose.yml
@@ -29,7 +27,7 @@ services:
     ports:
       - 12800
     depends_on:
-      es:
+      banyandb:
         condition: service_healthy
 
   provider:
diff --git a/test/cases/basic/expected/layer-list.yml 
b/test/cases/basic/expected/layer-list.yml
index 360f638..ff59649 100644
--- a/test/cases/basic/expected/layer-list.yml
+++ b/test/cases/basic/expected/layer-list.yml
@@ -13,50 +13,55 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-- CLICKHOUSE
+# The query pipes through `yq e 'sort'`, so this is the layer set in sorted 
order — the
+# OAP layer registry's own iteration order is not stable across builds, so we 
normalize
+# both sides by sorting. Update this set when the OAP layer list changes.
+- ACTIVEMQ
+- ALIPAY_MINI_PROGRAM
+- APISIX
+- AWS_DYNAMODB
+- AWS_EKS
+- AWS_GATEWAY
+- AWS_S3
+- BANYANDB
+- BOOKKEEPER
 - BROWSER
-- RABBITMQ
-- MESH
-- GENERAL
+- CACHE
+- CILIUM_SERVICE
+- CLICKHOUSE
+- DATABASE
+- ELASTICSEARCH
+- ENVOY_AI_GATEWAY
 - FAAS
-- MESH_CP
-- AWS_GATEWAY
+- FLINK
 - GENAI
-- BANYANDB
-- NGINX
-- SO11Y_JAVA_AGENT
-- ACTIVEMQ
-- SO11Y_SATELLITE
+- GENERAL
+- IOS
+- K8S
 - K8S_SERVICE
-- VIRTUAL_GATEWAY
-- CILIUM_SERVICE
-- AWS_EKS
+- KAFKA
+- KONG
+- MESH
+- MESH_CP
+- MESH_DP
+- MONGODB
 - MQ
 - MYSQL
-- VIRTUAL_DATABASE
-- K8S
-- VIRTUAL_MQ
-- PULSAR
-- MONGODB
-- CACHE
-- OS_WINDOWS
-- SO11Y_GO_AGENT
-- ENVOY_AI_GATEWAY
-- MESH_DP
-- VIRTUAL_GENAI
-- SO11Y_OAP
-- DATABASE
+- NGINX
 - OS_LINUX
+- OS_WINDOWS
+- POSTGRESQL
+- PULSAR
+- RABBITMQ
 - REDIS
-- KAFKA
-- ELASTICSEARCH
 - ROCKETMQ
-- APISIX
+- SO11Y_GO_AGENT
+- SO11Y_JAVA_AGENT
+- SO11Y_OAP
+- SO11Y_SATELLITE
 - VIRTUAL_CACHE
-- POSTGRESQL
-- AWS_S3
-- BOOKKEEPER
-- KONG
-- FLINK
-- AWS_DYNAMODB
-
+- VIRTUAL_DATABASE
+- VIRTUAL_GATEWAY
+- VIRTUAL_GENAI
+- VIRTUAL_MQ
+- WECHAT_MINI_PROGRAM
diff --git a/test/cases/basic/expected/trace-users-detail.yml 
b/test/cases/basic/expected/trace-users-detail.yml
deleted file mode 100644
index a286162..0000000
--- a/test/cases/basic/expected/trace-users-detail.yml
+++ /dev/null
@@ -1,103 +0,0 @@
-# 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.
-
-spans:
-  {{- contains .spans }}
-  - traceid: {{ .traceid }}
-    segmentid: {{ .segmentid }}
-    spanid: {{ .spanid }}
-    parentspanid: {{ .parentspanid }}
-    refs: [ ]
-    servicecode: consumer
-    serviceinstancename: consumer1
-    starttime: {{ gt .starttime 0 }}
-    endtime: {{ gt .endtime 0 }}
-    endpointname: /users
-    type: Entry
-    peer: {{ .peer }}
-    component: Python
-    iserror: false
-    layer: Http
-    tags:
-      {{- contains .tags }}
-      - key: http.method
-        value: POST
-      - key: http.url
-        value: {{ notEmpty .value }}
-      - key: http.status_code
-        value: 200
-      {{- end }}
-    logs: [ ]
-    attachedevents: []
-  - traceid: {{ notEmpty .traceid }}
-    segmentid: {{ .segmentid }}
-    spanid: {{ .spanid }}
-    parentspanid: {{ .parentspanid }}
-    refs: [ ]
-    servicecode: consumer
-    serviceinstancename: consumer1
-    starttime: {{ gt .starttime 0 }}
-    endtime: {{ gt .endtime 0 }}
-    endpointname: /users
-    type: Exit
-    peer: {{ .peer }}
-    component: Python
-    iserror: false
-    layer: Http
-    tags:
-      {{- contains .tags }}
-      - key: http.method
-        value: POST
-      - key: http.url
-        value: {{ notEmpty .value }}
-      - key: http.status_code
-        value: 200
-      {{- end }}
-    logs: [ ]
-    attachedevents: []
-  - traceid: {{ notEmpty .traceid }}
-    segmentid: {{ .segmentid }}
-    spanid: {{ .spanid }}
-    parentspanid: {{ .parentspanid }}
-    refs:
-      {{- contains .refs }}
-      - traceid: {{ notEmpty .traceid }}
-        parentsegmentid: {{ .parentsegmentid }}
-        parentspanid: 1
-        type: CROSS_PROCESS
-      {{- end }}
-    servicecode: provider
-    serviceinstancename: provider1
-    starttime: {{ gt .starttime 0 }}
-    endtime: {{ gt .endtime 0 }}
-    endpointname: /users
-    type: Entry
-    peer: {{ .peer }}
-    component: Python
-    iserror: false
-    layer: Http
-    tags:
-      {{- contains .tags }}
-      - key: http.method
-        value: POST
-      - key: http.url
-        value: {{ notEmpty .value }}
-      - key: http.status_code
-        value: 200
-      {{- end }}
-    logs: [ ]
-    attachedevents: []
-  {{- end }}
-debuggingtrace: null
\ No newline at end of file
diff --git a/test/cases/basic/test.yaml b/test/cases/basic/test.yaml
index 6214a1c..c094692 100644
--- a/test/cases/basic/test.yaml
+++ b/test/cases/basic/test.yaml
@@ -84,7 +84,7 @@ verify:
     - query: swctl --display yaml 
--base-url=http://${oap_host}:${oap_12800}/graphql metrics single --name 
service_instance_cpm --service-name provider --instance-name provider1
       expected: expected/value.yml
 
-    - query: swctl --display yaml 
--base-url=http://${oap_host}:${oap_12800}/graphql layer list
+    - query: swctl --display yaml 
--base-url=http://${oap_host}:${oap_12800}/graphql layer list | yq e 'sort' -
       expected: expected/layer-list.yml
 
     - query: swctl --display yaml 
--base-url=http://${oap_host}:${oap_12800}/graphql service list
@@ -94,11 +94,11 @@ verify:
     - query: swctl --display yaml 
--base-url=http://${oap_host}:${oap_12800}/graphql service layer GENERAL
       expected: expected/service.yml
 
-    - query: swctl --display yaml 
--base-url=http://${oap_host}:${oap_12800}/graphql trace ls
-      expected: expected/traces-list.yml
-    - query: |
-        swctl --display yaml 
--base-url=http://${oap_host}:${oap_12800}/graphql trace $( \
-          swctl --display yaml 
--base-url=http://${oap_host}:${oap_12800}/graphql trace ls \
-            | yq e '.traces | select(.[].endpointnames[0]=="/users") | 
.[0].traceids[0]' -
-        )
-      expected: expected/trace-users-detail.yml
+    # BanyanDB rejects the v1 trace API ("BanyanDB Trace Model changed, please 
use
+    # queryTraces"), so trace coverage uses the v2 `trace-v2` command. 
`trace-v2 list`
+    # returns full spans inline, so one command verifies both the listing and 
the
+    # per-endpoint detail the old `trace`/`trace ls` cases covered.
+    - query: swctl --display yaml 
--base-url=http://${oap_host}:${oap_12800}/graphql trace-v2 list | yq e 
'.traces | length' -
+      expected: expected/value.yml
+    - query: swctl --display yaml 
--base-url=http://${oap_host}:${oap_12800}/graphql trace-v2 list | yq e 
'[.traces[].spans[] | select(.endpointname == "/users")] | length' -
+      expected: expected/value.yml
diff --git a/test/cases/basic/docker-compose.yml 
b/test/cases/live-debugging/docker-compose.yml
similarity index 73%
copy from test/cases/basic/docker-compose.yml
copy to test/cases/live-debugging/docker-compose.yml
index 30fe99f..9edb61e 100644
--- a/test/cases/basic/docker-compose.yml
+++ b/test/cases/live-debugging/docker-compose.yml
@@ -13,23 +13,29 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+# Topology for the OAL live-debugging case: BanyanDB + OAP (admin REST on 
17128) plus
+# the provider/consumer demo apps. Unlike the static `admin` case, the DSL live
+# debugger captures live ingest, so this case needs traffic — the 
consumer→provider
+# calls drive Service / ServiceRelation source events through the shipped 
core.oal
+# pipeline while the debug session is attached.
+# Requires an admin-capable OAP (OAP_TAG >= 11.0.0).
+
 version: "2.1"
 
 services:
-  es:
+  banyandb:
     extends:
       file: ../../base/docker-compose.yml
-      service: es
-    ports:
-      - 9200
+      service: banyandb
   oap:
     extends:
       file: ../../base/docker-compose.yml
       service: oap
     ports:
       - 12800
+      - 17128
     depends_on:
-      es:
+      banyandb:
         condition: service_healthy
 
   provider:
diff --git a/test/cases/basic/expected/traces-list.yml 
b/test/cases/live-debugging/expected/ok.yml
similarity index 75%
rename from test/cases/basic/expected/traces-list.yml
rename to test/cases/live-debugging/expected/ok.yml
index a51cb7f..e33f055 100644
--- a/test/cases/basic/expected/traces-list.yml
+++ b/test/cases/live-debugging/expected/ok.yml
@@ -13,15 +13,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-traces:
-  {{- contains .traces }}
-- segmentid: {{ notEmpty .segmentid }}
-  endpointnames:
-    - /users
-  duration: {{ ge .duration 0 }}
-  start: "{{ notEmpty .start}}"
-  iserror: false
-  traceids:
-    - {{ index .traceids 0 }}
-  {{- end }}
-debuggingtrace: null
\ No newline at end of file
+ok
diff --git a/test/cases/live-debugging/oal-debug-flow.sh 
b/test/cases/live-debugging/oal-debug-flow.sh
new file mode 100755
index 0000000..6834df3
--- /dev/null
+++ b/test/cases/live-debugging/oal-debug-flow.sh
@@ -0,0 +1,116 @@
+#!/usr/bin/env bash
+#
+# 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.
+
+# Drives swctl's DSL live-debugger commands end-to-end against the OAL 
pipeline:
+#   admin dsl-debug status  → injection enabled
+#   admin dsl-debug session start (catalog=oal, name=core.oal, 
ruleName=<metric>)
+#   admin dsl-debug session get   → poll until the live trace flow is captured
+#   admin dsl-debug session stop
+#
+# The consumer→provider demo traffic fires the OAL source events that the 
session
+# captures. Mirrors apache/skywalking test/e2e-v2/cases/dsl-debugging/oal but 
exercises
+# swctl instead of curl. All diagnostics go to stderr; the only stdout is the 
final
+# "ok" the e2e verify matches.
+
+set -euo pipefail
+
+log() { echo "[oal-debug] $*" >&2; }
+fail() { log "FAIL: $*"; exit 1; }
+
+OAP_HOST="${OAP_HOST:-127.0.0.1}"
+OAP_ADMIN_PORT="${OAP_ADMIN_PORT:-17128}"
+SETTLE_SECONDS="${SETTLE_SECONDS:-300}"
+
+# OAL debug is per-metric. service_cpm is a shipped core.oal rule on the 
Service source
+# that every instrumented service fires on each inbound request, so the demo 
traffic
+# drives it reliably.
+CATALOG="oal"
+NAME="core.oal"
+METRIC="${METRIC:-service_cpm}"
+# OAL source class the metric is derived from — surfaces as the input sample's
+# payload.type (service_cpm = from(Service.*).cpm() → Service).
+SOURCE_TYPE="${SOURCE_TYPE:-Service}"
+CLIENT_ID="e2e-oal-debug-$$"
+
+ADMIN=(--display=json "--admin-url=http://${OAP_HOST}:${OAP_ADMIN_PORT}";)
+
+# --- Phase 0: module status 
-----------------------------------------------------------
+log "=== Phase 0: dsl-debug status ==="
+swctl "${ADMIN[@]}" admin dsl-debug status | yq e '.injectionEnabled' - | grep 
-qx true \
+    || fail "injectionEnabled is not true"
+
+# --- Phase 1: start session 
-----------------------------------------------------------
+log "=== Phase 1: start OAL session (catalog=${CATALOG}, name=${NAME}, 
ruleName=${METRIC}) ==="
+start_out="$(swctl "${ADMIN[@]}" admin dsl-debug session start \
+    --catalog "${CATALOG}" --name "${NAME}" --rule-name "${METRIC}" 
--client-id "${CLIENT_ID}")"
+log "  start → ${start_out}"
+SESSION_ID="$(echo "${start_out}" | yq e '.sessionId // ""' -)"
+[ -n "${SESSION_ID}" ] || fail "session start did not return a sessionId"
+log "✓ session started: ${SESSION_ID}"
+
+# --- Phase 2: poll until the live OAL pipeline is captured 
-----------------------------
+log "=== Phase 2: poll for captured records (budget ${SETTLE_SECONDS}s) ==="
+deadline=$(( $(date +%s) + SETTLE_SECONDS ))
+records=0
+body=""
+while [ "$(date +%s)" -lt "${deadline}" ]; do
+    body="$(swctl "${ADMIN[@]}" admin dsl-debug session get "${SESSION_ID}")"
+    records="$(echo "${body}" | yq e '[.nodes[].records[]] | length' -)"
+    [ "${records}" -gt 0 ] && break
+    sleep 5
+done
+[ "${records}" -gt 0 ] || fail "no records captured within ${SETTLE_SECONDS}s"
+log "✓ captured ${records} record(s)"
+
+# --- Phase 3: verify the capture is EXACTLY the bound metric 
--------------------------
+log "=== Phase 3: assert the captured pipeline is exactly ${METRIC} ==="
+
+samples="$(echo "${body}" | yq e '[.nodes[].records[].samples[]] | length' -)"
+[ "${samples}" -gt 0 ] || fail "captured records carry no samples"
+
+# We assert the bound rule from the verbatim .dsl source and the output 
samples rather
+# than the per-record .rule envelope, which the server does not reliably 
populate for OAL.
+
+# 3a. Each record carries the verbatim core.oal source of ${METRIC}.
+dsl_hits="$(echo "${body}" | yq e "[.nodes[].records[] | select(.dsl | 
contains(\"${METRIC}\"))] | length" -)"
+[ "${dsl_hits}" -gt 0 ] || fail "no record's .dsl carries the ${METRIC} OAL 
source"
+
+# 3b. Source stage: an input sample drawn from the ${SOURCE_TYPE} source.
+src="$(echo "${body}" | yq e "[.nodes[].records[].samples[] | select(.type == 
\"input\" and .payload.type == \"${SOURCE_TYPE}\")] | length" -)"
+[ "${src}" -gt 0 ] || fail "no input sample from the ${SOURCE_TYPE} source"
+
+# 3c. Aggregation stage: the verbatim cpm() function from the rule.
+agg="$(echo "${body}" | yq e '[.nodes[].records[].samples[] | select(.type == 
"aggregation" and .sourceText == "cpm()")] | length' -)"
+[ "${agg}" -gt 0 ] || fail "no cpm() aggregation sample for ${METRIC}"
+
+# 3d. Output stage: the materialised ${METRIC} metric.
+out="$(echo "${body}" | yq e "[.nodes[].records[].samples[] | select(.type == 
\"output\" and .sourceText == \"${METRIC}\")] | length" -)"
+[ "${out}" -gt 0 ] || fail "no output sample for metric ${METRIC}"
+
+# 3e. Per-metric gate isolation: no OTHER metric's output leaked through this 
session.
+leak="$(echo "${body}" | yq e "[.nodes[].records[].samples[] | select(.type == 
\"output\" and .sourceText != \"${METRIC}\")] | length" -)"
+[ "${leak}" = "0" ] || fail "${leak} output sample(s) for a metric other than 
${METRIC} leaked into the session"
+
+log "✓ capture is exactly ${METRIC}: ${samples} samples — ${SOURCE_TYPE} 
source → cpm() → ${METRIC} output, no other metric leaked"
+
+# --- Phase 4: stop session 
------------------------------------------------------------
+log "=== Phase 4: stop session ==="
+swctl "${ADMIN[@]}" admin dsl-debug session stop "${SESSION_ID}" | yq e 
'.localStopped' - | grep -qx true \
+    || fail "localStopped is not true"
+
+log "=== OAL LIVE-DEBUG FLOW PASSED (${records} records, ${samples} samples) 
==="
+echo ok
diff --git a/test/cases/live-debugging/test.yaml 
b/test/cases/live-debugging/test.yaml
new file mode 100644
index 0000000..5111b4e
--- /dev/null
+++ b/test/cases/live-debugging/test.yaml
@@ -0,0 +1,46 @@
+# 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.
+
+# OAL live-debugging E2E: drives swctl's `admin dsl-debug session` lifecycle 
against a
+# real OAL capture. The trigger sends continuous traffic so the OAL pipeline 
keeps
+# firing while the debug session is attached; the flow script does the stateful
+# start → poll → assert → stop and prints "ok" on success. Requires an 
admin-capable
+# OAP (OAP_TAG >= 11.0.0).
+
+setup:
+  env: compose
+  file: docker-compose.yml
+  timeout: 25m
+  steps:
+    - name: install yq
+      command: yq > /dev/null 2>&1 || go install 
github.com/mikefarah/yq/v4@latest
+
+trigger:
+  action: http
+  interval: 3s
+  # Continuous traffic: the live debugger captures ingest as it flows, so the 
OAL
+  # pipeline must keep firing for the whole session lifecycle.
+  times: -1
+  url: http://${consumer_host}:${consumer_9090}/users
+  method: POST
+
+verify:
+  # The flow script owns its own polling/retry budget, so a single verify pass 
is enough.
+  retry:
+    count: 1
+    interval: 1s
+  cases:
+    - query: OAP_HOST=${oap_host} OAP_ADMIN_PORT=${oap_17128} bash 
test/cases/live-debugging/oal-debug-flow.sh
+      expected: expected/ok.yml


Reply via email to