Skip to main content

validate_resolved_target

Function validate_resolved_target 

Source
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:

  1. The toolkit’s curated http build has no pmcp-code-mode edge and must not gain one (SC-1), so the shared rule cannot live in this crate.
  2. 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.

  1. %25 is refused outright (it is what bounds the decode to a single pass), so a query carrying a percent-encoded percent sign is refused.
  2. The query half also faces the rules about path SHAPE — no //, no trailing /, no empty segment — because validate_resolved_path checks bytes AND segment structure together. So /a?redirect=https://example.com, /a?b=//x and /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 by resolved_target_applies_path_segment_structure_to_the_query_too so 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.