Skip to content

feat(mcp): OAuth 2.1 resource-server discovery surface for /mcp - #859

Open
membphis wants to merge 2 commits into
mainfrom
feat/mcp-oauth-inbound-dp
Open

feat(mcp): OAuth 2.1 resource-server discovery surface for /mcp#859
membphis wants to merge 2 commits into
mainfrom
feat/mcp-oauth-inbound-dp

Conversation

@membphis

@membphis membphis commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Part of api7/AISIX-Cloud#1143 (the inbound leg; the upstream-leg client-side MUSTs are tracked separately in api7/AISIX-Cloud#1152).

What

Makes the gateway's /mcp surface discoverable as a spec-compliant OAuth 2.1 resource server per the MCP authorization spec (2025-11-25). When an environment projects the new mcp_auth_settings row (its canonical /mcp resource URL) AND has at least one enabled oidc_provider:

  • GET /.well-known/oauth-protected-resource and its RFC 9728 path-insertion sibling .../mcp serve the Protected Resource Metadata document, derived entirely from configuration: resource = the configured URL (never the request Host), authorization_servers = enabled providers' issuers, scopes_supported = the union of their required_scopes, bearer_methods_supported = ["header"].
  • /mcp auth failures carry RFC 6750 WWW-Authenticate challenges: bare resource_metadata on missing credentials; error="invalid_token" on any rejected credential; error="insufficient_scope" naming ONLY the currently-required scopes on a scope failure (SEP-2350 forward-compatible: no echo of previously granted scopes).
  • A bound_claims policy denial deliberately carries NO challenge (re-consenting cannot cure it); JwksUnavailable (503) likewise.

Without the settings row the surface is dormant: the well-known routes 404 and no header is attached — every existing environment is byte-identical to before.

Wire compatibility

The scope failure is split out of JwtClaimsRejected into a dedicated JwtInsufficientScope variant so the /mcp middleware can classify it. It renders byte-identically on the wire (same 403, permission_denied type, jwt_claims_rejected code, same message) — pinned by a test — so /v1 callers cannot observe the split. Token validation itself is unchanged: the existing JWT chain (signature/iss/exp/aud with inclusion semantics, scope/claim enforcement, jwt_subject key binding) is reused as-is.

New resource: mcp_auth_settings

Env-scoped singleton (the CP keys the row by the environment id) with one field, resource_url (absolute http(s), path exactly /mcp, no query/fragment — malformed rows keep the surface dormant with one process-wide warning). Wired through the full config path: schema variant (schemas/resources/mcp_auth_settings.schema.json, emitted by dump-schema), snapshot table, etcd loader + watch supervisor (put/delete/clone/resource-counts), declarative filesource (a second entry per file is a load error) and export.

Sequencing (important)

AISIX-Cloud's nightly cross-plane contract check fails on any unregistered DP schema. The registration entry (AISIX-Cloud side) must merge the same day as this PR, before the next nightly run. The AISIX-Cloud counterpart PRs (config surface + dashboard + the full-chain live-DP e2e) follow this one because the DP drops projected rows with unknown fields.

Prior art

Surveyed eight mainstream AI/MCP gateway products before designing (per repo rule):

  • Serving BOTH well-known path forms is deliberate compat hardening: one LLM-proxy gateway shipped a non-standard metadata path that broke spec-strict clients, and several mainstream MCP clients ignore path segments during discovery.
  • Only one surveyed product enforces audience binding to the resource URI strictly by default (another shipped the check commented out); we keep the existing strict inclusion-semantics validation and make the canonical URI explicit configuration.
  • A hosting platform documented Host-header-derived metadata breaking behind reverse proxies; the resource URL here is never derived from the request.
  • A purpose-built agent gateway generates its metadata document from operator policy (issuer list + scopes) — the same derivation-from-config shape used here.

Testing

  • cargo test across aisix-core / aisix-etcd / aisix-proxy / aisix-server: all green (1400+ tests), clippy + fmt clean.
  • New unit/integration coverage: PRM document shape on both paths + 404 when dormant; challenge header per error variant including the negative assertions (no header on bound_claims 403, on dormant environments, or on non-/mcp routes); exact insufficient_scope scope attribute; /v1 scope-failure rendering unchanged; audience inclusion in both directions; filesource singleton enforcement; watch-supervisor propagation for the new kind (put + delete).
  • The full-chain e2e (real Keycloak -> discovery -> authorization code + PKCE -> governed tools/call through a live DP built from this branch) lives in the AISIX-Cloud counterpart and passes 7/7 against this branch.

