pub trait RequestPolicy: Send + Sync {
// Required method
fn check<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
req: &'life1 OutboundRequest<'life2>,
) -> Pin<Box<dyn Future<Output = Result<(), PolicyRefusal>> + Send + 'async_trait>>
where Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait;
}Expand description
A rule about what may LEAVE the server, consulted before every outbound backend request on both HTTP surfaces (Phase 128, E1 / D-12).
§Where it runs
Between the base-URL join and the auth application, on the curated
single-call surface (http::HttpClient::execute) and on the Code Mode /
script-tool surface (code_mode::HttpCodeExecutor::execute_request). The
position is what makes the D-12 guarantee structural: the request is fully
assembled, and the credential does not exist yet. A refusal returns before
the auth provider is called and before anything is sent.
§One invocation per LOGICAL request, not per wire attempt
The curated client’s send_with_retries retries the ALREADY-BUILT request up
to three times on a 5xx / connect / timeout. Those retries happen after the
hook, so this trait gives exactly one invocation per logical outbound request.
A policy counting requests against a rate budget is therefore counting LOGICAL
requests; it will under-count wire attempts.
§What E1 does and does not govern
It governs the two HTTP egress surfaces named above. It does NOT intercept SQL
connector traffic: a SqlConnector request is a statement plus bound
parameters, not a method/path/query, so it needs a different seam and a
different trait. A team writing a PHI policy must know that its coverage stops
at HTTP egress.
Redirects are governed by construction rather than by this trait: the
OpenAPI binary’s shared client is built with
reqwest::redirect::Policy::none(), so a redirect surfaces as a response the
caller handles rather than as a hop inside the client that this hook never
saw (T-128-39a). A client built elsewhere with reqwest’s default
redirect policy re-opens that gap — every hop after the first would be
invisible here.
§Concurrency and latency
check takes &self and the trait requires Send + Sync, so ONE instance
serves every concurrent request. Any per-session state is the
implementation’s own responsibility. check is async and the toolkit cannot
bound third-party code: a slow policy delays the request path (T-128-44,
accepted — the per-request time budget is a separate, deferred piece of work).
§Example
use pmcp_server_toolkit::{async_trait, OutboundRequest, PolicyRefusal, RequestPolicy};
struct AllowlistPrefix(&'static str);
#[async_trait]
impl RequestPolicy for AllowlistPrefix {
async fn check(&self, req: &OutboundRequest<'_>) -> Result<(), PolicyRefusal> {
if req.path.starts_with(self.0) {
Ok(())
} else {
// A FIXED message: it names the rule, never the request.
Err(PolicyRefusal::new("outbound endpoint is not on the allowlist"))
}
}
}Required Methods§
Sourcefn check<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
req: &'life1 OutboundRequest<'life2>,
) -> Pin<Box<dyn Future<Output = Result<(), PolicyRefusal>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
fn check<'life0, 'life1, 'life2, 'async_trait>(
&'life0 self,
req: &'life1 OutboundRequest<'life2>,
) -> Pin<Box<dyn Future<Output = Result<(), PolicyRefusal>> + Send + 'async_trait>>where
Self: 'async_trait,
'life0: 'async_trait,
'life1: 'async_trait,
'life2: 'async_trait,
Allow or refuse one outbound backend request.
§Errors
Return PolicyRefusal to refuse. The request is then never
authenticated and never sent, and the refusal’s message is surfaced to the
MCP client — so it must carry no byte of the rejected request.
Dyn Compatibility§
This trait is dyn compatible.
In older versions of Rust, dyn compatibility was called "object safety".