1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
//! Property arms for `pmcp::server::schema_validation` (Phase 128, SC-8).
//!
//! # Why this file exists at all, when plan 08 already wrote property arms
//!
//! Plan 08's arms live in `crates/pmcp-server-toolkit/`, and `make test-property`
//! **cannot reach them**: its selector is root-package-scoped
//! (`cargo test --features "full" -- --ignored property_`, `Makefile`), so a
//! toolkit arm is invisible to it no matter how it is named. Those arms are reached
//! by `make test-server-toolkit` instead, and that is fine — but it leaves the
//! CLAUDE.md ALWAYS/PROPERTY requirement discharged by a leg that does not measure
//! this phase. This file is the ROOT-package home that leg can select.
//!
//! # The `#[ignore]` marker is load-bearing, not decoration
//!
//! `make test-property` selects `--ignored property_`, so a `property_`-prefixed
//! test WITHOUT `#[ignore]` is filtered OUT of that run and contributes nothing.
//! `tests/property_tests.rs` is the cautionary case: 19 `property_*` functions,
//! zero `#[ignore]` attributes, and therefore zero of them selected — which is why
//! the leg measured only 3 tests across the whole root package before this file
//! (MEASURED 2026-09-27: `tests/log_emitter.rs` 2 + `tests/typed_tool_garde.rs` 1;
//! `128-RESEARCH.md` Finding 9b recorded 2, which was one short). The marker string
//! below is copied VERBATIM from `tests/log_emitter.rs` so the convention has one
//! spelling.
//!
//! # Why this arm may use the STRICT no-echo oracle
//!
//! `fuzz/fuzz_targets/fuzz_input_schema_enforcement.rs` deliberately does NOT
//! assert "the refusal contains no substring of the instance" — for a fuzzer that
//! oracle produces false failures, because a DECLARED property name is
//! legitimately echoed and an independently generated schema and instance can
//! share strings by coincidence. It uses a provenance sentinel instead.
//!
//! Here the strict form IS sound, and the reason is worth stating so the two are
//! not later "harmonized" in the wrong direction: a property generator controls
//! BOTH sides. The declaration is drawn from `DECLARED_NAMES` (lowercase ASCII) and
//! the caller data from `CALLER_ALPHABET` (uppercase ASCII + digits), and the two
//! alphabets are DISJOINT — asserted in `alphabets_are_disjoint` below, so the
//! premise is checked rather than assumed. Provenance is therefore established by
//! construction, and "no byte of the caller's data appears in the refusal" is
//! exactly the phase prohibition stated at full strength:
//!
//! > A refusal message returned to an MCP client must never contain any byte of
//! > the rejected value, and must never contain an attacker-supplied argument key.
//!
//! Run with:
//!
//! ```bash
//! PROPTEST_CASES=256 cargo test -p pmcp --features "full" \
//! --test schema_validation_props -- --ignored property_
//! ```
use ;
use ;
/// Declared property names. Lowercase ASCII only — see the module header's
/// disjointness argument, which `alphabets_are_disjoint` checks.
const DECLARED_NAMES: = ;
/// The alphabet the variable tail of a generated caller string is drawn from.
const CALLER_ALPHABET: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
/// The fixed prefix every generated caller value and caller-chosen key carries.
///
/// A bare alphabet is NOT enough, and that was MEASURED rather than reasoned: the
/// first draft of this file generated caller strings from `CALLER_ALPHABET` alone,
/// and proptest immediately found `key_seed = [32], bound = 6` — a one-character key
/// `"6"` that IS a substring of the serialized schema, because the schema declares
/// `"maxLength":6`. That is precisely the coincidence
/// `fuzz/fuzz_targets/fuzz_input_schema_enforcement.rs`'s header names as the third
/// way a naive absence oracle produces false failures, reproduced inside this file's
/// own generator on the first run.
///
/// Prefixing every caller string with characters that cannot appear in any schema
/// this file builds removes the coincidence by construction, rather than by
/// weakening the assertion — which is the move the whole phase is about.
const CALLER_PREFIX: &str = "CALLER";
/// The premise the strict oracle rests on, checked rather than assumed.
///
/// Not `property_`-prefixed and not `#[ignore]`d on purpose: it must run on every
/// ordinary `cargo test` too. Two things are asserted:
///
/// 1. no declared name shares a character with `CALLER_ALPHABET`;
/// 2. `CALLER_PREFIX` does not appear as a SUBSTRING of ANY schema this file can
/// build, over every `(shape, bound, closed)` combination the generator draws
/// from. Since every generated caller string BEGINS with that prefix, this is
/// exactly what makes a caller string unable to be a substring of the
/// declaration.
///
/// SUBSTRING, not per-character, and the first draft got that wrong in a way worth
/// recording: a per-character check fails on the letter `L`, because the JSON Schema
/// keyword `maxLength` contains one. Individual characters coinciding is harmless;
/// the whole prefixed string coinciding is what would turn a coincidence into a
/// reported leak. Over-strict premises get relaxed wholesale rather than corrected,
/// so the granularity matters.
///
/// (2) is what makes the coincidence impossible rather than merely unlikely. If a
/// later edit adds a lowercase letter to `CALLER_ALPHABET`, or puts the prefix into
/// a schema, the property arm below would start producing false failures and someone
/// would relax it. This test fails first, and says why.
/// Build one of four declared-schema shapes, each exercising a different arm of
/// the refusal renderer.
///
/// `shape` selects; `bound` supplies the numeric limit. Every string emitted is
/// drawn from `DECLARED_NAMES` or the JSON Schema vocabulary, never from
/// `CALLER_ALPHABET`.
/// Render a caller string: the fixed prefix, then a tail from the disjoint
/// alphabet. See [`CALLER_PREFIX`] for why the prefix is not optional.
/// SC-8 / SC-7: no byte of the caller's data reaches the refusal, over generated
/// (schema shape, value, undeclared key) triples rather than over fixtures.
///
/// Four arms of the renderer are reached (`maxLength`, `pattern`, `enum`,
/// `minimum`/`type`) and both envelope shapes (`additionalProperties` true and
/// false), so the `additionalProperties` message — the one that legitimately lists
/// ALLOWED names, and must never list the offending one — is covered too.
///
/// Both surfaces the caller's data can escape through are asserted, not just the
/// rendered string: `InputViolation::pointer` and `InputViolation::expected` are
/// PUBLIC fields that reach logs, execution records and third-party renderers.
/// Plan 02 recorded that two of its own fixture rows would have passed vacuously
/// had they asserted only on the rendered message, because `render_one` suppresses
/// an undeclared pointer as defence in depth and thereby masks a leak in the field
/// itself. This arm does not repeat that mistake.
///
/// The `#[ignore]` marker is what makes `make test-property` select this. See the
/// module header.