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.7.0". The
# ones on a line mentioning rmcp: one stamp says only "in 3.8.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.7 usage notes", "rmcp
# 3.7 default") stay unchecked -- the file also names rmcp 2.2 and
# DESIGN.md's other two-part mentions ("rmcp 3.8 usage notes", "rmcp
# 3.8 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
12 changes: 6 additions & 6 deletions Cargo.lock

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

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.7.0's `parse_allowed_authority` would
/// loopback spelling. rmcp 3.8.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.7.0 would compare to a
/// True when `entry` is a Host authority rmcp 3.8.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.7.0 would store (or skip) without
// Each of these is what rmcp 3.8.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
4 changes: 2 additions & 2 deletions crates/bugwarden/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ 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.7.0's `ServerInitializeError`
/// rmcp 3.8.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
Expand Down Expand Up @@ -522,7 +522,7 @@ 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.7.0: the one construction site
// Unreachable on rmcp 3.8.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
Expand Down
26 changes: 13 additions & 13 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.7.0 sets only on the stdio handshake-free path
/// rmcp 3.8.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.7.0 routes on `_meta` shape, not on the negotiated revision and
/// rmcp 3.8.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.7 `handler/server/router/tool.rs`), so a scope-hidden tool is
/// (rmcp 3.8 `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.7 defaults are set by name rather than inherited,
/// These rmcp 3.8 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.7 usage notes".
/// DESIGN.md under "rmcp 3.8 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.7 usage notes"; the caller in `main` adds
/// under "rmcp 3.8 usage notes"; the caller in `main` adds
/// `cancellation_token` so a SIGINT or SIGTERM reaches the live
/// transport (issue #114).
///
Expand Down Expand Up @@ -6117,12 +6117,12 @@ 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.7.0 strips
// The SEP-2549 hints are ours to gate: rmcp 3.8.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
// rmcp bump. `None`, meaning no request revision and no session, is
// legacy.
// predicate is HAND-COPIED from its `is_2026_07_28_or_later` — no
// shared constant, no test pinning the agreement — so re-read both on
// an rmcp bump. `None`, meaning no request revision and no session,
// is legacy.
let serves_cache_hints = context
.protocol_version()
.is_some_and(|v| v.as_str() >= ProtocolVersion::V_2026_07_28.as_str());
Expand Down Expand Up @@ -7721,7 +7721,7 @@ mod tests {
}
}

