#[non_exhaustive]pub struct OutboundRequest<'a> {
pub tool: &'a str,
pub method: &'a str,
pub path: &'a str,
pub query: &'a [(String, String)],
pub body: Option<&'a Value>,
pub call_id: &'a str,
pub phase: RequestPhase,
}Expand description
One outbound backend request, as it will be sent, handed to a
RequestPolicy for inspection.
§The D-12 guarantee, stated as a guarantee
This struct has NO credential-bearing field, and it is constructed BEFORE the
HttpAuthProvider runs on either HTTP surface. A policy implementation
therefore cannot observe an outgoing credential — not because it is asked not
to, but because the value it is handed was assembled before the credential
existed. tests/request_policy.rs’s credential-scan row asserts this by
searching every field for the configured secret.
Borrowed throughout (&'a str, &'a [_]): a server with no registered policy
never constructs one, so the empty case adds no allocation.
Fields (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.tool: &'a strThe MCP tool whose tools/call produced this request.
Both shipped surfaces name a tool: the curated single-call surface passes
the synthesized tool’s own name, and the Code Mode surface passes the
label attached to the executor at synthesis (a script tool’s [[tools]]
name, or execute_code for the generic Code Mode tool). On the Code
Mode surface ONE tools/call may produce many outbound requests, and all
of them carry that same label.
Empty ONLY when a caller drives a connector directly rather than through a
synthesized handler — there is then no tool to name. A policy that keys on
the tool name should treat the empty string as “unattributed”, never as a
tool called "".
method: &'a strThe HTTP method, upper-cased (GET, POST, …).
path: &'a strThe FULLY RESOLVED request target: every path placeholder substituted and the configured base URL already joined on. The SDK appends no query string to it.
It is the resolved path and never the template, so an endpoint allowlist
sees the URL as it will be sent. The query pairs the SDK will add are
carried separately in Self::query.
§An author-written ? STAYS in path
“The SDK appends no query string” is about what the SDK adds, not about what
a script author wrote. On the Code Mode surface,
api.get('/search/current?string=x') puts a literal ?string=x in the path
template, and the path floor deliberately permits ONE author-written ?
(validate_resolved_target), so it reaches a policy INSIDE path and never
appears in Self::query. A policy that must see or refuse every query pair
therefore has to look for a ? in path as well as read query. The
object form, api.get(path, { .. }), is what populates query.
query: &'a [(String, String)]The query pairs that will be appended to Self::path, EXCLUDING any
pair the auth provider contributes.
An API-key-in-query credential is an auth contribution and is therefore absent here by construction — that omission is the D-12 guarantee, not an oversight.
Populated on BOTH surfaces. On the Code Mode surface that required moving
the non-auth half of the remaining-body-to-query conversion above the hook
(Phase 128 plan 09); without that move a policy written to inspect query
pairs would have inspected an empty slice while the pairs that were about
to be sent still sat in Self::body.
body: Option<&'a Value>The JSON request body, when one will be sent.
None for a GET-like request, whose remaining fields have already been
converted into Self::query by the time the policy runs.
call_id: &'a strAn opaque identifier for the tools/call that produced this request.
Stable across every request ONE tools/call makes. On the Code Mode
surface a single execute_code run can send many requests, and all of them
carry the same id, so a policy can budget a whole run (total bytes, request
count, distinct endpoints) instead of seeing each request in isolation. A
per-request cap alone lets a caller split free text across several requests
that each fit under it. On the curated surface a tools/call is one
request, so the id is simply unique per request.
Unique across calls within a process, and with overwhelming probability across restarts. It is NOT a secret and NOT a distributed trace id: it is generated here, never taken from the client, and it should not be put on the wire.
Empty ONLY when unattributed (a caller driving a connector directly rather
than through a synthesized handler), the same convention as Self::tool.
Treat the empty string as “no grouping”, never as one shared bucket.
phase: RequestPhaseWhether this request is about to be SENT or is a validation-time preview.
RequestPhase::Validate marks a dry run: Code Mode’s validate_code
asks the policy about the fully literal calls in a script before any
approval token is issued, so a refusal reaches the model at validation
instead of after it has been approved. Nothing is sent.
A stateless policy (an allowlist, a size cap) should ignore this: it
gives the same answer in both phases, which is the point. A stateful
policy (a request or byte budget, a rate limiter) MUST NOT charge a
Validate request, or a script validated three times would spend its
budget before it ran once.
Implementations§
Source§impl<'a> OutboundRequest<'a>
impl<'a> OutboundRequest<'a>
Sourcepub fn new(
tool: &'a str,
method: &'a str,
path: &'a str,
query: &'a [(String, String)],
body: Option<&'a Value>,
) -> Self
pub fn new( tool: &'a str, method: &'a str, path: &'a str, query: &'a [(String, String)], body: Option<&'a Value>, ) -> Self
Construct an OutboundRequest.
A constructor rather than a struct literal because the type is
#[non_exhaustive]; this is also what lets an out-of-crate test build one
to exercise a policy in isolation.
Sourcepub fn with_phase(self, phase: RequestPhase) -> Self
pub fn with_phase(self, phase: RequestPhase) -> Self
Mark the request as a validation-time preview, see Self::phase.
Sourcepub fn with_call_id(self, call_id: &'a str) -> Self
pub fn with_call_id(self, call_id: &'a str) -> Self
Attach the per-tools/call identifier, see Self::call_id.
A builder rather than a sixth parameter of Self::new, so existing
callers of new keep compiling. The type is #[non_exhaustive], which is
what makes adding the field itself additive.
Trait Implementations§
Source§impl<'a> Clone for OutboundRequest<'a>
impl<'a> Clone for OutboundRequest<'a>
impl<'a> Copy for OutboundRequest<'a>
Auto Trait Implementations§
impl<'a> Freeze for OutboundRequest<'a>
impl<'a> RefUnwindSafe for OutboundRequest<'a>
impl<'a> Send for OutboundRequest<'a>
impl<'a> Sync for OutboundRequest<'a>
impl<'a> Unpin for OutboundRequest<'a>
impl<'a> UnsafeUnpin for OutboundRequest<'a>
impl<'a> UnwindSafe for OutboundRequest<'a>
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more