Skip to content

fix(core): enforce the a2a/mcp name and credential rules on every config path - #848

Open
moonming wants to merge 3 commits into
mainfrom
fix/lift-a2a-mcp-validation
Open

fix(core): enforce the a2a/mcp name and credential rules on every config path#848
moonming wants to merge 3 commits into
mainfrom
fix/lift-a2a-mcp-validation

Conversation

@moonming

@moonming moonming commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Prerequisite for removing the Admin API write path — and a bug fix in its own right.

The bug

The per-auth_type credential coupling, the name-shape rules, and the type/spec/api_key_header coupling are enforced only in this gateway's Admin API write handlers. A declaratively configured gateway never sees them. On main today:

# resources.yaml
a2a_agents:
  - name: bad/name          # the name IS the /a2a/<name> path segment
    url: https://agents.example.com/a2a
    auth_type: bearer       # …with no secret to send
$ aisix validate --resources resources.yaml
OK: resources.yaml loaded 1 resource(s)

Both problems are real: a slash splits the agent's route into two segments, and bearer with no secret means the gateway authenticates upstream with nothing. The struct comments said this was deliberate — "this gateway's own Admin API re-checks it on write" — but that design leans on a write path that is about to be removed.

What is lifted

Twelve rules, defined once and injected into the generated schema following the existing model_one_of / mcp_policy_root_schema pattern, so the published JSON Schema and every runtime validator share one definition:

Rule Why it matters
a2a name excludes /?#%, whitespace, control chars the name is interpolated into the advertised agent-card URL unencoded (a2a.rs:237), so a?b advertises a URL whose path is just /a2a/a
mcp name excludes __ and a trailing _ tools are advertised as <name>__<tool> and parsed with split_once("__"); github_+listgithub___list → resolves to the non-existent server github, and gh_+x collides with gh+_x
a2a/mcp bearer/api_key ⇒ non-empty secret otherwise the gateway sends an empty credential
mcp oauth2secret + client_id + token_url incomplete credential exchange fails at first tool call instead of at load
mcp spec required for, and limited to, type: openapi; must be an object; must not be Swagger 2.0
mcp api_key_header limited to type: openapi + auth_type: api_key; must be a legal RFC 7230 header name

Cross-field coupling is an injected allOf of if/then subschemas, so the resources stay flat — no oneOf restructuring, which the original comments gave as the reason for leaving the rules out.

Not lifted (3 rules)

aisix_mcp::validate_spec, the http::HeaderName typing, and duplicate-tool-name detection all need to parse the OpenAPI document, and aisix-mcp depends on aisix-core — lifting them would be a dependency cycle. They stay on the write path for now; their disposition belongs to the removal PR. (The header-name shape is covered by the RFC 7230 pattern above; only the exact HeaderName parse is not.)

⚠️ Behavior changes

One assertion is deliberately inverted. mcp_server_schema_stays_permissive_on_credential_coupling asserted that an incomplete oauth2 row still validates, so the loader keeps it and the runtime degrades that server gracefully. That reasoning depended on a write path existing to catch it. Rejecting at load is also the more diagnosable failure — a rejected row is named in GET /status/config's rejected[], whereas a loaded-but-degraded server silently serves no tools. The rationale is recorded at the test.

Upgrade impact differs by configuration source:

  • etcd: a now-invalid row is dropped by the loader and named in /status/config's rejected[]; the gateway keeps serving (degraded).
  • resources.yaml: the file source is all-or-nothing (filesource/status.rs:6). A single now-invalid a2a_agents/mcp_servers entry rejects the whole file, so a gateway that boots with one exits at startup (aisix-server/src/main.rs:493) rather than starting degraded. Run aisix validate --resources <file> before rolling the image. On SIGHUP the last-good snapshot is retained and the entry appears in rejected[].

One diagnosability regression, accepted and recorded at the constraint. Rejecting Swagger 2.0 in the schema loses the write path's targeted "convert the spec to OpenAPI 3.x" hint — a validator reports a pointer and a constraint, not advice. The rule is intact on every path. A post-schema semantic hook in the loaders would let the rule and the advice live together; worth doing, out of scope here.

Also

The write handlers' own checks now sit behind the schema gate, so their tests observe AdminError::Schema instead of BadRequest. Both map to 400 (error.rs:46), so the wire contract is unchanged; those assertions now check the status and the failing pointer. Those handler branches are consequently unreachable — they are the thing being lifted, and the removal PR deletes them.

Verification

cargo fmt, clippy --workspace --all-targets clean, full workspace tests pass, schemas regenerated so the drift gate passes.

Every rule was then exercised against a locally built aisix validate, not just unit tests — which mattered: the first draft of the api_key_header coupling used dependentSchemas (draft 2019-09) in a schema declaring draft-07, so the validator silently ignored it while every unit test passed. Running the nine documents through the binary caught it. It is now dependencies, with a regression test that says why.

Summary by CodeRabbit

  • New Features

    • Added stricter validation for agent and MCP server names.
    • Added credential requirements based on authentication type.
    • Added validation for MCP server configuration fields, including OpenAPI settings and API key headers.
  • Bug Fixes

    • Exports now fail when rejected entries would result in incomplete resource files.
    • Standardized invalid configuration responses to HTTP 400.
  • Documentation

    • Updated validation guidance and error-handling comments.

…fig path

The per-`auth_type` credential coupling and the name-shape rules lived
only in this gateway's Admin API write handlers. Declaratively configured
gateways never saw them, so a `resources.yaml` like

    a2a_agents:
      - name: bad/name
        url: https://agents.example.com/a2a
        auth_type: bearer

loaded clean on main (`OK: loaded 1 resource(s)`) even though the name is
the `/a2a/<name>` path segment and `bearer` has no secret to send.

Lifts four rules into the canonical schemas, following the
`model_one_of` pattern — defined once, injected into the generated
schema, so the published JSON Schema and every runtime validator share
one definition:

- a2a agent `name` must not contain `/` (it is the URL path segment)
- mcp server `name` must not contain `__` (tools are exposed as
  `<name>__<tool>`, so the separator would make the split ambiguous)
- a2a `bearer`/`api_key` require a non-empty `secret`
- mcp `bearer`/`api_key` require `secret`; `oauth2` additionally
  requires `client_id` and `token_url`

One assertion is deliberately inverted:
`mcp_server_schema_stays_permissive_on_credential_coupling` asserted
that an incomplete oauth2 row still validates, on the reasoning that the
write path would catch it and the runtime would degrade that server
gracefully. That reasoning depends on a write path existing. Rejecting at
load is also the more diagnosable failure — a rejected row is named in
`GET /status/config`'s `rejected` array, where a loaded-but-degraded
server silently serves no tools. The rationale is recorded at the test.

The write handlers' own checks now sit behind the schema gate, so their
tests see `AdminError::Schema` instead of `BadRequest`. Both map to 400,
so the wire contract is unchanged; those assertions now check the status.

Verified with a locally built `aisix validate`: each bad document is
rejected with a precise pointer, and valid ones — including a trailing
single underscore in an mcp name — still load.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds conditional credential validation and stricter identifier patterns for A2A agents and MCP servers, wires these rules into generated and published schemas, updates admin tests to assert 400 Bad Request, and makes exports fail when entries are rejected.

Changes

Schema validation

Layer / File(s) Summary
Model constraints and credential rules
crates/aisix-core/src/models/a2a_agent.rs, crates/aisix-core/src/models/mcp_server.rs
A2A and MCP model schemas add identifier patterns and public credential-coupling helpers with validation tests.
Root and published schema integration
crates/aisix-core/src/models/schema.rs, schemas/resources/*.schema.json
Generated and published schemas enforce authentication-specific credentials, OpenAPI field coupling, and identifier formats.
Admin rejection status assertions
crates/aisix-admin/src/*_handlers.rs
Handler tests assert 400 Bad Request for schema-rejected names, credentials, and OpenAPI configurations.

Export integrity

Layer / File(s) Summary
Rejected export entries fail the run
crates/aisix-server/src/export/mod.rs
Export execution now exits with an error when rejected entries are omitted from the resources file.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdminRequest
  participant decode
  participant RootSchema
  participant JSONSchemaValidator
  AdminRequest->>decode: submit agent or MCP configuration
  decode->>RootSchema: validate configuration
  RootSchema->>JSONSchemaValidator: apply name and credential rules
  JSONSchemaValidator-->>decode: return validation result
  decode-->>AdminRequest: return 400 Bad Request on rejection
Loading

Suggested reviewers: jarvis9443, kayx23

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
E2e Test Quality Review ⚠️ Warning Only unit/schema-handler tests cover the new A2A/MCP rules; no black-box E2E validates bootstrap/status/export behavior for those cases. Add at least one E2E case that loads a bad resources.yaml or etcd entry and asserts the new invalid-name/secret/openapi coupling is rejected end-to-end.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the core change: enforcing A2A/MCP name and credential rules across configuration paths.
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.
Security Check ✅ Passed No new sensitive logging, secret persistence, auth bypass, or ownership/TLS issues were introduced; schema errors are masked before reaching logs or 400 bodies.
✨ 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 fix/lift-a2a-mcp-validation

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/aisix-admin/src/mcp_servers_handlers.rs (1)

95-132: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Dead code: schema validation on line 96 already subsumes these checks.

validate_mcp_server(raw)? now enforces both the __-in-name rule and the per-auth_type credential coupling via the injected schema allOf. Since it runs first and returns early on any violation, lines 99-132 can never execute for the cases they guard — the test module's own comment above (lines 200-205) confirms "the schema gate now rejects these payloads before decode's own checks run." Leaving this in place is dead code and a second, now-divergence-prone source of truth for the same rule.

🧹 Proposed removal of now-unreachable checks
     let server: McpServer = serde_json::from_value(raw.clone())
         .map_err(|e| AdminError::BadRequest(format!("malformed McpServer payload: {e}")))?;
-    if server.name.contains(TOOL_NAMESPACE_SEPARATOR) {
-        return Err(AdminError::BadRequest(format!(
-            "name must not contain the reserved separator `{TOOL_NAMESPACE_SEPARATOR}`"
-        )));
-    }
-    // Per-auth_type credential coupling. The JSON schema stays flat and
-    // permissive on this (see the note on the McpServer struct); the write
-    // path is where an incomplete credential set is rejected outright.
-    let has_secret = !server.secret.as_deref().unwrap_or_default().is_empty();
-    match server.auth_type {
-        McpAuthType::None => {}
-        McpAuthType::Bearer if !has_secret => {
-            return Err(AdminError::BadRequest(
-                "secret is required and must be non-empty when auth_type is `bearer`".to_string(),
-            ));
-        }
-        McpAuthType::ApiKey if !has_secret => {
-            return Err(AdminError::BadRequest(
-                "secret is required and must be non-empty when auth_type is `api_key`".to_string(),
-            ));
-        }
-        McpAuthType::OAuth2 => {
-            let has_client_id = !server.client_id.as_deref().unwrap_or_default().is_empty();
-            let has_token_url = !server.token_url.as_deref().unwrap_or_default().is_empty();
-            if !has_secret || !has_client_id || !has_token_url {
-                return Err(AdminError::BadRequest(
-                    "client_id, token_url, and secret (the OAuth client secret) are required \
-                     and must be non-empty when auth_type is `oauth2`"
-                        .to_string(),
-                ));
-            }
-        }
-        McpAuthType::Bearer | McpAuthType::ApiKey => {}
-    }
     // Per-type coupling: an openapi-backed server must carry a spec that

Note: verify TOOL_NAMESPACE_SEPARATOR isn't left unused elsewhere in this file after removal.

🤖 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-admin/src/mcp_servers_handlers.rs` around lines 95 - 132, Remove
the redundant name-separator and auth_type credential checks from decode after
validate_mcp_server(raw)?; retain deserialization and error mapping. Verify
TOOL_NAMESPACE_SEPARATOR and any related imports or helpers remain used
elsewhere, removing unused ones if necessary.
crates/aisix-core/src/models/mcp_server.rs (1)

84-91: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Stale comment contradicts the credential-coupling function added right below it.

This comment still says the oauth2/bearer/api_key coupling "is deliberately NOT expressed in this flat schema" and is enforced by "the control plane... this gateway's own Admin API... at write time." mcp_server_credential_coupling() (added a few lines below in this same PR) now expresses exactly this coupling in the schema via allOf/if/then, and per the admin test suite the Admin API write-path checks are now dead code the schema gate preempts (see the linked comment in mcp_servers_handlers.rs). This is the same doc drift the sibling a2a_agent.rs comment was correctly updated to avoid.

📝 Proposed comment update (mirrors a2a_agent.rs's updated wording)
-    // Cross-field coupling (`oauth2` requires `client_id` + `secret` +
-    // `token_url`; `bearer`/`api_key` require `secret`) is deliberately NOT
-    // expressed in this flat schema — that would force restructuring the
-    // resource into a oneOf. The control plane enforces the coupling strictly
-    // at write time, this gateway's own Admin API re-checks it on write, and
-    // the runtime degrades gracefully when a snapshot-loaded server is
-    // mis-configured: its credential exchange fails, its tools become
-    // unavailable, and the failure is logged like any other upstream failure.
+    // Cross-field coupling (`oauth2` requires `client_id` + `secret` +
+    // `token_url`; `bearer`/`api_key` require `secret`) is expressed as an
+    // injected `allOf` of `if`/`then` subschemas rather than in this flat
+    // struct — see `mcp_server_credential_coupling`. That keeps the resource
+    // flat (no oneOf restructuring) while giving the published schema and
+    // every runtime validator one shared definition, so a declarative
+    // `resources.yaml` and the control plane reject the same documents.
🤖 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_server.rs` around lines 84 - 91, Update the
comment immediately above mcp_server_credential_coupling() to describe that
oauth2, bearer, and api_key credential requirements are now enforced by the
schema’s conditional allOf/if/then rules. Remove the outdated claims that
coupling is intentionally absent and primarily enforced by write-time
control-plane or Admin API checks, while retaining the runtime misconfiguration
behavior if still applicable.
🧹 Nitpick comments (1)
crates/aisix-core/src/models/a2a_agent.rs (1)

141-158: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated then block could be factored out like the MCP sibling.

mcp_server_credential_coupling() extracts the repeated {"required": ["secret"], "properties": {...}} into a secret_required variable; this function inlines the same block twice. Minor stylistic inconsistency across the two nearly-identical functions added in this PR.

♻️ Optional dedup
 pub fn a2a_agent_credential_coupling() -> Value {
+    let secret_required = json!({
+        "required": ["secret"],
+        "properties": { "secret": { "type": "string", "minLength": 1 } }
+    });
     json!([
         {
             "if": { "properties": { "auth_type": { "const": "bearer" } }, "required": ["auth_type"] },
-            "then": {
-                "required": ["secret"],
-                "properties": { "secret": { "type": "string", "minLength": 1 } }
-            }
+            "then": secret_required
         },
         {
             "if": { "properties": { "auth_type": { "const": "api_key" } }, "required": ["auth_type"] },
-            "then": {
-                "required": ["secret"],
-                "properties": { "secret": { "type": "string", "minLength": 1 } }
-            }
+            "then": secret_required
         }
     ])
 }
🤖 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/a2a_agent.rs` around lines 141 - 158, Refactor
a2a_agent_credential_coupling() to extract the repeated secret requirement and
string validation object into a shared secret_required value, then reuse it in
both bearer and api_key then clauses. Preserve the existing validation behavior
and structure.
🤖 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.

Outside diff comments:
In `@crates/aisix-admin/src/mcp_servers_handlers.rs`:
- Around line 95-132: Remove the redundant name-separator and auth_type
credential checks from decode after validate_mcp_server(raw)?; retain
deserialization and error mapping. Verify TOOL_NAMESPACE_SEPARATOR and any
related imports or helpers remain used elsewhere, removing unused ones if
necessary.

In `@crates/aisix-core/src/models/mcp_server.rs`:
- Around line 84-91: Update the comment immediately above
mcp_server_credential_coupling() to describe that oauth2, bearer, and api_key
credential requirements are now enforced by the schema’s conditional
allOf/if/then rules. Remove the outdated claims that coupling is intentionally
absent and primarily enforced by write-time control-plane or Admin API checks,
while retaining the runtime misconfiguration behavior if still applicable.

---

Nitpick comments:
In `@crates/aisix-core/src/models/a2a_agent.rs`:
- Around line 141-158: Refactor a2a_agent_credential_coupling() to extract the
repeated secret requirement and string validation object into a shared
secret_required value, then reuse it in both bearer and api_key then clauses.
Preserve the existing validation behavior and structure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4369a94c-29a6-4692-9c69-9ca60a547f84

📥 Commits

Reviewing files that changed from the base of the PR and between 22b8353 and d80477b.

📒 Files selected for processing (7)
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-core/src/models/a2a_agent.rs
  • crates/aisix-core/src/models/mcp_server.rs
  • crates/aisix-core/src/models/schema.rs
  • schemas/resources/a2a_agent.schema.json
  • schemas/resources/mcp_server.schema.json

moonming added 2 commits July 30, 2026 16:39
…trailing `_`

Audit findings on the previous commit.

The mcp name pattern was wrong, and the previous commit message argued
for the wrong side of it. A trailing `_` is not cosmetic: tools are
advertised as `<name>__<tool>` and parsed back with `split_once("__")`,
so `github_` + `list` serializes to `github___list` and resolves to the
non-existent server `github` — every tool call against that server
fails. Worse, `gh_` + `x` and `gh` + `_x` produce the same wire name,
which is the ambiguity the rule exists to prevent. Pattern reverted to
reject both `__` and a trailing `_`, with the reasoning recorded.

Eight more write-handler rules were left behind, not one. All eight are
plain JSON Schema and are now lifted: `spec` is required for and limited
to `type: openapi`, must be an object, and must not be a Swagger 2.0
document; `api_key_header` is limited to `type: openapi` with
`auth_type: api_key` and must be a legal RFC 7230 header name. Only the
three rules that need to parse the OpenAPI document itself
(`aisix_mcp::validate_spec`, header-name typing, duplicate tool names)
remain, blocked by the aisix-mcp → aisix-core dependency direction.

The first draft of that lift silently did nothing: it used
`dependentSchemas`, a draft 2019-09 keyword, in a schema that declares
draft-07, so the validator ignored it as unknown. Unit tests passed.
Caught by running each case through a built `aisix validate`; now spelled
`dependencies` and covered by a regression test that says why.

Also from the audit:

- the a2a name pattern only excluded `/`, but the name is interpolated
  into the advertised agent-card URL unencoded, so `a?b` advertises a URL
  whose path is `/a2a/a`. Now excludes `/?#%`, whitespace and control
  characters
- four comments asserted the schema stays permissive, directly above the
  code that now enforces it
- restored the `client_id` doc comment an earlier comment rewrite
  swallowed, and titled the new `anyOf` branches for the ReDoc tabs
- `decode_openapi_type_coupling` asserted on handler-authored messages
  that the schema gate now pre-empts; it asserts the 400 and the failing
  pointer instead

One diagnosability regression is accepted and recorded at the
constraint: rejecting Swagger 2.0 in the schema loses the write path's
"convert the spec to OpenAPI 3.x" hint, because a validator reports a
pointer and a constraint, not advice. A post-schema semantic hook in the
loaders would let the rule and the advice live together.
… rule

Findings from a second review pass focused on lifecycle, export round-trip
and mutation coverage.

`aisix export` omitted rows the loader rejected, printed a warning, and
exited 0. Export is the migration path off a store-backed deployment, so
a scripted migration would treat an incomplete file as a finished one and
lose those resources silently. A rejected row now fails the export the
same way a non-loadable file does; the written file stays for inspection.

`api_key_header: null` was rejected. The field is `Option<String>` and
an explicit null means absent — the comment right above the constraint
said so — but `dependencies` keys off presence alone, null included. It
is now a value-sensitive `if`/`then` on `type: string`, verified both
ways: null loads, a real header with non-api_key auth still fails.

The suite pinned only 5 of the 12 lifted rules at single-constraint
granularity. `spec` must-be-an-object was pinned by nothing — deleting it
left the string fixture rejected by the sibling swagger check, so the
test passed either way. Added mutation-pinning tests that start from a
fully valid document and violate exactly one obligation: each oauth2
field independently, empty and null for every credential, an explicit
`type: mcp` carrying openapi-only fields, a header with
`auth_type: none`, and every excluded a2a character under both the `name`
and `display_name` spellings.

One test fixture called `{"openapi": "3.0.0"}" a complete spec, but tool
generation needs a `paths` object — it now uses a spec that generates a
tool.
@moonming

Copy link
Copy Markdown
Collaborator Author

Second review pass — findings and disposition

A second reviewer went at this from angles the first pass did not cover: revert-fails-test (would deleting each constraint actually fail a test), the full ingestion lifecycle, export round-trip, and upgrade hazard. It found two things the first pass missed. I reproduced every claim before acting on it.

Fixed in 9d79dbc

aisix export silently dropped newly-invalid rows and exited 0. Export is the migration path off a store-backed deployment. A row the loader rejects is absent from the output, the file still loads, so document.blocking is empty and the command succeeds — a scripted migration treats an incomplete file as finished and loses those resources. Rejected rows now fail the export the same way a non-loadable file does; the written file remains for inspection.

api_key_header: null was rejected. The field is Option<String> and an explicit null means absent — the comment directly above my own constraint said exactly that — but dependencies triggers on key presence, null included. Now a value-sensitive if/then on type: string. Verified both directions: null loads; a real header with non-api_key auth still fails.

Only 5 of the 12 rules were pinned at single-constraint granularity. spec must-be-an-object was pinned by nothing at all: deleting it left the string fixture rejected by the sibling swagger check, so its test passed either way. Added mutation-pinning tests that start from a fully valid document and violate exactly one obligation — each oauth2 field independently, empty and null for every credential, an explicit type: mcp carrying openapi-only fields, a header with auth_type: none, and every excluded a2a character under both the name and display_name spellings.

A fixture called {"openapi": "3.0.0"} a complete spec, but tool generation needs a paths object; it now uses a spec that actually generates a tool.

Confirmed, no action needed

The canonical schema does run on both supported ingestion paths — resources.yaml (filesource/mod.rs:403) and etcd/kine including initial load, cache restore, watch PUT and resync (loader.rs:123/249/312) — and aisix export goes through build_snapshot, so it does not bypass validation either.

⚠️ Open question for the maintainer

The review flagged that this PR mixes two different things:

  • lifting the rules the old write handlers already enforced (__ for mcp, / for a2a) — no upgrade risk, purely moving enforcement to where both paths see it;
  • tightening beyond them (trailing _, and ?#%/whitespace for a2a) — the old handlers accepted these, so an existing deployment may legitimately hold such a row, created through the Admin API. In file mode the load is all-or-nothing, so such a gateway would fail to start after the upgrade.

I verified the premise against the base commit: 22b8353 mcp checked only contains("__"), a2a only contains('/').

Three ways forward:

  1. Ship as-is with a breaking-changes note. gh_ is already broken at runtime — every tool call against it resolves to the wrong server — and a?b breaks the advertised agent-card URL. Keeping them loadable only defers the failure, and aisix validate --resources <file> detects them before an upgrade.
  2. Narrow this PR to strict parity with the old handlers, and do the extra tightening in its own PR with a migration story.
  3. Two-phase: report-only first, hard rejection a release later.

I lean 1 and the PR is written that way, but it is a compatibility call rather than a technical one — happy to switch to 2 or 3.

@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: 2

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

412-414: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale comment: implementation no longer uses dependencies.

This comment says the api_key_header/auth_type coupling is "guarded by draft-07 dependencies", but the actual schema (mcp_server_credential_coupling, lines 252-263) uses nested allOf/if/then, not the dependencies keyword. This looks like a leftover from an earlier iteration (per the PR's commit history) that was superseded by the value-sensitive if/then fix for api_key_header: null.

♻️ Suggested comment update
-        // `api_key_header` only makes sense with `auth_type: api_key`, and has to
-        // be a legal header name. This one is guarded by draft-07 `dependencies`
-        // — the draft 2019-09 spelling would be ignored, so keep this test.
+        // `api_key_header` only makes sense with `auth_type: api_key`, and has to
+        // be a legal header name. This is guarded by a nested `allOf`/`if`/`then`
+        // (not `dependencies`, which is presence-based and would incorrectly
+        // reject an explicit `api_key_header: null`); keep this test.
🤖 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_server.rs` around lines 412 - 414, Update
the comment near the MCP server schema test to describe the current nested
allOf/if/then coupling in mcp_server_credential_coupling, including its
value-sensitive handling of api_key_header: null; remove the obsolete references
to draft-07 dependencies and its spelling.
crates/aisix-admin/src/mcp_servers_handlers.rs (1)

104-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Stale comment: schema is no longer "flat and permissive" on credential coupling.

This comment says the JSON schema stays permissive and the write path enforces credential/type coupling. That's no longer accurate — mcp_server.rs's mcp_server_credential_coupling() (wired into the schema in schema.rs) now rejects incomplete credentials and openapi-only-field violations before validate_mcp_server(raw)? on line 96 even lets execution reach these checks. The updated test-module comment (lines 200-205) already reflects this; this comment in decode() doesn't.

Most of lines 107-181 are effectively dead/unreachable now (the schema already rejects those payloads), except the aisix_mcp-dependent parts (spec parsing, duplicate-tool detection) that must stay on the write path per the PR's design. Consider updating the comment and/or trimming the now-redundant per-field checks to avoid future drift between this function and the schema.

🤖 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-admin/src/mcp_servers_handlers.rs` around lines 104 - 181,
Update the credential-coupling comment in decode() to reflect that
mcp_server_credential_coupling() in the schema already rejects incomplete
credentials and openapi-only fields before these checks run. Remove the
redundant per-authentication and field-coupling validation from decode(), while
retaining aisix_mcp::validate_spec and other write-path checks that require
OpenAPI parsing or tool-generation validation.
🤖 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-core/src/models/a2a_agent.rs`:
- Around line 29-38: Update the regex pattern on the A2A agent name field in the
surrounding serde/schemars attributes to also reject DEL (U+007F), while
preserving the existing exclusions for separators, percent signs, whitespace,
and C0 controls.

In `@crates/aisix-server/src/export/mod.rs`:
- Around line 107-113: Update the export error flow around stats.rejections and
document.blocking so both rejection and blocking-issue diagnostics are reported
before returning failure. Move the rejection bail after the existing
blocking-reporting block, or otherwise aggregate both reports while preserving
one non-zero result and the separate handling of blocking export issues.

---

Nitpick comments:
In `@crates/aisix-admin/src/mcp_servers_handlers.rs`:
- Around line 104-181: Update the credential-coupling comment in decode() to
reflect that mcp_server_credential_coupling() in the schema already rejects
incomplete credentials and openapi-only fields before these checks run. Remove
the redundant per-authentication and field-coupling validation from decode(),
while retaining aisix_mcp::validate_spec and other write-path checks that
require OpenAPI parsing or tool-generation validation.

In `@crates/aisix-core/src/models/mcp_server.rs`:
- Around line 412-414: Update the comment near the MCP server schema test to
describe the current nested allOf/if/then coupling in
mcp_server_credential_coupling, including its value-sensitive handling of
api_key_header: null; remove the obsolete references to draft-07 dependencies
and its spelling.
🪄 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: 56395e86-2456-467b-8a64-54150859e507

📥 Commits

Reviewing files that changed from the base of the PR and between d80477b and 9d79dbc.

📒 Files selected for processing (8)
  • crates/aisix-admin/src/a2a_agents_handlers.rs
  • crates/aisix-admin/src/mcp_servers_handlers.rs
  • crates/aisix-core/src/models/a2a_agent.rs
  • crates/aisix-core/src/models/mcp_server.rs
  • crates/aisix-core/src/models/schema.rs
  • crates/aisix-server/src/export/mod.rs
  • schemas/resources/a2a_agent.schema.json
  • schemas/resources/mcp_server.schema.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/aisix-admin/src/a2a_agents_handlers.rs

Comment on lines +29 to +38
/// be a single non-empty URL path segment. The name is interpolated into the
/// advertised agent-card URL without percent-encoding, so `/`, `?`, `#`, `%`
/// and whitespace are rejected: `a?b` would advertise a URL whose path is
/// just `/a2a/a`, and the lookup is an exact match on the stored name.
// `display_name` is the field's former name; stored documents and
// callers that still use it keep deserializing (schema-side acceptance
// lives in `schema::a2a_agent_root_schema`). Re-serialization always
// emits `name`.
#[serde(alias = "display_name")]
#[schemars(length(min = 1))]
#[schemars(regex(pattern = "^[^/?#%\\s\\x00-\\x1f]+$"), length(min = 1))]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Regex omits DEL (0x7F), a control character.

The pattern ^[^/?#%\s\x00-\x1f]+$ only excludes C0 controls (0x00–0x1f). DEL (U+007F) is also a control character (Unicode category Cc) but isn't blocked here, so a name containing DEL would still validate despite the stated goal of rejecting "control characters" generically.

🐛 Proposed fix
-    #[schemars(regex(pattern = "^[^/?#%\\s\\x00-\\x1f]+$"), length(min = 1))]
+    #[schemars(regex(pattern = "^[^/?#%\\s\\x00-\\x1f\\x7f]+$"), length(min = 1))]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// be a single non-empty URL path segment. The name is interpolated into the
/// advertised agent-card URL without percent-encoding, so `/`, `?`, `#`, `%`
/// and whitespace are rejected: `a?b` would advertise a URL whose path is
/// just `/a2a/a`, and the lookup is an exact match on the stored name.
// `display_name` is the field's former name; stored documents and
// callers that still use it keep deserializing (schema-side acceptance
// lives in `schema::a2a_agent_root_schema`). Re-serialization always
// emits `name`.
#[serde(alias = "display_name")]
#[schemars(length(min = 1))]
#[schemars(regex(pattern = "^[^/?#%\\s\\x00-\\x1f]+$"), length(min = 1))]
/// be a single non-empty URL path segment. The name is interpolated into the
/// advertised agent-card URL without percent-encoding, so `/`, `?`, `#`, `%`
/// and whitespace are rejected: `a?b` would advertise a URL whose path is
/// just `/a2a/a`, and the lookup is an exact match on the stored name.
// `display_name` is the field's former name; stored documents and
// callers that still use it keep deserializing (schema-side acceptance
// lives in `schema::a2a_agent_root_schema`). Re-serialization always
// emits `name`.
#[serde(alias = "display_name")]
#[schemars(regex(pattern = "^[^/?#%\\s\\x00-\\x1f\\x7f]+$"), length(min = 1))]
🤖 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/a2a_agent.rs` around lines 29 - 38, Update the
regex pattern on the A2A agent name field in the surrounding serde/schemars
attributes to also reject DEL (U+007F), while preserving the existing exclusions
for separators, percent signs, whitespace, and C0 controls.

Comment on lines +107 to +113
if !stats.rejections.is_empty() {
anyhow::bail!(
"export omitted {} rejected entr(ies); the resources file is incomplete. \
Fix them in the source and re-export — the written file is for inspection only",
stats.rejections.len()
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not suppress blocking-issue diagnostics when rejections also exist.

If stats.rejections and document.blocking are both non-empty, this bail exits before the collision/dangling-reference details at Lines 118-125 are printed. Report both classes first, then return one non-zero result (or move this bail after the existing blocking-reporting block). This preserves the stated separate handling for blocking export issues.

🤖 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-server/src/export/mod.rs` around lines 107 - 113, Update the
export error flow around stats.rejections and document.blocking so both
rejection and blocking-issue diagnostics are reported before returning failure.
Move the rejection bail after the existing blocking-reporting block, or
otherwise aggregate both reports while preserving one non-zero result and the
separate handling of blocking export issues.

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