/// SEP-2243's schema annotation, byte-exact as rmcp 3.7.0 reads it —
/// SEP-2243's schema annotation, byte-exact as rmcp 3.8.0 reads it —
/// `schema.get("x-mcp-header")` in `transport/common/mcp_headers.rs`
/// (`collect_property_annotations`, behind `param_header_annotations`
/// and `validate_param_header_annotations`). Upstream exports no
Expand All @@ -7739,7 +7739,7 @@ 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.7.0 is
/// Deliberately broader than rmcp's functional read, which in 3.8.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
Expand Down
6 changes: 3 additions & 3 deletions crates/bugwarden/src/stdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ type QueuedSend<E> = Pin<Box<dyn Future<Output = Result<(), E>> + Send>>;
///
/// `server/discover` is a probe, but over stdio rmcp lets it CHOOSE the
/// session lifecycle before anyone answers it: `serve_server_with_ct_inner`
/// (rmcp 3.7.0 `service/server.rs:544-672`) takes any first non-`ping`
/// (rmcp 3.8.0 `service/server.rs:544-672`) takes any first non-`ping`
/// frame that is not `initialize` as a commitment to the handshake-free
/// lifecycle and calls `Peer::require_request_metadata()` (`:653`) — a
/// sticky `AtomicBool` that no later `initialize` clears and no public API
Expand Down Expand Up @@ -923,7 +923,7 @@ mod discover {
async fn a_pipelined_probe_is_answered_under_write_contention() {
// The one thing this wrapper does that the inner transport does
// not: hold a reply. rmcp polls `receive` as one arm of a
// `select!` (rmcp 3.7.0 `service.rs:1398`) and drops the future
// `select!` (rmcp 3.8.0 `service.rs:1398`) and drops the future
// whenever another arm wins, so a reply awaited on `receive`'s own
// stack dies with the frame that asked for it — consumed, never
// answered, the client hung on that id while every other reply
Expand Down Expand Up @@ -1078,7 +1078,7 @@ mod discover {
"the probe must name this build, and nothing else: {probe}"
);
// Present whatever revision the probe declares:
// `strip_result_type_for_legacy_peer` (rmcp 3.7.0 model.rs) has no
// `strip_result_type_for_legacy_peer` (rmcp 3.8.0 model.rs) has no
// `DiscoverResult` arm, so rmcp never stripped it here either.
assert_eq!(probe["result"]["resultType"], "complete", "{probe}");
}
Expand Down
2 changes: 1 addition & 1 deletion crates/bugwarden/tests/binary_discover_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ async fn a_probe_only_client_hangs_up_cleanly() {
#[tokio::test]
async fn a_pipelined_probe_is_answered_over_real_pipes() {
// A probe behind other traffic, which no lock-step row here reaches:
// rmcp polls `receive` as one arm of a `select!` (rmcp 3.7.0
// rmcp polls `receive` as one arm of a `select!` (rmcp 3.8.0
// `service.rs:1398`) and drops that future whenever another arm wins,
// so a reply the wrapper awaited on `receive`'s own stack would be
// dropped with it — the frame consumed, the id never answered, the
Expand Down
4 changes: 2 additions & 2 deletions crates/bugwarden/tests/binary_shutdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
//! - any `serve_failure` arm deleted so its failure falls to the wildcard,
//! or any two of its literals exchanged. Three arms have no row, and
//! cannot: `ExpectedInitializeRequest(None)` is constructed nowhere in
//! rmcp 3.7.0, `UnexpectedInitializeResponse` cannot happen while rmcp
//! rmcp 3.8.0, `UnexpectedInitializeResponse` cannot happen while rmcp
//! maps this handler's own `InitializeResult` into the result it then
//! checks, and `Cancelled` needs a cancellation token `ServiceExt::serve`
//! creates and never cancels. cargo-mutants reaches only the OUTER
Expand Down Expand Up @@ -914,7 +914,7 @@ async fn stdio_an_over_cap_frame_after_initialize_exits_one() {

#[tokio::test]
async fn stdio_a_pre_initialize_tool_call_never_logs_the_frame() {
// rmcp 3.7.0 answers this frame in-band: one stdout -32602
// rmcp 3.8.0 answers this frame in-band: one stdout -32602
// with a fixed `_meta` message, no client bytes, and the
// session keeps waiting for `initialize`. The handler never
// runs, stderr stays free of the frame, and the process only
Expand Down
2 changes: 1 addition & 1 deletion crates/bugwarden/tests/common/deadline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use std::time::Duration;

/// Bound on every request one of these harnesses awaits an answer to.
///
/// rmcp 3.7.0's typed client calls send with
/// rmcp 3.8.0's typed client calls send with
/// `PeerRequestOptions::no_options()` — `Self::default()`, `timeout: None`
/// (`service.rs`) — and the `initialize` handshake reads its response off
/// the transport with no deadline either. So a request the
Expand Down
10 changes: 5 additions & 5 deletions crates/bugwarden/tests/http_transport_wiremock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ async fn guard_denies_uniformly_over_http() {

#[tokio::test]
async fn a_client_addressing_the_server_by_name_is_served() {
// rmcp 3.7's `allowed_hosts` default is loopback only, so inheriting it
// rmcp 3.8's `allowed_hosts` default is loopback only, so inheriting it
// would answer every request whose `Host` is the name the operator
// actually deployed under — every containerised one — with a rejection.
// main.rs disables that validation by name; this pins the decision,
Expand Down Expand Up @@ -576,7 +576,7 @@ async fn an_allowed_hosts_entry_naming_no_host_leaves_validation_off() {

#[tokio::test]
async fn an_unparsable_allowed_hosts_entry_is_a_startup_error() {
// The same path `serve_http` and `main` take: rmcp 3.7.0 would store
// The same path `serve_http` and `main` take: rmcp 3.8.0 would store
// `*` as a host that matches only `Host: *`, turning validation on as
// a silent deny-all. Refusing here is what the operator sees instead
// of one 403 at a time.
Expand All @@ -599,7 +599,7 @@ async fn an_unparsable_allowed_hosts_entry_is_a_startup_error() {

#[tokio::test]
async fn a_handshake_free_call_is_refused_and_never_names_a_client() {
// rmcp 3.7.0 routes to its handshake-free lifecycle when the request's
// rmcp 3.8.0 routes to its handshake-free lifecycle when the request's
// revision is 2026-07-28 or newer, or `_meta` carries BOTH
// `protocolVersion` and `clientCapabilities` — not for `initialize`,
// which always takes the session path (#1228). The body below sends
Expand Down Expand Up @@ -892,7 +892,7 @@ async fn traceparent_over_http_lands_in_the_audit_record() {
// `CallToolRequestParams.meta` arrives `None` here — and delivers it
// to the handler as the extensions-backed `RequestContext.meta`, so
// this test pins the `context.meta` fallback that every serialized
// transport depends on (see the rmcp 3.7 usage notes in DESIGN.md).
// transport depends on (see the rmcp 3.8 usage notes in DESIGN.md).
let mock = MockServer::start().await;
mount_bug_for_key(&mock, world_readable_bug(7), "srv-key").await;

Expand Down Expand Up @@ -1525,7 +1525,7 @@ async fn a_per_request_call_naming_no_client_is_served_and_names_no_placeholder(
// The mutation this kills is `client_of` reading `ctx.client_info()` or
// `peer_info()` on this path: rmcp synthesises the stateless peer with
// `Implementation::default()`, so either would put
// `{"name":"rmcp","version":"3.7.0"}` into the record — not a missing
// `{"name":"rmcp","version":"3.8.0"}` into the record — not a missing
// field but a plausible wrong one. The whole file is checked for the
// string, not just this record's field, because a placeholder that
// leaked into any other record would be the same defect.
Expand Down
Loading
Loading