api7 / api7/aisix

bug: on /mcp the guardrail scan text and the mask surface are different texts — block, would_mask and mask disagree

Open
#1,027 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
157
Forks
32
Avg merge
1h 25m
Merged PRs (30d)
145

Description

Summary

On /mcp the guardrail scan (which decides block, and which monitor mode
previews as would_mask) and the guardrail rewrite (which actually masks)
run over different texts, in three different shapes. So a rule can block
without ever masking, mask without ever being able to block, and preview as
would-mask while enforcement does nothing — with no error and no signal in
telemetry.

The three texts

what text handed to the rules source
input scan — block + monitor would_mask params.arguments serialized with Value::to_string(): one compact JSON string, keys and punctuation included crates/aisix-proxy/src/mcp.rs
output scan — block + monitor would_mask the non-empty string leaves of result.content[].{text,description,title}, result.content[].resource.text and every leaf of result.structuredContent, joined with \n (falling back to result.to_string() when nothing was collected) collect_string_leaves, apply_output_guardrails
enforcement — mask each string leaf on its own, spliced back byte-wise rewrite_tool_arguments, apply_output_guardrails

Monitor mode inherits the scan text: MonitorGuardrail::probe_redaction
(crates/aisix-guardrails/src/build.rs) runs the redactor over "the hook's
scan text", not over the surface enforcement rewrites.

Consequences

  1. A rule anchored to a whole leaf (^…$) can mask but can never block.
    Enforcement offers it the bare leaf, so ^\d{4}$ masks. The scan offers it
    a joined/serialized blob, where ^ is the start of that blob, so the same
    pattern as action: block silently never fires. Block is the stricter
    action — the gap is fail-open.
  2. A rule that only matches across the serialized form blocks but masks
    nothing.
    Anything spanning a key and its value, the "," between two
    members, or the \n the output scan joins with, matches the scan and finds
    nothing to rewrite per-leaf.
  3. Monitor mode is unreliable in both directions. Case 2 previews
    would_mask hits and then masks nothing once enforced — the
    accepted-and-echoed-but-never-enforced shape. Case 1 previews nothing and
    then masks once enforced. Either way the documented staging workflow
    (monitor → observe → enforce) does not predict enforcement.
  4. Input and output disagree with each other: the input scan sees object
    keys, the output scan drops them.

Suggested direction

Derive the scan text from the same walk enforcement uses, so the two cannot
drift: run the walker with a recording probe that rewrites nothing (the
SegmentCollector in crates/aisix-proxy/src/redact.rs is the existing
template) and scan the collected slots. Then block, would_mask and mask
all see the same slots, and the only remaining choice is whether a rule is
allowed to match across slot boundaries — which becomes an explicit decision
instead of an accident of three call sites.

If that is too large, the minimum is to document the three shapes so an
operator can predict which of their rules works with which action.

Test

  • A ^…$ rule with action: block on both hooks must block (case 1).
  • A rule seeded with enforcement_mode: monitor must report would_mask
    counts equal to the counts the same rule produces when enforced (case 3).
  • Both directions, since the input and output scans differ.

Context

Found by the independent pre-merge audit of api7/aisix#1025. That PR is closed
— its own feature is no longer needed — but this divergence is independent of
it and is present on main today. Supersedes #1026, which described one
symptom of the same root cause from the (now withdrawn) key-context angle.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in crates/aisix-proxy/src/mcp.rs and follow collect_string_leaves, rewrite_tool_arguments, and apply_output_guardrails; compare their scan and rewrite paths with SegmentCollector in crates/aisix-proxy/src/redact.rs. Read MonitorGuardrail::probe_redaction in crates/aisix-guardrails/src/build.rs. Done means anchored block rules work on both hooks and monitor would_mask counts match enforced masking in both directions.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
backend-api-design, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.