bug: on /mcp the guardrail scan text and the mask surface are different texts — block, would_mask and mask disagree
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
- 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 asaction: blocksilently never fires. Block is the stricter
action — the gap is fail-open. - 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\nthe output scan joins with, matches the scan and finds
nothing to rewrite per-leaf. - Monitor mode is unreliable in both directions. Case 2 previews
would_maskhits 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. - 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 withaction: blockon both hooks must block (case 1). - A rule seeded with
enforcement_mode: monitormust reportwould_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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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