Summary by CodeRabbit

  • New Features
    • Added MCP authentication settings with configurable resource URLs.
    • Added OAuth protected-resource discovery endpoints for MCP.
    • Added WWW-Authenticate challenges for applicable MCP authentication failures.
    • MCP authentication settings are now supported across file, etcd, snapshot, and export workflows.
  • Bug Fixes
    • Improved JWT authorization feedback by distinguishing insufficient scopes from other claim failures while preserving existing API responses.
  • Documentation
    • Added a JSON Schema for MCP authentication settings.

Implements the inbound-leg half of AISIX-Cloud#1143. When an
environment projects the new mcp_auth_settings row (canonical /mcp
resource URL) and has at least one enabled oidc_provider, the gateway:

- serves the RFC 9728 Protected Resource Metadata document on both
  /.well-known/oauth-protected-resource and its path-insertion /mcp
  form, derived entirely from configuration (resource URL, enabled
  providers' issuers, union of their required_scopes);
- attaches WWW-Authenticate challenges to /mcp auth failures:
  bare resource_metadata on missing credentials, error=invalid_token
  on rejected credentials, and error=insufficient_scope naming only
  the currently-required scopes on a scope failure (the scope failure
  is split into a dedicated JwtInsufficientScope variant that renders
  byte-identically to JwtClaimsRejected, so /v1 is unchanged;
  bound_claims policy denials deliberately carry no challenge).

Without the settings row the surface is dormant: the well-known routes
404 and no header is attached — every existing environment behaves
byte-identically to before. A malformed row (path other than /mcp,
query/fragment, non-http scheme) keeps the surface dormant with one
process-wide warning. The resources file treats mcp_auth_settings as a
singleton: a second entry is a load error.

The row is wired through the full config path: schema variant
(schemas/resources/mcp_auth_settings.schema.json via dump-schema),
snapshot table, etcd loader + watch supervisor (put/delete/clone),
declarative filesource and export.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@membphis, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: af5414cd-3065-4afe-9bc1-d806caa94ce4

📥 Commits

Reviewing files that changed from the base of the PR and between c9d783a and e1b4b11.

📒 Files selected for processing (5)
  • crates/aisix-core/src/filesource/mod.rs
  • crates/aisix-core/src/filesource/tests.rs
  • crates/aisix-proxy/src/jwt.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp_auth.rs
📝 Walkthrough

Walkthrough

Adds the singleton mcp_auth_settings resource across core models, file and etcd loading, snapshot updates, and exports. Adds MCP OAuth protected-resource discovery and scoped authentication challenges, with typed JWT scope-failure classification and comprehensive tests.

Changes

MCP OAuth support

Layer / File(s) Summary
MCP auth settings contract
crates/aisix-core/src/models/..., schemas/resources/...
Defines the strict McpAuthSettings model, schema validation, snapshot storage, and generated schema.
File-source singleton loading
crates/aisix-core/src/filesource/...
Loads one validated mcp_auth_settings entry per file using fixed singleton identity handling and tests duplicate rejection.
etcd synchronization and export
crates/aisix-etcd/src/..., crates/aisix-server/src/export/document.rs
Adds initial loading, incremental put/delete handling, snapshot cloning, resource counts, tests, and export support.
JWT authentication classification
crates/aisix-proxy/src/error.rs, crates/aisix-proxy/src/jwt.rs
Classifies insufficient-scope failures separately while preserving the existing HTTP error envelope and attaching internal auth challenges.
MCP discovery and challenge middleware
crates/aisix-proxy/src/mcp_auth.rs, crates/aisix-proxy/src/lib.rs
Adds conditional RFC 9728 metadata endpoints and MCP-only WWW-Authenticate challenge middleware with validation and tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ProxyRouter
  participant MCPAuth
  participant Snapshot
  participant OIDCProviders
  Client->>ProxyRouter: Request OAuth protected-resource metadata
  ProxyRouter->>MCPAuth: Handle discovery request
  MCPAuth->>Snapshot: Read mcp_auth_settings
  MCPAuth->>OIDCProviders: Collect enabled issuers and scopes
  MCPAuth-->>Client: Return PRM metadata or 404
  Client->>ProxyRouter: Request /mcp
  ProxyRouter->>MCPAuth: Apply challenge middleware
  MCPAuth-->>Client: Return scoped WWW-Authenticate challenge
Loading

Possibly related PRs

  • api7/aisix#822: Adds another MCP resource kind through the shared model, schema, snapshot, and etcd pipelines.
  • api7/aisix#825: Changes related JWT scope and claim failure classification paths.

Suggested reviewers: moonming


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Security Check ❌ Error FAIL: mcp_auth.rs publishes identity.resource_url verbatim, and the validator never rejects userinfo; a credentialed URL would leak via the unauthenticated PRM response. Reject credential-bearing URLs in validate_resource_url (userinfo/query secrets) or sanitize the emitted resource before returning PRM.
E2e Test Quality Review ⚠️ Warning Coverage is solid but not true E2E: the new MCP paths use router oneshot, WireMock, and FakeProvider, not a live API→etcd/IdP flow. Add at least one full-stack test that boots the proxy with a real config source and a live OIDC/etcd fixture, then verifies discovery + /mcp auth end to end.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an OAuth 2.1 resource-server discovery surface for /mcp.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-oauth-inbound-dp

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
crates/aisix-core/src/models/mcp_auth_settings.rs (2)

1-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use public API wording for model documentation.

These comments expose etcd/control-plane and loader implementation details. Describe runtime_id as a runtime-only identity omitted from serialized configuration without exposing storage topology.

As per coding guidelines, model comments should be public API reference text and avoid internal shorthand.

Also applies to: 32-35

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aisix-core/src/models/mcp_auth_settings.rs` around lines 1 - 3, Update
the module and field documentation in McpAuthSettings to use public API
terminology: describe runtime_id as a runtime-only identity omitted from
serialized configuration, and remove references to etcd, control-plane keying,
loader details, and internal shorthand.

Source: Coding guidelines


5-10: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use generic references in shipped comments across the MCP settings flow.

These comments expose product-specific ticket identifiers in public model, snapshot, loader-test, and supervisor-test documentation.

  • crates/aisix-core/src/models/mcp_auth_settings.rs#L5-L10: replace the ticket reference with generic MCP OAuth discovery wording.
  • crates/aisix-core/src/models/snapshot.rs#L60-L65: remove the product-specific ticket from the snapshot documentation.
  • crates/aisix-core/src/filesource/tests.rs#L554-L555: use generic singleton-resource wording.
  • crates/aisix-etcd/src/supervisor.rs#L1049-L1051: use generic watch-activation wording.
  • crates/aisix-etcd/src/supervisor.rs#L1141-L1142: use generic watch-deactivation wording.

As per coding guidelines, shipped code and comments should refer to other products generically.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aisix-core/src/models/mcp_auth_settings.rs` around lines 5 - 10,
Replace product-specific ticket or product references with generic wording in
the comments at crates/aisix-core/src/models/mcp_auth_settings.rs:5-10,
crates/aisix-core/src/models/snapshot.rs:60-65, and
crates/aisix-core/src/filesource/tests.rs:554-555; use generic MCP OAuth
discovery and singleton-resource descriptions. Update the watch-activation and
watch-deactivation comments at crates/aisix-etcd/src/supervisor.rs:1049-1051 and
:1141-1142 similarly, without changing behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aisix-etcd/src/loader.rs`:
- Around line 297-308: Update the mcp_auth_settings branch in the
snapshot-building loader to enforce a single valid entry: reject or fail the
snapshot when another entry is already present instead of inserting every parsed
row. Preserve normal insertion for the first entry and ensure duplicate
singleton settings cannot be published or selected by a downstream resolver.

---

Nitpick comments:
In `@crates/aisix-core/src/models/mcp_auth_settings.rs`:
- Around line 1-3: Update the module and field documentation in McpAuthSettings
to use public API terminology: describe runtime_id as a runtime-only identity
omitted from serialized configuration, and remove references to etcd,
control-plane keying, loader details, and internal shorthand.
- Around line 5-10: Replace product-specific ticket or product references with
generic wording in the comments at
crates/aisix-core/src/models/mcp_auth_settings.rs:5-10,
crates/aisix-core/src/models/snapshot.rs:60-65, and
crates/aisix-core/src/filesource/tests.rs:554-555; use generic MCP OAuth
discovery and singleton-resource descriptions. Update the watch-activation and
watch-deactivation comments at crates/aisix-etcd/src/supervisor.rs:1049-1051 and
:1141-1142 similarly, without changing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1909f638-ed40-48e1-8085-bab92fdcd1d9

📥 Commits

Reviewing files that changed from the base of the PR and between fb80aaf and c9d783a.

📒 Files selected for processing (16)
  • crates/aisix-core/src/bin/dump-schema.rs
  • crates/aisix-core/src/filesource/desugar.rs
  • crates/aisix-core/src/filesource/mod.rs
  • crates/aisix-core/src/filesource/tests.rs
  • crates/aisix-core/src/models/mcp_auth_settings.rs
  • crates/aisix-core/src/models/mod.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-core/src/models/snapshot.rs
  • crates/aisix-etcd/src/loader.rs
  • crates/aisix-etcd/src/supervisor.rs
  • crates/aisix-proxy/src/error.rs
  • crates/aisix-proxy/src/jwt.rs
  • crates/aisix-proxy/src/lib.rs
  • crates/aisix-proxy/src/mcp_auth.rs
  • crates/aisix-server/src/export/document.rs
  • schemas/resources/mcp_auth_settings.schema.json

Comment on lines +297 to +308
"mcp_auth_settings" => {
if let Some(entry) = validate_and_parse::<McpAuthSettings>(
&raw.key,
raw.revision,
parsed,
&value,
validate_mcp_auth_settings,
&mut stats,
) {
snapshot.mcp_auth_settings.insert(entry);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)crates/aisix-etcd/src/loader.rs$|mcp_auth_settings|aisix-etcd' || true

echo "== loader outline =="
ast-grep outline crates/aisix-etcd/src/loader.rs --view expanded 2>/dev/null || true

echo "== relevant loader lines =="
sed -n '240,340p' crates/aisix-etcd/src/loader.rs

Repository: api7/aisix

Length of output: 5844


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate references without executing repository code.
rg -n "mcp_auth_settings|McpAuthSettings|validate_mcp_auth_settings|loader|snapshot|insert_into|load|watch" crates -S

Repository: api7/aisix

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== loader full parse/insert flow =="
sed -n '109,330p' crates/aisix-etcd/src/loader.rs

echo "== mcp auth settings model =="
sed -n '1,110p' crates/aisix-core/src/models/mcp_auth_settings.rs

echo "== snapshot and resource table definitions =="
ast-grep outline crates/aisix-core/src/snapshot.rs --view expanded 2>/dev/null || true
sed -n '1,220p' crates/aisix-core/src/snapshot.rs
sed -n '1,140p' crates/aisix-core/src/resource.rs

echo "== mcp gateway duplicate selection relevant sections =="
sed -n '130,230p' crates/aisix-mcp/src/bridge.rs
sed -n '270,320p' crates/aisix-mcp/src/gateway.rs

Repository: api7/aisix

Length of output: 30316


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== mcp auth settings usages =="
rg -n "mcp_auth_settings|McpAuthSettings" crates/aisix-mcp crates/aisix-core crates -S --glob '!target/**'

echo "== protected resource / auth discovery code =="
rg -n "oauth-protected-resource|WWW-Authenticate|resource_server|find_unique_by|auth_settings|protected resource" crates/aisix-mcp crates/aisix-proxy crates -S --glob '!target/**'

echo "== resource file duplicate detection =="
rg -n "mcp_auth_settings|one row|singleton|at most one|duplicate|ResourceTable|find_unique_by|build_snapshot" crates/aisix-admin crates/aisix-core crates/aisix-etcd tests -S --glob '!target/**' | head -n 200

Repository: api7/aisix

Length of output: 50366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== mcp_auth.rs resolver =="
sed -n '1,120p' crates/aisix-proxy/src/mcp_auth.rs
sed -n '720,870p' crates/aisix-proxy/src/lib.rs

echo "== etcd integration test around duplicate singleton behavior in loader =="
sed -n '340,450p' crates/aisix-admin/tests/etcd_integration.rs

Repository: api7/aisix

Length of output: 13616


Enforce the mcp_auth_settings singleton at snapshot build time.

The etcd loader accepts every valid /mcp_auth_settings/<id> row, so stale/migrated keys can publish multiple singleton settings with the same fixed resource name. The runtime picker currently falls back to the smallest id, but this violates the per-environment singleton model and can silently expose the wrong discovery URL/resource metadata. Reject duplicate entries before publishing the snapshot, or align the downstream resolver to fail closed when it sees more than one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aisix-etcd/src/loader.rs` around lines 297 - 308, Update the
mcp_auth_settings branch in the snapshot-building loader to enforce a single
valid entry: reject or fail the snapshot when another entry is already present
instead of inserting every parsed row. Preserve normal insertion for the first
entry and ensure duplicate singleton settings cannot be published or selected by
a downstream resolver.

Addresses the independent pre-merge audit of #859:

- reject userinfo in resource_url (validate_resource_url + a filesource
  load error mirroring the OIDC issuer/jwks_uri rule): the URL is
  published verbatim on the unauthenticated protected-resource-metadata
  endpoint, so an embedded credential must never activate the surface.
- extract claims_rejection_error() and unit-test both arms, pinning the
  single construction site of JwtInsufficientScope (scope failures must
  carry the provider's required scopes; bound-claims denials must keep
  the challenge-less variant).
- register the well-known routes with any() and gate GET/HEAD inside
  the handler, so a dormant environment answers the pre-existing bare
  404 for every method (previously non-GET flipped to 405 even while
  dormant); active non-GET/HEAD now 405s with an Allow header. Tests
  pin both.
- sanitize challenge-header interpolations to RFC 6750 NQCHAR: a space
  can no longer corrupt the scope list and a control byte loses one
  character instead of silently dropping the whole WWW-Authenticate
  header.
- deterministic multi-row pick test, and a comment on the middleware's
  second snapshot load (accepted eventual consistency).
@membphis

Copy link
Copy Markdown
Contributor Author

Independent pre-merge audit completed (six angles: correctness / reliability / security / sensitive-info leakage / breaking changes / test coverage). Findings and resolutions:

Finding Severity Resolution
Embedded userinfo in resource_url would be published verbatim on the unauthenticated PRM endpoint MEDIUM Fixed in e1b4b11validate_resource_url rejects userinfo; filesource adds the same load error the OIDC issuer/jwks_uri rule uses; tests added
Single construction site of JwtInsufficientScope untested — the headline insufficient_scope contract could regress silently MEDIUM Fixed in e1b4b11 — mapping extracted into claims_rejection_error() and unit-tested for both arms (scope failures carry the provider's required scopes; bound-claims denials keep the challenge-less variant)
Non-GET methods on the well-known paths flipped 404→405 even while dormant LOW Fixed in e1b4b11 — routes registered with any(...), dormancy check first (bare 404 for every method), active non-GET/HEAD 405 + Allow; both pinned by router tests
A control byte in a configured scope silently dropped the whole WWW-Authenticate header; a space corrupted the scope list LOW Fixed in e1b4b11 — interpolations filtered to RFC 6750 NQCHAR; test added
Challenge middleware loads the snapshot a second time (config-swap window) LOW Accepted — documented on the middleware as eventual consistency; the client re-runs discovery and self-heals
Multi-row deterministic pick untested; export round-trip of the new kind unpinned LOW Multi-row pick test added in e1b4b11; export shape is guaranteed by construction (runtime_id is #[serde(skip)], emit_entries serializes the value directly) — pinning it rides the pre-existing dump-schema drift-check follow-up

Clean angles per the audit: RFC 9728/6750 conformance, /v1 wire parity (envelope-level test), middleware scoping, log/error hygiene, and the etcd/filesource/snapshot plumbing symmetry.

Post-fix verification: cargo test green across the touched crates (485 + 779 in core/proxy), clippy/fmt clean, and the AISIX-Cloud full-chain live-DP e2e re-passes against this branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant