Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ jobs:
exit 1
fi
# Every three-or-more-part version token in the file, not just the
# ones on a line mentioning rmcp: one stamp says only "in 3.5.0". The
# ones on a line mentioning rmcp: one stamp says only "in 3.7.0". The
# loopback addresses are the sole other such token today and this
# check keeps it that way -- a new non-rmcp X.Y.Z fails here and has
# to be decided, rather than being silently skipped by a narrower
Expand All @@ -380,8 +380,8 @@ jobs:
# requirement, so it is compared with that manifest and not with the
# lock: a lock-only patch bump does not move the requirement, and
# comparing it there would teach the doc to misquote Cargo.toml.
# DESIGN.md's other two-part mentions ("rmcp 3.5 usage notes", "rmcp
# 3.5 default") stay unchecked -- the file also names rmcp 2.2 and
# DESIGN.md's other two-part mentions ("rmcp 3.7 usage notes", "rmcp
# 3.7 default") stay unchecked -- the file also names rmcp 2.2 and
# axum 0.8 in that shape, so no line rule tells a stamp from a
# deliberate reference. They can only go stale alongside a three-part
# stamp, which already fails above.
Expand Down
41 changes: 21 additions & 20 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/bugwarden/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ workspace = true
[dependencies]
bugwarden-core = { path = "../bugwarden-core", version = "0.7.0" }

rmcp = { version = "3.5", features = [
rmcp = { version = "3.6", features = [
"server",
"macros",
"transport-io",
Expand Down Expand Up @@ -91,7 +91,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# in-memory duplex transport (tools_wiremock) and over real streamable HTTP
# (http_transport_wiremock) — so the client half of rmcp is needed here. The
# rmcp `reqwest` feature selects rustls, matching the rustls-only workspace.
rmcp = { version = "3.5", features = [
rmcp = { version = "3.6", features = [
"client",
"transport-streamable-http-client-reqwest",
"reqwest",
Expand Down
6 changes: 3 additions & 3 deletions crates/bugwarden/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ impl Cli {
/// authority (`*`, a scheme-carrying URL, `a;b`, a percent-encoded
/// comma, zero-width unicode, a space-containing typo) is a startup
/// error. Bare IPv6 (`::1`) is matchable: it is rmcp's default
/// loopback spelling. rmcp 3.5.0's `parse_allowed_authority` would
/// loopback spelling. rmcp 3.7.0's `parse_allowed_authority` would
/// otherwise keep an unparsable list (validation on) and store a host
/// no inbound `Host` header will ever equal — a silent deny-all.
pub fn checked_allowed_hosts(&self) -> anyhow::Result<Option<Vec<&str>>> {
Expand Down Expand Up @@ -298,7 +298,7 @@ impl Cli {
}
}

/// True when `entry` is a Host authority rmcp 3.5.0 would compare to a
/// True when `entry` is a Host authority rmcp 3.7.0 would compare to a
/// successfully parsed inbound `Host`.
///
/// `http::uri::Authority` is the same parser rmcp uses. A bare IPv6
Expand Down Expand Up @@ -726,7 +726,7 @@ mod tests {

#[test]
fn unparsable_allowed_hosts_are_a_startup_error_named_in_the_message() {
// Each of these is what rmcp 3.5.0 would store (or skip) without
// Each of these is what rmcp 3.7.0 would store (or skip) without
// matching any inbound Host — validation on, deny-all. Refusing
// them here is what makes that fail at startup instead of one 403
// at a time. Deleting the refusal, or warning and continuing, must
Expand Down
18 changes: 10 additions & 8 deletions crates/bugwarden/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,12 @@ async fn main() -> anyhow::Result<()> {
// anything that then races an unhandled signal (#173).
let shutdown = shutdown_signal();
tokio::pin!(shutdown);
// Bounded stdin, never rmcp's bare `stdio()`: it reads a
// frame into an unbounded `Vec` (#234). Built here for the
// same reason `http_server_config()` is built in the other
// arm — the cap comes from `server`'s own policy, and
// `serve` takes it by value.
// Bounded stdin, never rmcp's bare `stdio()`: 3.5 read a
// frame into an unbounded `Vec` (#234), 3.6+ drops one over
// its own 16 MiB. Built here for the same reason
// `http_server_config()` is built in the other arm — the
// cap comes from `server`'s own policy, and `serve` takes
// it by value.
let (stdin, stdout) = server.stdio_transport();
// rmcp maps every read error to `receive() -> None`, which
// the service reports as an ordinary close. Two flags tell
Expand Down Expand Up @@ -473,8 +474,8 @@ const INPUT_STREAM_FAILED: &str = "the input stream failed before initialize";
/// A fixed, server-authored classification of a failed stdio handshake —
/// the only thing either exit path may say about one (#261).
///
/// rmcp 3.5.0's `ServerInitializeError`
/// (`service/server.rs:83-105`, `#[non_exhaustive]`) puts the client's
/// rmcp 3.7.0's `ServerInitializeError`
/// (`service/server.rs:70-91`, `#[non_exhaustive]`) puts the client's
/// whole first frame into `ExpectedInitializeRequest`, and its
/// `#[error("expect initialized request, but received: {0:?}")]` puts it
/// into DISPLAY as well as `Debug` — so neither the error nor its Display
Expand Down Expand Up @@ -521,7 +522,8 @@ fn serve_failure(error: &rmcp::service::ServerInitializeError) -> &'static str {
Frame::Response(_) => "the first frame was a response, not initialize",
Frame::Error(_) => "the first frame was an error reply, not initialize",
},
// Unreachable on rmcp 3.5.0: both construction sites pass `Some`.
// Unreachable on rmcp 3.7.0: the one construction site
// (`service/server.rs:667`) passes `Some`.
Failure::ExpectedInitializeRequest(None) => "no first frame was received",
// EOF before initialize. A read error and this build's own
// frame-cap refusal reach the same variant; the caller names
Expand Down
44 changes: 24 additions & 20 deletions crates/bugwarden/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,7 @@ fn allowlisted(params: Option<&JsonObject>) -> BTreeMap<String, Value> {
/// * [`Lifecycle::PerRequest`] — the request's own `_meta`. **Never
/// `ctx.client_info()`**: that convenience accessor falls back to
/// `peer_info().client_info` unless `request_metadata_required()`, which
/// rmcp 3.5.0 sets only on the stdio handshake-free path
/// rmcp 3.7.0 sets only on the stdio handshake-free path
/// (`service/server.rs`), so over streamable http it IS the
/// `{"name":"rmcp",…}` placeholder rmcp synthesises for a peer that
/// never handshook (`peer_info_for_stateless_request`, #34 §3c) — and
Expand Down Expand Up @@ -973,7 +973,7 @@ enum Lifecycle {
/// what gets served and who gets recorded cannot diverge by call-site
/// ordering.
///
/// rmcp 3.5.0 routes on `_meta` shape, not on the negotiated revision and
/// rmcp 3.7.0 routes on `_meta` shape, not on the negotiated revision and
/// not on [`SUPPORTED_PROTOCOL_VERSIONS`]: `is_legacy_request`
/// (`transport/streamable_http_server/tower.rs`) returns true — the session
/// path — for every `initialize` (rmcp #1228), and otherwise sends a POST
Expand Down Expand Up @@ -1048,7 +1048,7 @@ fn read_scope_serves(tool: &str) -> bool {
/// The refusal for a tool this request's credential does not reach.
///
/// Byte-identical to what `ToolRouter` answers for a name it does not route
/// (rmcp 3.5 `handler/server/router/tool.rs`), so a scope-hidden tool is
/// (rmcp 3.7 `handler/server/router/tool.rs`), so a scope-hidden tool is
/// indistinguishable from one that does not exist — the same
/// indistinguishability read-only mode already gives every caller (I13), and
/// no oracle about which tools the deployment serves (I2).
Expand Down Expand Up @@ -3365,7 +3365,7 @@ impl BugWarden {
/// there is one place where the policy field is read. Call it before
/// the server moves into the service closure.
///
/// These rmcp 3.5 defaults are set by name rather than inherited,
/// These rmcp 3.7 defaults are set by name rather than inherited,
/// because inheriting them changes how a deployment behaves without
/// anyone choosing it:
///
Expand Down Expand Up @@ -3409,15 +3409,15 @@ impl BugWarden {
/// have recorded the attempt — though rmcp's own refusal body prints
/// the number outright. That the boundary is observable to an
/// unauthenticated client, and what it discloses, is recorded in
/// DESIGN.md under "rmcp 3.5 usage notes".
/// DESIGN.md under "rmcp 3.7 usage notes".
///
/// `allowed_origins` is the browser-facing sibling of `allowed_hosts`
/// and the same reasoning covers it, but it is left inherited: its
/// empty default IS the disabled state, so naming it would assert
/// nothing. Anything that changes the `allowed_hosts` call below should
/// decide this one too rather than leave it behind. The remaining
/// fields, and why each stays inherited, are inventoried in DESIGN.md
/// under "rmcp 3.5 usage notes"; the caller in `main` adds
/// under "rmcp 3.7 usage notes"; the caller in `main` adds
/// `cancellation_token` so a SIGINT or SIGTERM reaches the live
/// transport (issue #114).
///
Expand Down Expand Up @@ -3460,9 +3460,12 @@ impl BugWarden {
/// The stdio transport pair to serve, in place of rmcp's bare
/// `stdio()`.
///
/// Stdin is wrapped in [`BoundedLines`] at `request_body_cap`:
/// rmcp reads a frame into an unbounded `Vec` (#234), so without this
/// the transport buffers whatever a peer writes before a newline.
/// Stdin is wrapped in [`BoundedLines`] at `request_body_cap`: rmcp
/// 3.5 read a frame into an unbounded `Vec` (#234), and rmcp 3.6+
/// bounds one at its own fixed 16 MiB and skips it, which
/// `DiscoverAnswering::framed` switches off — so without this the
/// transport either buffers whatever a peer writes before a newline or
/// answers a policy-admitted frame with silence.
/// Stdout is untouched — it carries this server's own output, which the
/// `FramedWrite` half already frames.
///
Expand Down Expand Up @@ -6114,7 +6117,7 @@ impl ServerHandler for BugWarden {
Reach::ReadOnly => tools.retain(|tool| read_scope_serves(&tool.name)),
Reach::Nothing => tools.clear(),
}
// The SEP-2549 hints are ours to gate: rmcp 3.5.0 strips
// The SEP-2549 hints are ours to gate: rmcp 3.7.0 strips
// `resultType` for a legacy peer and leaves these two alone. The
// predicate is HAND-COPIED from its `sep_2322_supported` — no shared
// constant, no test pinning the agreement — so re-read both on an
Expand Down Expand Up @@ -7718,12 +7721,12 @@ mod tests {
}
}

/// SEP-2243's schema annotation, byte-exact as rmcp 3.5.0 reads it —
/// SEP-2243's schema annotation, byte-exact as rmcp 3.7.0 reads it —
/// `schema.get("x-mcp-header")` in `transport/common/mcp_headers.rs`
/// (`param_header_annotations`, `validate_param_header_annotations`,
/// `reject_nested_annotations`). Upstream exports no constant for the
/// spelling, so this is our own copy: re-grep the vendored tree on every
/// rmcp bump.
/// (`collect_property_annotations`, behind `param_header_annotations`
/// and `validate_param_header_annotations`). Upstream exports no
/// constant for the spelling, so this is our own copy: re-grep the
/// vendored tree on every rmcp bump.
const X_MCP_HEADER: &str = "x-mcp-header";

/// The forbidden annotation anywhere in a serialized tool definition, as
Expand All @@ -7736,12 +7739,13 @@ mod tests {
/// with no recursion arms to keep in lockstep, which is the maintenance
/// surface `schema_portability_error` above has to carry.
///
/// Deliberately broader than rmcp's functional read, which in 3.5.0 is
/// top-level `properties` and string-valued only: the key is banned
/// Deliberately broader than rmcp's functional read, which in 3.7.0 is
/// every `properties` chain from the root (nested since 3.6, #1359;
/// top-level only before) and string-valued only: the key is banned
/// everywhere, at any value type, because upstream's depth rules are
/// version-specific. It will also trip on the substring appearing in a
/// description or a property name — accepted; a tripwire fails loud and
/// a human looks.
/// version-specific and have moved once. It will also trip on the
/// substring appearing in a description or a property name — accepted;
/// a tripwire fails loud and a human looks.
fn x_mcp_header_smell(name: &str, tool_json: &Value) -> Option<String> {
let serialized = tool_json.to_string();
serialized.contains(X_MCP_HEADER).then(|| {
Expand Down
Loading
Loading