pub fn validate_resolved_target(path: &str) -> Result<(), PlaceholderRefusal>Expand description
The D4 path-placeholder floor, re-exported from core pmcp.
There is exactly ONE implementation of these rules and it lives in
pmcp::server::schema_validation. This is a pub use, never a second copy
(Phase 128, Q2). Two reasons the home is core rather than here:
- The toolkit’s curated
httpbuild has nopmcp-code-modeedge and must not gain one (SC-1), so the shared rule cannot live in this crate. - This repo has a documented three-way-drift incident from a security rule that existed in more than one copy, so a second denylist is a prohibited shape rather than a style preference.
The re-export exists because D-09 obliges the SDK to publish the helper under
the name a third-party HttpExecutor implementor would look for. An
implementor whose template syntax is not OpenAPI’s {key} can call
pmcp_code_mode::validate_path_placeholder on each value it substitutes and
pmcp_code_mode::validate_resolved_path on the composed result — or
pmcp_code_mode::validate_resolved_target, which is that rule widened by the
single author-written ? separator, and is what ResolvedPath::from_checked
itself calls. Either reaches the
same rule the SDK itself applies before calling
HttpExecutor::execute_request. (Plain backticks, not an intra-doc link:
executor is gated on js-runtime and the link would not resolve in a
default-feature doc build.)
validate_resolved_path widened by the single author-written query
separator, for a composed path that may carry a ?.
Both HTTP surfaces compose a path template with resolved placeholder values
and then check the result. An operator may write a literal ? in the
template, so the composed string is a path AND a query — while
validate_resolved_path denies ? anywhere, deliberately, because a ?
arriving from a value silently changes the endpoint.
The narrowing is therefore: split at the FIRST ? and apply the unmodified
rule to each half. It lives HERE, beside the rule it widens, rather than at
either call site — the curated surface (HttpClient::check_composed_path)
and the Code Mode surface (ResolvedPath::from_checked) previously held one
copy each, which made this the one part of the floor that could drift
between them. One more sibling, never a second copy.
What the split does NOT relax:
- A SECOND
?is still refused: only the first is split off, so the query portion faces the unmodified rule, which denies?. - An empty query portion is still refused — a dangling
/x?is a trailing separator, the same class as a trailing/. - A
?reaching the composed string from a placeholder VALUE never gets here; the per-value floor has already refused it.
§Two inherited conservatisms, stated so they are not a surprise
Both come from applying the unmodified rule to the query half, and both are what the two former call-site copies already did — neither is new here.
%25is refused outright (it is what bounds the decode to a single pass), so a query carrying a percent-encoded percent sign is refused.- The query half also faces the rules about path SHAPE — no
//, no trailing/, no empty segment — becausevalidate_resolved_pathchecks bytes AND segment structure together. So/a?redirect=https://example.com,/a?b=//xand/a?b=1&c=x/are all refused. This is the one an operator actually hits: a query value holding a URL does not compose. Asserted byresolved_target_applies_path_segment_structure_to_the_query_tooso it cannot change silently. Relaxing it means splitting the byte floor from the segment-structure rules and applying only the former to the query half — deliberately NOT done here, because widening a security floor is a decision for its own change, not a side effect of de-duplicating two copies.
§Errors
The PlaceholderRefusal from validate_resolved_path. It is value-free:
it names the rule and the declared expectation, never a byte of the path.