diff --git a/.github/workflows/plugin-ci.yml b/.github/workflows/plugin-ci.yml index 5a04438510..15fb8a9666 100644 --- a/.github/workflows/plugin-ci.yml +++ b/.github/workflows/plugin-ci.yml @@ -378,6 +378,17 @@ jobs: printf '%s\n' "$red" | grep -q "crossing(s), pinned" \ || { printf '%s\n' "$red" >&2; echo "::error::RED: the moved count failed for another reason than the exact comparator"; exit 1; } echo "conformance: the published suite ran green; the moved count was refused" + - name: "busbar's plane gates over this plane's source (plane-purity, structure-lint, config-schema): no row that passes over busbar's pinned plane fails over this change" + # ARCHITECT row 106 (A): a plane that left busbar is read by busbar's gates from its pinned + # checkout; here the same gates, at this repo's busbar pin, read THIS change in its place + # (BUSBAR_PLANE_SOURCE), so the plane is judged where its source changes. + if: ${{ !cancelled() && inputs.plugin_kind == 'plane' && steps.crates.outputs.present == 'true' }} + working-directory: busbar + env: + CRATE: ${{ inputs.plugin_crate }} + run: | + set -euo pipefail + "$PLUGIN_GATES" plugin-gates plane "$GITHUB_WORKSPACE/busbar" "$GITHUB_WORKSPACE/plugin" "${CRATE%-plugin}" - name: cargo deny (the rendered deny.toml; busbar's .github/fleet/deps.toml at the pin) if: ${{ !cancelled() && steps.crates.outputs.present == 'true' }} working-directory: plugin diff --git a/Cargo.lock b/Cargo.lock index c8b06161e9..cf3e1a546d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -881,9 +881,9 @@ dependencies = [ [[package]] name = "busbar-plane-decisions" version = "1.6.0" +source = "git+https://github.com/GetBusbar/busbar-plane-decisions?rev=d78c070da407c53d7235145fb76d69ed41b7046b#d78c070da407c53d7235145fb76d69ed41b7046b" dependencies = [ "busbar-contract", - "busbar-plugin-loader", "serde", "serde_json", ] @@ -905,10 +905,10 @@ dependencies = [ [[package]] name = "busbar-plane-mcp" version = "1.6.0" +source = "git+https://github.com/GetBusbar/busbar-plane-mcp?rev=a5e2ae355c138d8a57a4012225bf441c79bb84bd#a5e2ae355c138d8a57a4012225bf441c79bb84bd" dependencies = [ "base64 0.22.1", "busbar-contract", - "busbar-plugin-loader", "bytes", "indexmap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 289dea8d45..0b382ae363 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,10 +18,8 @@ members = [ "crates/busbar-kernel-wal", "crates/busbar-kernel-audit", "crates/busbar-plane-llm", - "crates/busbar-plane-mcp", "crates/busbar-plane-a2a", "crates/busbar-plane-streaming", - "crates/busbar-plane-decisions", "crates/plugin-loader", "crates/busbar-transport-stdio", "crates/busbar-transport-webrtc", @@ -112,6 +110,14 @@ busbar-hook-ranking = { git = "https://github.com/GetBusbar/busbar-hook-ranking" # double (`busbar_kernel::test_support::store_double`), and the root's `linked_store_double` tests # prove the double states what this door states. busbar-store-memory = { git = "https://github.com/GetBusbar/busbar-store-memory", rev = "848851e63ac7a454eb3d5daf9e11d739c48d738b" } +# THE MCP PLANE (kind: plane), pulled at a pinned rev: the logic crate the root links as the mcp plane's +# door row (`plane-mcp-door`, `busbar_plane_mcp::tool_door::door`) and installs in every build's tests. +# Its dropped-in cdylib twin (`busbar-plane-mcp-plugin`) and its gates live in that repo's plugin CI. +busbar-plane-mcp = { git = "https://github.com/GetBusbar/busbar-plane-mcp", rev = "a5e2ae355c138d8a57a4012225bf441c79bb84bd" } +# THE DECISIONS PLANE (kind: plane), pulled at a pinned rev: the logic crate the root links as the +# decisions plane's door row (`plane-decisions`, `busbar_plane_decisions::plane_door::door`). Its +# dropped-in cdylib twin (`busbar-plane-decisions-plugin`) and its gates live in that repo's plugin CI. +busbar-plane-decisions = { git = "https://github.com/GetBusbar/busbar-plane-decisions", rev = "d78c070da407c53d7235145fb76d69ed41b7046b" } # ── A2A, from the a2aproject's OWN crates ────────────────────────────────────────────────────── # THE OBVIOUS NAME IS THE WRONG ONE. `a2a-rs` on crates.io is `emillindfors/a2a-rs` (MIT, v0.5.0) — # a different author entirely. The a2aproject family is the `a2a-*-lf` / `a2a-pb` set, all diff --git a/crates/busbar-contract/Cargo.toml b/crates/busbar-contract/Cargo.toml index aaba181aaf..df0183aa08 100644 --- a/crates/busbar-contract/Cargo.toml +++ b/crates/busbar-contract/Cargo.toml @@ -105,8 +105,11 @@ crate-type = ["cdylib"] [dependencies] serde = { workspace = true } serde_json = { workspace = true } -# The section-map split (`section`) reads a plane's section as YAML values, in insertion order. -serde_yaml = { workspace = true } +# The section-map split (`section::split_section`) and the door plane's provider list +# (`plane_calls::dialect_facts`) read a plane's section as YAML values, in insertion order. Optional, +# behind the `yaml` feature: the host and planes turn it on, so a plugin of another kind (store, +# secret, auth, hook, export, transport) links no YAML parser and carries no RUSTSEC-2024-0449. +serde_yaml = { workspace = true, optional = true } indexmap = { workspace = true } async-trait = { workspace = true } futures = { workspace = true } @@ -150,6 +153,11 @@ hyper = { workspace = true } dispatch = [] runtime = [] +# THE YAML READERS (`section::split_section`, `plane_calls::dialect_facts`). OFF by default. On in +# `busbar-kernel` (the host reads every plane section) and in every plane crate; every other +# workspace crate that calls them reaches this feature through its `busbar-kernel` edge. +yaml = ["dep:serde_yaml"] + # The TEST-ONLY seal (#65). `plugin::TestKernelSeal` is the one implementor of the now-SEALED # `plugin::KernelSeal` that exists for fixtures rather than for the kernel, and it exists at all # because a PLANE crate may not name `caps` (section 1.2) and so cannot mint a real token for its diff --git a/crates/busbar-contract/src/plane_calls.rs b/crates/busbar-contract/src/plane_calls.rs index 5c1579c895..81626f37b9 100644 --- a/crates/busbar-contract/src/plane_calls.rs +++ b/crates/busbar-contract/src/plane_calls.rs @@ -172,6 +172,10 @@ pub struct DialectFacts { /// the order first referenced, with the `protocol` and `error_map` `lookup` resolves it to (a name /// `lookup` does not know is skipped: the configuration's `resolve` refused it). An empty /// `error_map` is absent. +/// +/// Behind the `yaml` feature: the section it reads is a YAML value, and only the host and door +/// planes build this list. +#[cfg(feature = "yaml")] pub fn dialect_facts( section: &serde_yaml::Value, lookup: impl Fn(&str) -> Option<(String, std::collections::BTreeMap)>, diff --git a/crates/busbar-contract/src/section.rs b/crates/busbar-contract/src/section.rs index ce64ed3005..fa610e41c9 100644 --- a/crates/busbar-contract/src/section.rs +++ b/crates/busbar-contract/src/section.rs @@ -8,13 +8,16 @@ //! which are lifted out first as the all-plane `hooks:` attach (a LIST, so ADDITIVE) and the //! all-plane `upstream_credentials:` default (a SCALAR, so OVERRIDE). //! -//! [`split_section`] owns the ORDER and every SENTENCE, and a plane supplies the only three things -//! that differ: the WORDS for its section (the section key and the noun an operator reads back, both -//! off the plane's own declaration), the TYPE one registration parses into, and its own VALUE RULES. +//! `split_section` (behind the `yaml` feature) owns the ORDER and every SENTENCE, and a plane +//! supplies the only three things that differ: the WORDS for its section (the section key and the +//! noun an operator reads back, both off the plane's own declaration), the TYPE one registration +//! parses into, and its own VALUE RULES. //! //! No statics, no I/O and no registry: words and a deserializer in, a split section out. A plane //! reads its section through this without naming the kernel or another plane. +// The section split alone reads YAML values; see the `yaml` feature in Cargo.toml. +#[cfg(feature = "yaml")] use serde::Deserialize as _; /// THE TWO WORDS RESERVED AT EVERY PLANE SECTION'S TOP LEVEL: the all-plane `hooks:` attach list and @@ -193,6 +196,10 @@ pub struct Section { /// /// The deserializer's error carrying the refusal sentence: a reserved word used as a registration /// name, a malformed reserved knob, a registration that does not parse, or one `validate` refuses. +/// +/// Behind the `yaml` feature: only the host (`busbar-kernel`) and planes read a section, so a +/// plugin of another kind does not link a YAML parser. +#[cfg(feature = "yaml")] pub fn split_section<'de, D, T>( deserializer: D, section: &'static str, diff --git a/crates/busbar-core-admin/src/tests/planeverbs_tests.rs b/crates/busbar-core-admin/src/tests/planeverbs_tests.rs index 93048d0f12..f5238711a9 100644 --- a/crates/busbar-core-admin/src/tests/planeverbs_tests.rs +++ b/crates/busbar-core-admin/src/tests/planeverbs_tests.rs @@ -29,7 +29,18 @@ fn shipped_plane_keys() -> Vec { .map(str::to_string) }) .collect(); + // A PLANE THAT LEFT FOR ITS OWN REPO is still a plane this tree links: the root pins it as a + // `git` row of `[workspace.dependencies]` (`busbar-plane- = { git = …, rev = … }`), and + // its key is banned exactly as when its crate lived here (ARCHITECT row 106 (A)). + let root_manifest = std::fs::read_to_string(crates_dir.join("../Cargo.toml")) + .expect("the workspace's root Cargo.toml is readable"); + keys.extend(root_manifest.lines().filter_map(|l| { + let (name, spec) = l.trim().split_once('=')?; + let key = name.trim().strip_prefix("busbar-plane-")?; + (spec.contains("git =") && !key.ends_with("-plugin")).then(|| key.to_string()) + })); keys.sort(); + keys.dedup(); // A derivation that found nothing would make both ratchets below pass on any source at all. assert!( keys.len() >= 2, diff --git a/crates/busbar-kernel/Cargo.toml b/crates/busbar-kernel/Cargo.toml index 08c435f2df..3aa825f32f 100644 --- a/crates/busbar-kernel/Cargo.toml +++ b/crates/busbar-kernel/Cargo.toml @@ -36,7 +36,9 @@ bumpalo = { workspace = true } # stays on the admin surface; only the neutral scope model lives here, so a non-admin core # consumer (router, the chain-verification module, config, config_validate) never has to name # "admin" to check a scope. -busbar-contract = { path = "../busbar-contract" } +# `yaml`: the host reads every plane's section (`plane::config`, `plane::door`) through the contract's +# YAML readers; this edge is the one that turns them on for the workspace. +busbar-contract = { path = "../busbar-contract", features = ["yaml"] } # (The HOT-lane plugin/plane ABI the kernel hosts — the `#[repr(C)] PlaneHostVtable` and its POD types # — is `busbar_contract::abi::hot` since the #84 merge; consumed HOST-side by `src/plane_host`.) # THE ONE NETWORK JUDGE (#36: "the upstream leg — walk + swrr weighting + pre-dial guard; trust diff --git a/crates/busbar-kernel/src/config_validate/secret_refs.rs b/crates/busbar-kernel/src/config_validate/secret_refs.rs index 93c43c12e2..76b78e7e0b 100644 --- a/crates/busbar-kernel/src/config_validate/secret_refs.rs +++ b/crates/busbar-kernel/src/config_validate/secret_refs.rs @@ -400,18 +400,11 @@ pub(crate) const SECRET_BEARING_TYPES: &[(&str, SecretBearing)] = &[ ("AuthChainEntry", SecretBearing::Walked), ("IdentityProviderCfg", SecretBearing::Walked), ("BrowserLoginCfg", SecretBearing::Walked), - // `tools..token_exchange.subject_token` — busbar's OWN token, the SUBJECT of an RFC 8693 - // exchange, never the caller's. - ( - "TokenExchangeCfg", - SecretBearing::DoorDeclared( - "the `tools:` door's section: its Statement declares \ - `settings.*.token_exchange.subject_token` (and `settings.*.env.*` for a stdio child's \ - referenced environment), and the folded row's section enumerates every reference at \ - those paths", - ), - ), - // That plane's LEASED outbound delegation credential. Reached from `RootCfg` through + // The `tools:` door's own credential type (`tools..token_exchange.subject_token`) left + // this tree with its plane (the pinned plugin repo, #39): its references are read at the door's + // declared `settings.*.token_exchange.subject_token` / `settings.*.env.*` paths through the + // `DoorSection` row below, so no type of it is listed here. + // The `agents:` plane's LEASED outbound delegation credential. Reached from `RootCfg` through // `agent_defs -> agents..upstream_credential`. ("OutboundCredential", SecretBearing::Walked), // That plane's OUTBOUND CLIENT CERTIFICATE — busbar's own end of a mutual handshake with a diff --git a/crates/busbar-kernel/src/trust/tests/genericity_tests.rs b/crates/busbar-kernel/src/trust/tests/genericity_tests.rs index c129cf9929..d45a6184a9 100644 --- a/crates/busbar-kernel/src/trust/tests/genericity_tests.rs +++ b/crates/busbar-kernel/src/trust/tests/genericity_tests.rs @@ -241,6 +241,17 @@ fn the_lifecycle_names_no_plane_in_its_code() { rest.split('-').next().map(str::to_string) }) .collect(); + // A PLANE THAT LEFT FOR ITS OWN REPO is still a plane this tree links: the root pins it as a + // `git` row of `[workspace.dependencies]` (`busbar-plane- = { git = …, rev = … }`), and + // its key is banned exactly as when its crate lived here (ARCHITECT row 106 (A)). + let root_manifest = std::fs::read_to_string(crates_dir.join("../Cargo.toml")) + .expect("the workspace's root Cargo.toml is readable"); + plane_keys.extend(root_manifest.lines().filter_map(|l| { + let (name, spec) = l.trim().split_once('=')?; + let rest = name.trim().strip_prefix("busbar-plane-")?; + (spec.contains("git =") && !rest.ends_with("-plugin")) + .then(|| rest.split('-').next().map(str::to_string))? + })); plane_keys.sort(); plane_keys.dedup(); assert!( diff --git a/crates/busbar-plane-decisions/Cargo.toml b/crates/busbar-plane-decisions/Cargo.toml deleted file mode 100644 index b4fd81d4e8..0000000000 --- a/crates/busbar-plane-decisions/Cargo.toml +++ /dev/null @@ -1,83 +0,0 @@ -# busbar-plane-decisions — THE DECISIONS PLANE (jev), as an ADAPTER and nothing else. -# -# WHAT THIS CRATE IS. One implementation of the plugin contract's plane kind for the "decision" -# protocol surface, dialect `jev` — a byte-identity passthrough to api.typesafe.ai's two operations: -# POST /v1/systemone and GET /v1/models. The signed design (DECISION #39, jev v5 sign-off) rules -# this the ONE crate for the whole plane: unlike busbar-plane-mcp/busbar-plane-a2a, which are pure -# adapters over a separate "impure host" crate (busbar-mcp/busbar-a2a) that carries the config -# section, the admin wiring and the registry declaration, jev is deliberately a SINGLE crate that -# holds the pure plane AND its own typed config section. -# -# THE SECOND NON-CONTRACT EDGE IS GONE TOO, AND WITH IT THE ONE BANNED-SOURCE PATH ANY PURE PLANE -# HAD. This was the only pure plane whose manifest named the legacy plugin-facing crate — 1 -# occurrence here against 0 in the other four — and the single symbol it was named for, -# `UpstreamCreds`, is now `busbar_contract::config::UpstreamCreds`, so the dependency had nothing -# left to carry and was deleted. What it HAD been carrying was a purity breach the denylist family -# could not see until this crate was enrolled in a kind list at all: that crate depends on `sha2`, -# `sha2` on `cpufeatures`, `cpufeatures` on `libc`, and a pure plane may reach none of them. -# -# THE `busbar-kernel` EDGE IS GONE, AND SO IS THE ONLY FILE THAT NEEDED IT. Two things used to make -# this manifest name the kernel and neither does any more. `ModelCfg`, reused VERBATIM for -# `decisions.models.`, moved to `busbar_contract::config::ModelCfg` (DECISIONS #40/#38). -# `src/registry.rs` — a crate-local `PlaneDecl` constant typed against -# `busbar_kernel::plane::registry::{PlaneDecl, BuildCtx}`, declared for a FUTURE root registration — -# was DELETED rather than moved: nothing outside this crate ever named `PLANE_DECL` (the binary's -# `register_planes()` installs llm/mcp/a2a/voice and never this crate, which is not even in the -# binary's dependency graph), so it was unreachable generation-1 code, and #19 retires that -# generation anyway. Deleting it closed this crate's #40 dep-wall violation outright — no extraction -# of `PlaneDecl` into `busbar-contract` was needed, and none was done. When a real registration for -# this plane is wired, it is written against whatever seam exists then, not against a kernel type -# resurrected from a constant nobody read. -# -# WHAT THIS CRATE IS NOT. There is no governance here, no breaker, no hook seat, no approval -# decision and no metering arithmetic. Those are units, and a unit is on the other side of the -# kernel from a plane. The metering method returns LOCATORS — where the numbers are — and never a -# number the ledger would post. The routing method returns a plan, never a connection. jev holds no -# durable state of its own (it is a stateless request/response passthrough, provider-scoped), so it -# declares no record schemas. -[package] -name = "busbar-plane-decisions" -version = "1.6.0" -publish = false # internal workspace crate; never goes to crates.io -edition = "2021" -# One pinned number, set to what CI verifies — the same rationale every other member states. -rust-version = "1.97" -description = "The decisions plane for busbar: the plugin contract's plane kind implemented over the jev dialect (api.typesafe.ai byte-identity passthrough)." -license = "Apache-2.0" - -[lib] -name = "busbar_plane_decisions" -path = "src/lib.rs" - -# NO FEATURES, ON PURPOSE. A plane compiled two ways is two planes, and the kernel would have no way -# to tell which one it registered. The contract crate holds itself to the same rule and asserts it. - -[dependencies] -# THE CONTRACT, AND NOW THE ONLY BUSBAR CRATE THIS MANIFEST NAMES — which is what its four sibling -# planes have always looked like. Both reserved model-serving config shapes live here: -# `ModelCfg` (the `decisions.models.` value, uniform with `pools.models.`) at -# `busbar_contract::config::ModelCfg` since DECISIONS #40/#38, and `UpstreamCreds` (the reserved -# `upstream_credentials` value, the same enum `PoolCfg` names for its own per-section override) at -# `busbar_contract::config::UpstreamCreds`. The type was moved here rather than the old edge being -# waived: a waiver at first sighting suppresses the finding instead of the violation. -busbar-contract = { path = "../busbar-contract" } -# The object notation the jev dialect speaks. Byte-identity passthrough of the body is the norm; -# this crate parses ONLY the handful of declared pointers (never `state`/`answers`) to build facts. -serde_json = { workspace = true } -serde = { workspace = true } - -[dev-dependencies] -# The TEST-ONLY seal (#65). `busbar_contract::plugin::TestKernelSeal` is the one blessed -# fixture implementor of the SEALED `KernelSeal` trait; this crate's test harnesses used to -# forge their own. Dev-only and feature-gated, so it is absent from every release build. -busbar-contract = { path = "../busbar-contract", features = ["test-seal"] } -# THE DOOR'S BOTH-WAYS WITNESS: busbar's PUBLISHED conformance suite (`tests/conformance.rs`, -# the `conformance` feature) over the linked door and this crate's own dropped-in image (the -# `decisions_door` example below), admitted through the one loader and driven through the one -# dispatcher by the plane kind's script. -busbar-plugin-loader = { path = "../plugin-loader", features = ["conformance"] } - -# THE DROPPED-IN DOOR: built by `cargo test`. -[[example]] -name = "decisions_door" -crate-type = ["cdylib"] diff --git a/crates/busbar-plane-decisions/examples/decisions_door.rs b/crates/busbar-plane-decisions/examples/decisions_door.rs deleted file mode 100644 index 02b4be3c2b..0000000000 --- a/crates/busbar-plane-decisions/examples/decisions_door.rs +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! The decisions plane's door, dropped in: the one exported symbol, forwarding to the same door a -//! compiled-in row holds. `tests/conformance.rs` loads it beside the linked door and drives both -//! the same way. - -busbar_contract::export_door!(busbar_plane_decisions::plane_door::door); diff --git a/crates/busbar-plane-decisions/src/claims.rs b/crates/busbar-plane-decisions/src/claims.rs deleted file mode 100644 index a937801487..0000000000 --- a/crates/busbar-plane-decisions/src/claims.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! The claims this plane makes over arriving bytes. -//! -//! A claim is the only way a plane names a transport, and it names it as a claim — never as a -//! connection. jev has two exact-path claims, both spoken over HTTP, both carrying a bearer -//! credential this plane never sees (the core gateway credential — see `plane.rs::authenticate`). - -use busbar_contract::grammar::{Claim, Selector}; - -use crate::ops::{PATH_MODELS, PATH_SYSTEMONE}; - -/// The request transport both of this plane's claims are made against. -pub const TRANSPORT: &str = "http"; - -/// The credential scheme this plane's claims sit under. -/// -/// One scheme, one alternative: the bearer credential the core gateway resolves. The plane never -/// narrows to anything else and never sees what is behind it. -pub const SCHEME: &str = "decision-inbound"; - -/// The alternative a unit may be narrowed to. Every jev request carries a bearer credential; there -/// is no anonymous surface (unlike A2A's discovery documents — jev's model catalogue still needs -/// caller identity, because it is billed per the provider's own account scoping). -pub const ALT_TOKEN: &str = "bearer"; - -/// The alternatives declared on both claims. -const SCHEME_ALTERNATIVES: &[&str] = &[ALT_TOKEN]; - -/// The claims this plane declares, in declaration order. Both are exact paths — jev names its -/// operation in the HTTP verb and the path, never a pattern — so there is no overlap between them -/// for a boot to arbitrate. -pub const CLAIMS: &[Claim] = &[ - Claim { - transport: TRANSPORT, - selector: Selector::ExactPath(PATH_SYSTEMONE), - scheme: Some(SCHEME), - scheme_alternatives: SCHEME_ALTERNATIVES, - idempotency: None, - }, - Claim { - transport: TRANSPORT, - selector: Selector::ExactPath(PATH_MODELS), - scheme: Some(SCHEME), - scheme_alternatives: SCHEME_ALTERNATIVES, - idempotency: None, - }, -]; - -#[cfg(test)] -#[path = "tests/claims.rs"] -mod tests; diff --git a/crates/busbar-plane-decisions/src/codec/mod.rs b/crates/busbar-plane-decisions/src/codec/mod.rs deleted file mode 100644 index f8122c99c1..0000000000 --- a/crates/busbar-plane-decisions/src/codec/mod.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! The `jev` dialect: plain HTTP+JSON, byte-identity passthrough. -//! -//! There is no envelope to read the way MCP/A2A read a JSON-RPC envelope — jev's two operations are -//! named by the HTTP verb and path (`ops.rs`), and a request or response body is a bare JSON -//! document forwarded UNCHANGED. This module's whole job is: name the small, closed set of pointers -//! this plane ever resolves (never `state`, never `answers` — see `facts.rs`'s module note), and -//! read them through the contract's own span grammar rather than a second parser. -//! -//! ## Byte-identity passthrough, precisely -//! -//! "Byte-identity" means the body bytes that arrive are the body bytes that leave, on both legs: -//! `encode_egress` forwards the caller's request body to the provider unchanged, and -//! `encode_response` forwards the provider's response body to the caller unchanged. The pointer -//! list below is read ALONGSIDE that forwarding, never in place of it — `Ir::body()` always carries -//! the whole document; `Ir::pointer()` is a read into a handful of declared members of it, and -//! nothing this plane does discards or rewrites the rest. - -use busbar_contract::bounded::{Ir, PlaneAlloc}; -use busbar_contract::unit::Ctx; -use busbar_contract::wire::Decode; - -/// The pointer a response carries its top-level error object at, where it errored. -/// -/// Presence alone is what this plane reads — never the error's own fields, which could echo -/// caller-supplied content back (jev's error bodies are not exempted from the PII rule by being -/// "just an error"). -pub const PTR_ERROR: &str = "/error"; - -/// The pointer a response carries its provider-assigned request identifier at, where it names one. -/// Plain metadata: a correlation handle, never decision content. -pub const PTR_REQUEST_ID: &str = "/request_id"; - -/// The pointer a `systemone` request or response body echoes a bare `id` at, where jev uses that -/// spelling instead of `request_id`. Read as a fallback, same as `PTR_REQUEST_ID`. -pub const PTR_ID: &str = "/id"; - -/// The pointer a SUCCESSFUL `systemone` response reports its billable usage count at. -/// -/// This is the one number `plane.rs::meter` reads. Nested under `usage` rather than top-level so a -/// provider that reports several billing dimensions in the future has somewhere to add a sibling -/// without this plane growing a second top-level member to ignore. -pub const PTR_USAGE_UNITS: &str = "/usage/units"; - -/// The pointers a SUCCESSFUL `systemone` response reports its input and output tokens at, where it -/// reports them (DECISIONS Q1-5: "usage pointers are DIALECT data (/usage/units, -/// /usage/input_tokens,/usage/output_tokens)"). -pub const PTR_USAGE_INPUT_TOKENS: &str = "/usage/input_tokens"; -/// See [`PTR_USAGE_INPUT_TOKENS`]. -pub const PTR_USAGE_OUTPUT_TOKENS: &str = "/usage/output_tokens"; - -/// The pointer a SUCCESSFUL `nanojev` answer states the upstream's own billing unit at: the states -/// its evaluation ran (THE DESIGN section 2: "it meters the upstream's own billing unit, counted -/// from the answer already read (`execution.states`; an answer with no count refuses loudly"). -pub const PTR_EXECUTION_STATES: &str = "/execution/states"; - -/// Every pointer this plane resolves in a RESPONSE body (`systemone`, `evaluate` or `models`). Declared here -/// once, so the "never resolves `/state` or `/answers`" claim is checkable by reading one list -/// rather than auditing every call site. -pub const RESPONSE_PTRS: &[&str] = &[ - PTR_ERROR, - PTR_REQUEST_ID, - PTR_ID, - PTR_USAGE_UNITS, - PTR_USAGE_INPUT_TOKENS, - PTR_USAGE_OUTPUT_TOKENS, - PTR_EXECUTION_STATES, -]; - -/// The pointer a request names the model it is routed over at: its top-level `model` (THE DESIGN -/// section 2, model resolution; DECISIONS D8b "REQUEST_PTRS += /model (metadata)"). Routing -/// metadata, never decision content. -pub const PTR_MODEL: &str = "/model"; - -/// Every pointer this plane resolves in a REQUEST body: the model it routes over, and nothing else. -/// A `systemone` request carries the caller's `state`, which this plane never reads; jev names its -/// operation in the path, so the top-level `model` is the one member routing needs -/// (`driven::route_model`) and the one `upstream_model` rewrites (`driven::spliced`). -pub const REQUEST_PTRS: &[&str] = &[PTR_MODEL]; - -/// The span view of a body, built from one of the two declared pointer lists above. -/// -/// One scan of one closed grammar, into the unit's own arena — the same seam `busbar-plane-a2a` -/// reads through, named here rather than re-derived, so a declared-pointer list is the only way -/// this plane's view of a body can ever widen. -pub fn view<'u>(body: &'u [u8], pointers: &[&'u str], ctx: &Ctx<'u>) -> Result, Decode> { - let spans = busbar_contract::spans::resolve(body, pointers, ctx.arena()) - .map_err(|_| Decode::Oversize)?; - Ok(Ir::new(body, spans)) -} - -/// The span view of a body against an arbitrary arena, for callers (tests, `encode_egress`'s own -/// rebuild) that hold an arena without a full `Ctx`. -pub fn view_with_arena<'u>( - body: &'u [u8], - pointers: &[&'u str], - arena: &'u dyn PlaneAlloc, -) -> Result, Decode> { - let spans = - busbar_contract::spans::resolve(body, pointers, arena).map_err(|_| Decode::Oversize)?; - Ok(Ir::new(body, spans)) -} - -/// Whether a body has a member at one declared pointer at all. -#[must_use] -pub fn has(body: &[u8], pointer: &str) -> bool { - read_raw(body, pointer).is_some() -} - -/// The raw bytes at one declared pointer of a body. -#[must_use] -pub fn read_raw<'u>(body: &'u [u8], pointer: &str) -> Option<&'u [u8]> { - match busbar_contract::spans::resolve_pointer(body, pointer) { - busbar_contract::spans::Resolved::Found(span) => body.get(span.start..span.end), - _ => None, - } -} - -/// The string value at one declared pointer, with its quotes stripped. -#[must_use] -pub fn read_str<'u>(body: &'u [u8], pointer: &str) -> Option<&'u str> { - let raw = read_raw(body, pointer)?; - let inner = raw.strip_prefix(b"\"")?.strip_suffix(b"\"")?; - core::str::from_utf8(inner).ok() -} - -/// The integer value at one declared pointer, where it parses as a bare JSON number. -#[must_use] -pub fn read_u64(body: &[u8], pointer: &str) -> Option { - let raw = read_raw(body, pointer)?; - core::str::from_utf8(raw).ok()?.parse().ok() -} - -/// The egress-auth scheme an outbound hop to the provider is decorated under. -/// -/// The plane NAMES the scheme and never holds what is behind it. jev's auth is the core gateway's -/// own bearer credential (the signed design: "auth = core gateway credential, plane returns a -/// CredentialLocator, never sees the secret") — the same seam every other plane's egress hop uses. -pub(crate) const EGRESS_SCHEME: &str = "decision-egress"; - -/// The envelope member naming the document type of an outbound body. -pub(crate) const FIELD_CONTENT_TYPE: &str = "content-type"; - -/// The response field a refusal names its wait in: whole seconds until the caller may retry. -pub(crate) const FIELD_RETRY_AFTER: &str = "retry-after"; - -/// The document type every body of this protocol is. -pub(crate) const CONTENT_TYPE_JSON: &[u8] = b"application/json"; - -/// Build the bare JSON error object jev's refusal/error shape is: `{"error": {"code": .., -/// "message": ..}}`. Written by hand rather than through `serde_json::json!` so the byte order is -/// pinned and a conformance test can assert the exact bytes. -pub(crate) fn error_body(code: &str, message: &str) -> Vec { - format!( - "{{\"error\":{{\"code\":{},\"message\":{}}}}}", - serde_json::to_string(code).unwrap_or_else(|_| "\"internal\"".to_string()), - serde_json::to_string(message).unwrap_or_else(|_| "\"error\"".to_string()), - ) - .into_bytes() -} - -#[cfg(test)] -#[path = "../tests/codec.rs"] -mod tests; diff --git a/crates/busbar-plane-decisions/src/config.rs b/crates/busbar-plane-decisions/src/config.rs deleted file mode 100644 index 8f5e5d3856..0000000000 --- a/crates/busbar-plane-decisions/src/config.rs +++ /dev/null @@ -1,206 +0,0 @@ -//! The typed shape of the `decisions:` config section. -//! -//! The config-model ruling (owner, 2026-09-19, the jev v5 dictator sign-off) is explicit about -//! this section's shape: `DecisionsSection { models: Map, hooks, -//! upstream_credentials }`, reusing `ModelCfg` VERBATIM rather than inventing a second model type — -//! the uniform model-serving map applies to `pools` and `decisions` alike (per the ruling's -//! correction on the uniform `models` schema). This -//! is greenfield: `decisions` never shipped a `1.5.5` shape, so there is no migration to honor and -//! no byte-identity floor to hold — the section either parses or the whole document is refused, the -//! same fail-closed posture `deny_unknown_fields` gives every other section. -//! -//! `ModelCfg` is [`busbar_contract::config::ModelCfg`] (DECISIONS #40/#38 — moved out of -//! `busbar-kernel` so a plugin crate reusing it need not also depend on the kernel; see that -//! module's own doc). Same type, same field names, same wire bytes — only the module path moved. -//! -//! ## Canonical shape (from the ruling) -//! -//! ```yaml -//! decisions: -//! models: -//! jev: { provider: typesafe, upstream_model: jev-1.13.0 } -//! ``` -//! -//! `jev` here is the busbar-facing model name a caller targets; `provider` names a `providers:` -//! entry (the transport-agnostic connection — base_url, error_map, credential ref); `upstream_model` -//! is the real string sent to the provider, omitted meaning "send the key verbatim". -//! -//! ## Reserved sibling keys -//! -//! `hooks` and `upstream_credentials` are the two reserved members every model-serving section -//! carries alongside its `models` map (the ruling's R4/R-C: the shared pair stays `{hooks, -//! upstream_credentials}`, with `models` itself reserved only inside model-serving planes). `hooks` -//! is a list of names into the top-level `hooks:` registry — this crate carries no `HookCfg` type of -//! its own (that lives in busbar-core, which this crate may not depend on), so the reserved member -//! is typed as the list of names a plane names its hooks by, exactly as `busbar-plane-a2a`'s own -//! `CONFIG_SCHEMA` declares its `hooks` member. `upstream_credentials` reuses -//! [`busbar_contract::config::UpstreamCreds`] verbatim — the same type `PoolCfg` already names for -//! its own per-section override — rather than a second `own`/`passthrough` enum. -//! -//! That type used to live in the legacy plugin-facing crate, and naming it was the ONLY reason this -//! plane depended on that crate — the only one of the five that did. The dependency was not free: -//! it reached banned source through `sha2 -> cpufeatures -> libc`, which the transitive source -//! denylist forbids any pure plugin kind, and which went unreported for as long as this crate was -//! in no kind list. The type moved to the contract beside `ModelCfg`, so both reserved shapes now -//! arrive from the one crate a plugin may name, and the edge is deleted rather than waived. - -use std::collections::{HashMap, HashSet}; - -use serde::Deserialize; - -use busbar_contract::config::{ModelCfg, UpstreamCreds}; - -/// The `decisions:` section, typed. -/// -/// `deny_unknown_fields`: a typo'd top-level member of `decisions:` must fail boot, not be silently -/// ignored — the same posture every other section in this workspace takes. -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(deny_unknown_fields)] -pub struct DecisionsSection { - /// The busbar-facing model names this deployment exposes on the decisions plane, each bound to a - /// provider connection. REQUIRED-shaped in practice (an empty map means no `jev:` target is - /// reachable, which is a valid but inert configuration rather than a refusal). - #[serde(default)] - pub models: HashMap, - /// The reserved hook-name list every model-serving section carries. Each entry names an - /// existing top-level `hooks:` registration; validating that the name resolves and that the - /// hook it resolves to is one this plane may run is a boot-time concern this crate does not - /// perform standalone (it has no `HookCfg` to check a kind against) — the same posture - /// `busbar-plane-a2a`'s own reserved `hooks` member takes. - #[serde(default)] - pub hooks: Vec, - /// The reserved upstream-credential override: `own` (this deployment's own credential, the - /// default) or `passthrough` (relay the caller's). `None` means inherit whatever the - /// deployment-wide default is. - #[serde(default)] - pub upstream_credentials: Option, -} - -/// THE ONE WIRE DIALECT THIS PLANE SPEAKS. -/// -/// jev is a direct HTTP+JSON passthrough with no translating IR (`plane.rs`'s single -/// `wire_format_names` entry, `PLANE_DECL` in the composition root) — there is no dialect adapter -/// behind it the way the LLM plane's `openai`/`anthropic`/… translators exist. BUSBAR-1.6.0.md #51 -/// (OWNER-LOCKED 2026-09-20) rules the resolution/interpretation split explicitly: "the PLANE then -/// interprets the resolved dialect against what it supports: knows it ⇒ use it; doesn't ⇒ FAIL -/// (fail-closed) — e.g. the decisions plane (only jev) handed `anthropic` fails. Dialect validation -/// is the plane's job, never the kernel's." This constant is that knowledge, kept on the plane side -/// of the seam so no kernel file ever spells the literal `"jev"` (#49: the kernel names no plane or -/// transport-protocol string). -pub const PROTOCOL: &str = "jev"; - -/// THE SECOND WIRE DIALECT (THE DESIGN section 2, the decisions bullet): "a second dialect exists -/// only where a provider's request spine really differs (NanoJev is the one, dialect `nanojev`: -/// `/api/evaluate` is its second exact claim; a model's dialect is its override or its provider's -/// protocol ...)". -pub const PROTOCOL_NANOJEV: &str = "nanojev"; - -/// Every dialect this plane speaks, in its dialect-index order. -pub const PROTOCOLS: &[&str] = &[PROTOCOL, PROTOCOL_NANOJEV]; - -/// THE DECLARING SECTION: the top-level config key whose presence declares this plane, beside -/// `pools:`, `tools:`, `agents:` and `streams:`. The plane owns it and says so here; the composition -/// root reads this constant for its registry declaration and spells no literal of its own. -/// -/// Plural, and deliberately not the registry key: the key is the plane's identity (`decision`, -/// `PlaneMeta::KEY`) and the section is the operator's noun for the set of configured decision -/// models. -pub const SECTION: &str = "decisions"; - -/// CROSS-REFERENCE VALIDATION for a parsed `decisions:` section, run once the whole document is -/// known (siblings included) — the same moment the pools plane's own model→provider check runs -/// (`config_validate::validate`'s "model … references unknown provider" rule) and the `tools:` -/// plane's own hook-reference check runs (`config/mod.rs`'s `resolve`). This function is PURE and -/// kernel-free by construction (the dep wall, DECISIONS #40, forbids this crate naming a kernel -/// type), so a caller that holds the resolved document hands it borrowed, already-resolved -/// primitives (the served build makes the same two checks in the kernel, over the door's folded -/// section: its reserved `models` map's provider references and the dialects the door states): -/// -/// * `provider_protocols`: every configured `providers:` entry's NAME mapped to its RESOLVED -/// `protocol` (post catalog-merge, the same value `providers.

.protocol` resolves to -/// everywhere else) — so a `decisions.models..provider` reference is checked for EXISTENCE -/// and, when it exists, its dialect is checked against [`PROTOCOL`] (#51). -/// * `known_hooks`: every NAME defined in the top-level `hooks:` map — so a `decisions.hooks` -/// reference is checked for existence, exactly as `tools.hooks`/`tools..hooks` are. -/// -/// Returns one error string per violation (never panics, never short-circuits on the first one — -/// an operator with several mistakes sees all of them, matching every other collector in -/// `config_validate`). Each message NAMES THE KEY PATH (`decisions.models..provider`, -/// `decisions.hooks`) and the valid choices, in the same style as the existing -/// `config_validate::validate` refusals it mirrors ("model '{}' references unknown provider '{}'"). -pub fn validate_cross_refs( - section: &DecisionsSection, - provider_protocols: &HashMap, - known_hooks: &HashSet, -) -> Vec { - let mut errors = Vec::new(); - - let mut provider_names: Vec<&str> = provider_protocols.keys().map(String::as_str).collect(); - provider_names.sort_unstable(); - - let mut model_names: Vec<&str> = section.models.keys().map(String::as_str).collect(); - model_names.sort_unstable(); - for model_name in model_names { - let model_cfg = §ion.models[model_name]; - match provider_protocols.get(&model_cfg.provider) { - None => { - errors.push(format!( - "decisions.models.{model_name}.provider names '{}', which is not defined in \ - the top-level `providers:` map. Valid providers: {}. Define it there, or fix \ - the reference.", - model_cfg.provider, - if provider_names.is_empty() { - "(none configured)".to_string() - } else { - provider_names.join(", ") - } - )); - } - Some(protocol) - if !PROTOCOLS - .contains(&model_cfg.protocol.as_deref().unwrap_or(protocol.as_str())) => - { - let (key, protocol) = match model_cfg.protocol.as_deref() { - Some(own) => (format!("decisions.models.{model_name}.protocol"), own), - None => ( - format!( - "decisions.models.{model_name}.provider '{}'", - model_cfg.provider - ), - protocol.as_str(), - ), - }; - errors.push(format!( - "{key} resolves to protocol '{protocol}', but the decision plane speaks only \ - {} (BUSBAR-1.6.0.md #51: an unknown dialect FAILS CLOSED rather than boot). \ - Point decisions.models.{model_name} at a provider whose protocol resolves to \ - one of them, or remove this model.", - PROTOCOLS.join(", ") - )); - } - Some(_) => {} - } - } - - let mut hook_names: Vec<&str> = known_hooks.iter().map(String::as_str).collect(); - hook_names.sort_unstable(); - for hook in §ion.hooks { - if !known_hooks.contains(hook) { - errors.push(format!( - "decisions.hooks names '{hook}', which is not defined in the top-level `hooks:` \ - map. Valid hooks: {}. Define it there, or remove the reference.", - if hook_names.is_empty() { - "(none configured)".to_string() - } else { - hook_names.join(", ") - } - )); - } - } - - errors -} - -#[cfg(test)] -#[path = "tests/config.rs"] -mod tests; diff --git a/crates/busbar-plane-decisions/src/driven.rs b/crates/busbar-plane-decisions/src/driven.rs deleted file mode 100644 index 244aa1f9de..0000000000 --- a/crates/busbar-plane-decisions/src/driven.rs +++ /dev/null @@ -1,841 +0,0 @@ -//! This plane's answers to the kernel plane driver (`BUSBAR-1.6.0.md` Part 3, section 12, -//! "The plane driver"), in this plane's own vocabulary. -//! -//! The driver serves a unit in five crossings, and this module answers each one: -//! -//! | driver crossing | here | -//! |---|---| -//! | the Statement tail | [`tail`]: declaring and consumed sections, dialects, scope kinds, op classes, billable classes, fee units, needs, claims | -//! | `arrive` | [`arrive`]: the op class, the principal need and the dialect of a claimed request | -//! | `on_piece`, from the caller | [`caller_piece`]: the caller's whole body arrives as one piece; the kernel keeps it | -//! | `on_piece`, ATTEMPT (from the kernel) | [`attempt`]: the request bound for the far end, verb and target explicit | -//! | `on_piece`, from the far end | [`FarEndReading`]: the bytes relayed unchanged, and the units the far end reported | -//! | `refusal` | [`refusal_body`]: the kernel's status and text, in this dialect's error shape | -//! -//! Every answer is plain data. Nothing here holds a byte across calls: [`FarEndReading`] is a value -//! the caller owns for the length of one unit, in fixed space, and it is the only accumulating -//! thing in the module. -//! Writing these answers into the host buffers of the plane ABI (`busbar_contract::abi::plane`) is -//! the plane door's job, one generic adapter for every plane; it is not this plane's. -//! -//! MODEL RESOLUTION (THE DESIGN section 2, the decisions bullet; DECISIONS D8b): "a request routes -//! by its model; one configured model and none named is the default (byte-identical); more than one -//! and none named is 400; an unknown model is 404. `upstream_model` rewrites only the top-level model -//! value, by span splice, when it is set and differs; otherwise the body passes through." -//! -//! | config | `POST /v1/systemone` | -//! |---|---| -//! | `decisions.models: {}` | `404`, the kernel's unclaimed-route body (nothing is claimed) | -//! | one model, none named | routed over that model, the body byte for byte | -//! | several models, none named | `400` ([`Unrouted::Unnamed`]) | -//! | a model named | routed over it; a name no model has is `404` ([`Unrouted::Unknown`]) | -//! -//! With no model configured the snapshot publishes no claim ([`served_claims`]), so the request -//! falls through to the kernel's unclaimed-route body, -//! `{"error":{"code":null,"message":"the requested resource was not found","param":null,"type":"not_found_error"}}`, -//! byte for byte as before. The top-level `model` ([`codec::PTR_MODEL`]) is the one request member -//! this plane reads, and only to route ([`route_model`]) and splice ([`spliced`]). -//! -//! THE SECOND DIALECT (THE DESIGN section 2): "NanoJev is the one, dialect `nanojev`: `/api/evaluate` -//! is its second exact claim; a model's dialect is its override or its provider's protocol; a model -//! of the wrong dialect gets that dialect's existing unknown-model refusal; it meters the upstream's -//! own billing unit, counted from the answer already read (`execution.states`; an answer with no -//! count refuses loudly ...), with no extra egress". Each dialect's claim is published when the -//! generation configures a model of that dialect ([`served_claims`]); a request routes among the -//! models of the dialect its claim is ([`route_model`]), so a model of the other dialect is that -//! dialect's unknown model (`404`). Its answer is read by its dialect's count -//! ([`FarEndReading::for_dialect`]): `/execution/states`, required. -//! -//! WHAT THIS PLANE DOES NOT CLAIM: `GET /v1/models`. That path keeps its 1.5.5 bytes (the model -//! list busbar already serves there), so [`tail::CLAIMS`] and [`tail::OP_CLASSES`] carry `systemone` alone and -//! [`arrive`] answers the models operation as unclaimed. -//! -//! Money-blind, like the rest of the crate: the plane reports what a successful answer bills, in its -//! billable classes, and never what it is worth (THE DESIGN section 7, the owner money rulings -//! 2026-09-28: "bills one `decision` per successful, settled answer from every provider; its card -//! prices `decision`, `input_tokens` and `output_tokens` (family `decision`, all three required, 0 -//! = free); the hosted provider keeps 1.5.5's `/usage/units` as the decision count"). - -use busbar_contract::ids::{MeterClassId, OpClassId}; - -use crate::codec::{ - self, error_body, CONTENT_TYPE_JSON, EGRESS_SCHEME, FIELD_CONTENT_TYPE, PTR_EXECUTION_STATES, - PTR_USAGE_INPUT_TOKENS, PTR_USAGE_OUTPUT_TOKENS, PTR_USAGE_UNITS, -}; -use crate::ops; - -/// The plane's Statement tail, as data. -pub mod tail { - use super::{MeterClassId, OpClassId}; - use crate::{claims, config, meta, ops}; - - /// The top-level config section whose presence declares this plane. - pub const SECTION_DECLARING: &str = config::SECTION; - - /// The sections this plane reads and does not own: every model names its provider there. - pub const SECTIONS_CONSUMED: &[&str] = &["providers"]; - - /// The dialects this plane speaks, in index order: `jev` ([`DIALECT_JEV`]) and `nanojev` - /// ([`DIALECT_NANOJEV`]). - pub const DIALECTS: &[&str] = config::PROTOCOLS; - - /// The index of `jev` in [`DIALECTS`]. - pub const DIALECT_JEV: u32 = 0; - - /// The index of `nanojev` in [`DIALECTS`]. - pub const DIALECT_NANOJEV: u32 = 1; - - /// The outbound auth style a jev far end is reached under: the operator's credential as a - /// bearer (the signed design: "auth = core gateway credential, plane returns a - /// CredentialLocator, never sees the secret"). - pub const OUTBOUND_STYLE: &str = "bearer"; - - /// Each dialect's default outbound auth style, `(dialect index, style)` (the design, "Outbound auth", step 2: - /// a provider entry's `auth:`, else this; ARCHITECT Q-L1-AUTH (A), 2026-10-03). - pub const DIALECT_AUTH: &[(u32, &str)] = &[ - (DIALECT_JEV, OUTBOUND_STYLE), - (DIALECT_NANOJEV, OUTBOUND_STYLE), - ]; - - /// The resource granularity a grant names: a configured decision provider. - pub const SCOPE_KINDS: &[&str] = &["decision_provider"]; - - /// What one registration on this plane is called. - pub const SUBJECT_NOUN: &str = "decision provider"; - - /// The singular noun for one registration in admin responses. - pub const ADMIN_NOUN: &str = "decision-provider"; - - /// The record resource kind a registration is audited under: the scope kind. - pub const AUDIT_KIND: &str = SCOPE_KINDS[0]; - - /// The op classes this plane serves, in index order: each dialect's decision call (see the - /// module doc). - pub const OP_CLASSES: &[OpClassId] = &[ops::OP_SYSTEMONE, ops::OP_EVALUATE]; - - /// The billable classes, in index order, each with its family: the decision count, the fee - /// unit (also a class: the tail check holds `fee_units ⊆ billable_classes`), and the far end's - /// input and output tokens. THE DESIGN section 7 (owner money rulings 2026-09-28): the card - /// "prices `decision`, `input_tokens` and `output_tokens` (family `decision`, all three - /// required, 0 = free)". - pub const BILLABLE_CLASSES: &[(MeterClassId, &str)] = &[ - (meta::CLASS_DECISION, DECISION_FAMILY), - (MeterClassId::new(FEE_PER_REQUEST), FEE_FAMILY), - (CLASS_INPUT_TOKENS, DECISION_FAMILY), - (CLASS_OUTPUT_TOKENS, DECISION_FAMILY), - ]; - - /// The family every priced class of this plane is in. - pub const DECISION_FAMILY: &str = "decision"; - - /// The far end's input tokens, where its answer reports them (`/usage/input_tokens`). - pub const CLASS_INPUT_TOKENS: MeterClassId = MeterClassId::new("input_tokens"); - - /// The far end's output tokens, where its answer reports them (`/usage/output_tokens`). - pub const CLASS_OUTPUT_TOKENS: MeterClassId = MeterClassId::new("output_tokens"); - - /// The index of [`meta::CLASS_DECISION`] in [`BILLABLE_CLASSES`]. - pub const CLASS_DECISION_INDEX: u32 = 0; - - /// The index of [`CLASS_INPUT_TOKENS`] in [`BILLABLE_CLASSES`]. - pub const CLASS_INPUT_TOKENS_INDEX: u32 = 2; - - /// The index of [`CLASS_OUTPUT_TOKENS`] in [`BILLABLE_CLASSES`]. - pub const CLASS_OUTPUT_TOKENS_INDEX: u32 = 3; - - /// THE FEE UNIT: the request fee (`decisions.fees.per_request`), counted `1` on a unit whose - /// far end answered a success, so a refused or failed unit owes none (the signed design C-3, - /// "billable-success fee gated kernel-side", the tool plane's twin; ARCHITECT Q-L5-FEE (C)). A - /// fee unit is no priced class: the rate card never names it. - pub const FEE_PER_REQUEST: &str = busbar_contract::plane::PER_REQUEST; - - /// The family of the fee unit's class: a count of 0 or 1. - pub const FEE_FAMILY: &str = "count"; - - /// The index of the fee unit's class in [`BILLABLE_CLASSES`]. - pub const CLASS_FEE_INDEX: u32 = 1; - - /// The fee units this plane counts. - pub const FEE_UNITS: &[&str] = &[FEE_PER_REQUEST]; - - /// The connection needs, `(transport, auth)`, for the far-end direction: its dialect's style, the - /// one a member resolves to by default (a member is bound on the need its resolved style names). - /// The plane names the style and never holds what is behind it. - pub const NEEDS: &[(&str, &str)] = &[(claims::TRANSPORT, OUTBOUND_STYLE)]; - - /// The claims a generation snapshot may publish, `(verb, target, dialect)`: each dialect's one - /// exact claim. - pub const CLAIMS: &[(&str, &str, u32)] = &[ - ("POST", ops::PATH_SYSTEMONE, DIALECT_JEV), - ("POST", ops::PATH_EVALUATE, DIALECT_NANOJEV), - ]; -} - -/// The claims a generation's snapshot publishes, given the dialect of each model it configures: -/// each dialect's claim when at least one model is of it, none for none (see the module doc). -#[must_use] -pub fn served_claims(dialects: &[u32]) -> Vec<(&'static str, &'static str, u32)> { - tail::CLAIMS - .iter() - .filter(|(_, _, dialect)| dialects.contains(dialect)) - .copied() - .collect() -} - -/// A protocol's index in [`tail::DIALECTS`], where this plane speaks it. -#[must_use] -pub fn dialect_of(protocol: &str) -> Option { - tail::DIALECTS - .iter() - .position(|d| *d == protocol) - .and_then(|i| u32::try_from(i).ok()) -} - -/// Why a claimed request routes over no configured model (THE DESIGN section 2: "more than one and -/// none named is 400; an unknown model is 404"). -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Unrouted { - /// The request names no model and the generation configures several. - Unnamed, - /// The request's `model` is not a string. - NotAName, - /// The request names a model the generation does not configure. - Unknown(String), -} - -impl Unrouted { - /// The status the refusal wears. - #[must_use] - pub const fn status(&self) -> u16 { - match self { - Self::Unnamed | Self::NotAName => 400, - Self::Unknown(_) => 404, - } - } - - /// The words the caller reads, in the dialect's error shape ([`refusal_body`]). - #[must_use] - pub fn text(&self) -> String { - match self { - Self::Unnamed => "the request names no model, and several decisions models are \ - configured: name one in `model`" - .to_string(), - Self::NotAName => "`model` must be a string naming a decisions model".to_string(), - Self::Unknown(model) => { - format!("The model '{model}' does not exist or you do not have access to it.") - } - } - } -} - -/// The top-level `model` a request body names: `Ok(None)` when it names none, and -/// [`Unrouted::NotAName`] when the member is there but is not a string. -/// -/// # Errors -/// -/// [`Unrouted::NotAName`], as above. -pub fn requested_model(body: &[u8]) -> Result>, Unrouted> { - let Some(raw) = codec::read_raw(body, codec::PTR_MODEL) else { - return Ok(None); - }; - if let Ok(name) = serde_json::from_slice::<&str>(raw) { - return Ok(Some(std::borrow::Cow::Borrowed(name))); - } - serde_json::from_slice::(raw) - .map(|name| Some(std::borrow::Cow::Owned(name))) - .map_err(|_| Unrouted::NotAName) -} - -/// THE MODEL A CLAIMED REQUEST ROUTES OVER among the `models` its claim's dialect `serves` (the -/// generation's configured names, each with what this plane keeps of it): the one it names, else -/// the only one of that dialect configured. Answers the configured name and its entry. A model of -/// another dialect is this dialect's unknown model (THE DESIGN section 2: "a model of the wrong -/// dialect gets that dialect's existing unknown-model refusal"). -/// -/// # Errors -/// -/// [`Unrouted`]: several configured and none named, a `model` that is not a string, or a name none -/// of the dialect's models has. -pub fn route_model<'m, V>( - body: &[u8], - models: &'m std::collections::BTreeMap, - serves: impl Fn(&V) -> bool, -) -> Result<(&'m str, &'m V), Unrouted> { - match requested_model(body)? { - Some(name) => models - .get_key_value(name.as_ref()) - .filter(|(_, v)| serves(v)) - .map(|(k, v)| (k.as_str(), v)) - .ok_or_else(|| Unrouted::Unknown(name.into_owned())), - None => { - let mut all = models.iter().filter(|(_, v)| serves(v)); - match (all.next(), all.next()) { - (Some((k, v)), None) => Ok((k.as_str(), v)), - _ => Err(Unrouted::Unnamed), - } - } - } -} - -/// THE `upstream_model` SPLICE: `body` with its top-level `model` value replaced by `upstream`, when -/// the body names a model and it differs from `upstream`; `None` when the body passes through -/// unchanged. Every byte outside the value's span is kept: member order, whitespace and the rest of -/// the document are the caller's. -#[must_use] -pub fn spliced(body: &[u8], upstream: &str) -> Option> { - let busbar_contract::spans::Resolved::Found(span) = - busbar_contract::spans::resolve_pointer(body, codec::PTR_MODEL) - else { - return None; - }; - let named = requested_model(body).ok().flatten()?; - if named == upstream { - return None; - } - let value = serde_json::to_string(upstream).ok()?; - let mut out = Vec::with_capacity(body.len() - (span.end - span.start) + value.len()); - out.extend_from_slice(body.get(..span.start)?); - out.extend_from_slice(value.as_bytes()); - out.extend_from_slice(body.get(span.end..)?); - Some(out) -} - -/// Whether the kernel must verify a principal before the first piece. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum PrincipalNeed { - /// No principal. - None, - /// A principal is verified before the first piece. - Required, - /// A principal is used when one is presented. - Optional, -} - -/// A claimed request, classified. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Arrived { - /// The operation, by name. - pub op: OpClassId, - /// Its index in [`tail::OP_CLASSES`]. - pub op_class: u32, - /// Whether a principal is verified first. - pub principal: PrincipalNeed, - /// Its index in [`tail::DIALECTS`]. - pub dialect: u32, -} - -/// Classify an arriving request by its verb and target: its operation, and the dialect its claim -/// is. `None` for anything this plane does not claim, including `GET /v1/models`. -#[must_use] -pub fn arrive(verb: &str, target: &str) -> Option { - let row = ops::row_for(verb, target)?; - let op_class = tail::OP_CLASSES.iter().position(|op| *op == row.op)?; - let dialect = tail::CLAIMS - .iter() - .find(|(v, t, _)| *t == target && v.eq_ignore_ascii_case(verb))? - .2; - Some(Arrived { - op: row.op, - op_class: u32::try_from(op_class).ok()?, - // A decision is billed to someone: the provider account is the deployment's, and the unit - // is the caller's. - principal: PrincipalNeed::Required, - dialect, - }) -} - -/// What the plane says to a piece of the caller's body. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CallerAnswer { - /// Nothing to emit. The kernel keeps the body and re-pushes it on every attempt. - Keep, - /// The body ended empty: a `systemone` request carries the caller's state, so there is nothing - /// to forward. The kernel refuses the unit. - Empty, -} - -/// Answer a caller piece. The kernel gathers the whole body and pushes it as one piece. -#[must_use] -pub fn caller_piece(bytes: &[u8], last: bool) -> CallerAnswer { - if last && bytes.is_empty() { - CallerAnswer::Empty - } else { - CallerAnswer::Keep - } -} - -/// The request bound for the far end, answered to an ATTEMPT piece. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct FarEndRequest<'a> { - /// The verb, explicit. - pub verb: &'static str, - /// The target, explicit. - pub target: &'static str, - /// The dialect fields. The kernel adds the auth fields for [`FarEndRequest::auth`]. - pub fields: [(&'static str, &'static [u8]); 1], - /// The auth scheme the kernel decorates the request under. - pub auth: &'static str, - /// The caller's body, unchanged: byte identity is the dialect. - pub body: &'a [u8], -} - -/// Answer an ATTEMPT piece of a unit of `dialect`: the same request on every attempt, whichever -/// member the kernel picked, at the dialect's own path. Neither dialect names a provider on the -/// wire, so the member changes nothing in the request. Named `attempt_request`, not `attempt`, so -/// it is not a cross-plane re-spelling of llm/a2a's `attempt` (structure-lint plane-dup; ARCHITECT -/// ruling 2b 2026-10-04). -#[must_use] -pub fn attempt_request(dialect: u32, caller_body: &[u8]) -> FarEndRequest<'_> { - let target = tail::CLAIMS - .iter() - .find(|(_, _, d)| *d == dialect) - .map_or(ops::PATH_SYSTEMONE, |(_, t, _)| t); - FarEndRequest { - verb: "POST", - target, - fields: [(FIELD_CONTENT_TYPE, CONTENT_TYPE_JSON)], - auth: EGRESS_SCHEME, - body: caller_body, - } -} - -/// One unit count, as the plane reports it. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Units { - /// Index into [`tail::BILLABLE_CLASSES`]. - pub class: u32, - /// Reported by the far end. The plane never estimates a decision count. - pub reported: bool, - /// The cumulative count. - pub amount: u64, -} - -/// A usage member the far end's success answer carries but that is not a whole count: the money -/// rule's loud failure (finding 395, kept: "a usage member present but not a whole count is -/// LOCATED, never dropped"), never a silent 0. Names the member's pointer. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Unreadable(pub &'static str); - -/// What one answer bills, in [`tail::BILLABLE_CLASSES`]' order of reading: the decision count, -/// the input and output tokens, and the request fee. `None` = nothing in that class. -pub type Counts = [Option; 4]; - -/// A usage member as the answer states it. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -enum Member { - /// The answer has no such member. - #[default] - Absent, - /// A whole count. - Whole(u64), - /// Present, and not a whole count. - Unreadable, -} - -/// What a member of the answer is to the reading: `/error`, `/usage`, one of the three usage -/// counts (by its index in its dialect's [`Usage::counted`]), or none of them. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -enum Slot { - #[default] - Other, - Error, - Usage, - Count(usize), -} - -/// Where a dialect's answer states its counts: the top-level member holding them, and the members -/// under it, `(name, pointer)`, in [`Counts`]' order (decision, input tokens, output tokens). -/// Usage pointers are dialect data (THE DESIGN section 2). -struct Usage { - parent: &'static [u8], - counted: &'static [(&'static [u8], &'static str)], - /// The decision count is required: an answer with none refuses loudly. - required: bool, -} - -/// Each dialect's [`Usage`], in [`tail::DIALECTS`]' order: `jev` reads `/usage/units` (one decision -/// when absent) and its tokens; `nanojev` reads the upstream's own billing unit, -/// `/execution/states`, and an answer with no count refuses loudly. -const USAGE: [Usage; 2] = [ - Usage { - parent: b"usage", - counted: &[ - (b"units", PTR_USAGE_UNITS), - (b"input_tokens", PTR_USAGE_INPUT_TOKENS), - (b"output_tokens", PTR_USAGE_OUTPUT_TOKENS), - ], - required: false, - }, - Usage { - parent: b"execution", - counted: &[(b"states", PTR_EXECUTION_STATES)], - required: true, - }, -]; - -/// The longest member name compared ("output_tokens"), with room. -const KEY_CAP: usize = 16; -/// The longest scalar a whole count is spelled in (`u64::MAX` is 20 digits), with room. -const SCALAR_CAP: usize = 24; - -/// The far end's answer, read as it is relayed. -/// -/// Every piece is relayed to the caller unchanged, as it arrives, and read as it passes: a scan of -/// the JSON structure that keeps only what the money rule reads (whether `/error` is there and not -/// `null`, and the dialect's counts: `/usage/*`, or `/execution/states`), in fixed space. No byte of the answer is kept, so an -/// answer of any size is read in the same few bytes (THE DESIGN section 11.2: "no allocation and no -/// blocking on the hot path"), and the pieces may split the answer anywhere. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct FarEndReading { - /// The nesting depth: `1` inside the top-level value. - depth: u32, - /// Whether the container at depth 1 and at depth 2 is an object. - object: [bool; 3], - /// In an object at depth 1 or 2: the next string is a member name. - want_key: bool, - /// Inside a string; the string is a member name; the last byte was its escape. - in_str: bool, - str_key: bool, - escaped: bool, - /// The member name being read (cut at [`KEY_CAP`]: a longer one is none compared). - key: [u8; KEY_CAP], - key_len: usize, - /// The member a value at depth 1, and at depth 2, belongs to. - at: [Slot; 3], - /// The scalar being read for `scalar_of` (cut at [`SCALAR_CAP`]: a longer one is no count). - scalar: [u8; SCALAR_CAP], - scalar_len: usize, - scalar_of: Option, - /// `/error`: absent (`None`), `null` (`Some(false)`), or there (`Some(true)`). - error: Option, - /// The counts, in the dialect's [`Usage::counted`] order. - members: [Member; 3], - /// The dialect the answer is in ([`USAGE`]'s index). - dialect: u32, -} - -impl FarEndReading { - /// A `jev` reading with nothing received. - #[must_use] - pub fn new() -> Self { - Self::default() - } - - /// A reading of an answer in `dialect` ([`tail::DIALECTS`]' index), nothing received. - #[must_use] - pub fn for_dialect(dialect: u32) -> Self { - Self { - dialect, - ..Self::default() - } - } - - /// The dialect's [`Usage`]. - fn usage(&self) -> &'static Usage { - USAGE.get(self.dialect as usize).unwrap_or(&USAGE[0]) - } - - /// Take one far-end piece and return the bytes to relay to the caller: the piece itself. - pub fn piece<'p>(&mut self, bytes: &'p [u8]) -> &'p [u8] { - for &b in bytes { - self.byte(b); - } - bytes - } - - /// The member at the current depth a value starting now belongs to. - fn slot(&self) -> Slot { - match self.depth { - 1 if self.object[1] => self.at[1], - 2 if self.object[2] && self.at[1] == Slot::Usage => self.at[2], - _ => Slot::Other, - } - } - - /// Whether the container the reading is in is an object it reads member names of. - fn in_object(&self) -> bool { - matches!(self.depth, 1 | 2) && self.object[self.depth as usize] - } - - /// A value starts: `scalar` = it is a bare scalar (a number, `true`, `false`, `null`). - fn value(&mut self, scalar: Option) { - let slot = self.slot(); - match (slot, scalar) { - (Slot::Error, None) => self.error = Some(true), - (Slot::Count(i), None) => self.members[i] = Member::Unreadable, - (Slot::Error | Slot::Count(_), Some(b)) => { - self.scalar[0] = b; - self.scalar_len = 1; - self.scalar_of = Some(slot); - } - _ => {} - } - } - - /// The scalar being read, ended. - fn end_scalar(&mut self) { - let Some(slot) = self.scalar_of.take() else { - return; - }; - let raw = &self.scalar[..self.scalar_len]; - match slot { - Slot::Error => self.error = Some(raw != b"null"), - Slot::Count(i) => { - self.members[i] = if self.scalar_len > SCALAR_CAP - 1 { - Member::Unreadable - } else { - whole(raw).map_or(Member::Unreadable, Member::Whole) - }; - } - _ => {} - } - } - - /// A member name, read whole. - fn end_key(&mut self) { - let key = &self.key[..self.key_len.min(KEY_CAP)]; - let named = if self.key_len > KEY_CAP { - Slot::Other - } else if self.depth == 1 { - match key { - b"error" => Slot::Error, - k if k == self.usage().parent => Slot::Usage, - _ => Slot::Other, - } - } else { - self.usage() - .counted - .iter() - .position(|(name, _)| *name == key) - .map_or(Slot::Other, Slot::Count) - }; - if let 1 | 2 = self.depth { - self.at[self.depth as usize] = named; - } - } - - /// One byte of the answer. - fn byte(&mut self, b: u8) { - if self.in_str { - if self.escaped { - self.escaped = false; - } else if b == b'"' { - self.in_str = false; - if self.str_key { - self.end_key(); - } - return; - } else if b == b'\\' { - self.escaped = true; - } - if self.str_key { - if self.key_len < KEY_CAP { - self.key[self.key_len] = b; - } - self.key_len = self.key_len.saturating_add(1); - } - return; - } - if self.scalar_of.is_some() { - if !matches!( - b, - b' ' | b'\t' | b'\n' | b'\r' | b',' | b'}' | b']' | b':' | b'"' | b'{' | b'[' - ) { - if self.scalar_len < SCALAR_CAP { - self.scalar[self.scalar_len] = b; - self.scalar_len += 1; - } - return; - } - self.end_scalar(); - } - match b { - b' ' | b'\t' | b'\n' | b'\r' | b':' => {} - b'"' => { - self.in_str = true; - self.escaped = false; - self.str_key = self.in_object() && self.want_key; - if self.str_key { - self.key_len = 0; - self.want_key = false; - } else { - self.value(None); - } - } - b'{' | b'[' => { - self.value(None); - self.depth = self.depth.saturating_add(1); - if let 1 | 2 = self.depth { - self.object[self.depth as usize] = b == b'{'; - self.at[self.depth as usize] = Slot::Other; - } - self.want_key = b == b'{'; - } - b'}' | b']' => { - self.depth = self.depth.saturating_sub(1); - self.want_key = false; - } - b',' => self.want_key = self.in_object(), - _ => self.value(Some(b)), - } - } - - /// The reading as the answer stands: a scalar the answer ended in is read as it is. - fn settled(&self) -> (Option, [Member; 3]) { - let mut done = self.clone(); - done.end_scalar(); - (done.error, done.members) - } - - /// Whether the answer, with the far end's `status`, is a SUCCESS: a `2xx` status and no - /// `/error` member (a `null` one is none). The one success rule every count below reads. - #[must_use] - pub fn success(&self, status: Option) -> bool { - status.is_some_and(|s| (200..300).contains(&s)) && self.settled().0 != Some(true) - } - - /// WHAT THE ANSWER BILLS, read at the last piece (THE DESIGN section 7, the owner money rulings): - /// a non-success bills nothing; a success bills one `decision`, or the whole count its - /// `/usage/units` reports (the hosted provider's count, DECISIONS D9), its `/usage/input_tokens` - /// and `/usage/output_tokens` where it reports them, and the request fee (`1` of - /// [`tail::FEE_PER_REQUEST`]: a refused or failed unit owes none). - /// - /// # Errors - /// - /// [`Unreadable`]: a success whose usage member is present but is not a whole count a signed - /// 64-bit count holds (negative, fractional, a string, `null`, past `i64::MAX`). - pub fn counts(&self, status: Option) -> Result { - if !self.success(status) { - return Ok([None; 4]); - } - let (_, members) = self.settled(); - let usage = self.usage(); - let mut whole = [None; 3]; - for (i, (_, pointer)) in usage.counted.iter().enumerate() { - whole[i] = match members[i] { - Member::Absent if i == 0 && usage.required => return Err(Unreadable(pointer)), - Member::Absent => None, - Member::Whole(n) => Some(n), - Member::Unreadable => return Err(Unreadable(pointer)), - }; - } - let reported = |class: u32, amount: u64| Units { - class, - reported: true, - amount, - }; - Ok([ - Some(reported(tail::CLASS_DECISION_INDEX, whole[0].unwrap_or(1))), - whole[1].map(|n| reported(tail::CLASS_INPUT_TOKENS_INDEX, n)), - whole[2].map(|n| reported(tail::CLASS_OUTPUT_TOKENS_INDEX, n)), - Some(reported(tail::CLASS_FEE_INDEX, 1)), - ]) - } -} - -/// A usage member's raw bytes read as a whole count a signed 64-bit count holds: ASCII digits -/// alone, at most `i64::MAX`. -#[must_use] -pub fn whole(raw: &[u8]) -> Option { - if raw.is_empty() || !raw.iter().all(u8::is_ascii_digit) { - return None; - } - core::str::from_utf8(raw) - .ok()? - .parse::() - .ok() - .filter(|n| i64::try_from(*n).is_ok()) -} - -/// THE CALLER FIELDS THE FAR END NEVER RECEIVES: the ones the plane writes itself (the document -/// type), the ones naming the hop (`host`), and the credential carriers (the far end is presented -/// the member's credential by the kernel, never the caller's). Compared without case. Hop-by-hop -/// fields and the body framing never reach the plane (the framer drops them). -pub const NOT_RELAYED: &[&str] = &[ - FIELD_CONTENT_TYPE, - "host", - "authorization", - "proxy-authorization", -]; - -/// The caller's fields the far end receives, held in one buffer per unit: each field's name -/// (lowercased) and value, back to back, and where each ends. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct Relayed { - bytes: Vec, - /// Per field, `(end of its name, end of its value)` in `bytes`. - ends: Vec<(usize, usize)>, -} - -impl Relayed { - /// Every field, `(name, value)`, in the caller's order. - pub fn iter(&self) -> impl Iterator + '_ { - let mut from = 0; - self.ends.iter().map(move |&(name, value)| { - let field = (&self.bytes[from..name], &self.bytes[name..value]); - from = value; - field - }) - } - - /// Whether no field is relayed. - #[must_use] - pub fn is_empty(&self) -> bool { - self.ends.is_empty() - } -} - -/// Whether the caller's field `name` reaches the far end. -fn relayed(name: &[u8]) -> bool { - !NOT_RELAYED - .iter() - .any(|n| name.eq_ignore_ascii_case(n.as_bytes())) -} - -/// THE CALLER'S FIELDS THE FAR END RECEIVES, in the order the caller sent them, a repeated name -/// keeping every value: every field the caller sent but [`NOT_RELAYED`]'s (a same-dialect relay -/// passes every header but the governed set; ARCHITECT DEC-SERVE Q2, the OWNER's DIALECT-FIDELITY -/// F2). Names are lowercased, as the wire compares them. Kept in one buffer sized once. -#[must_use] -pub fn relayed_fields<'f, I>(caller: I) -> Relayed -where - I: IntoIterator, - I::IntoIter: Clone, -{ - let fields = caller.into_iter(); - let (mut size, mut count) = (0, 0); - for (name, value) in fields.clone().filter(|(name, _)| relayed(name)) { - size += name.len() + value.len(); - count += 1; - } - let mut kept = Relayed { - bytes: Vec::with_capacity(size), - ends: Vec::with_capacity(count), - }; - for (name, value) in fields.filter(|(name, _)| relayed(name)) { - kept.bytes.extend(name.iter().map(u8::to_ascii_lowercase)); - let name_end = kept.bytes.len(); - kept.bytes.extend_from_slice(value); - kept.ends.push((name_end, kept.bytes.len())); - } - kept -} - -/// Render a refusal the kernel decided, in this dialect's error shape: `{"error": {"code", "message"}}`. -/// The kernel chose the status and wrote the text; the plane chooses only the code word for the -/// status. -#[must_use] -pub fn refusal_body(status: u16, text: &str) -> Vec { - let code = match status { - 403 | 429 => "unsupported_operation", - 404 => "invalid_params", - 400..=499 => "invalid_request", - 500 => "internal", - _ => "unsupported_operation", - }; - error_body(code, text) -} - -#[cfg(test)] -#[path = "tests/driven.rs"] -mod tests; diff --git a/crates/busbar-plane-decisions/src/facts.rs b/crates/busbar-plane-decisions/src/facts.rs deleted file mode 100644 index 981f04f66c..0000000000 --- a/crates/busbar-plane-decisions/src/facts.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! The fact keys this plane writes, and the ones it deliberately never does. -//! -//! A fact is evidence. It is never an amount, never a decision and never a credential — and here it -//! is never the decision CONTENT either. jev's `systemone` request carries a caller-supplied -//! `state`; its response (success or 422) carries the provider's `answers`. Both are the payload a -//! `decision` plane exists to move, not evidence a fact map should carry forward into a record or -//! an export. So this module declares metadata keys ONLY, and `codec.rs`'s declared-pointer list — -//! the only pointers this plane ever resolves — has no entry for either member. A key that is never -//! declared here cannot leak through `Facts::set`, because nothing in this crate ever builds the -//! value to set it to. - -/// Which of the two operations a request was, as the wire spelled the path. -pub const FACT_OP: &str = "op"; - -/// The request identifier the provider's answer carried, where it did. -/// -/// jev correlates on a body-level `id`/`request_id` member — plain metadata, never the decision -/// content. It names ONE exchange, so it is a response fact and never a session fact. -pub const FACT_REQUEST_ID: &str = "request_id"; - -/// The configured provider a unit is dialled against — its operator-given id, the same name the -/// scope unit judges. Written on every draft where exactly one provider resolves (`plane.rs`'s -/// `provider`), and absent where none does. -pub const FACT_PROVIDER: &str = "provider"; - -/// Whether the response carried a top-level `error` member. -/// -/// This is how the metering step tells a billable success from a 4xx without reading the decision -/// content: `error` present means nothing is metered; absent means the usage pointer (if the -/// response carried one) is read. -pub const FACT_HAS_ERROR: &str = "has_error"; - -/// The provider's own billing unit count for a `systemone` call, where a success response reports -/// one. Metadata about HOW MUCH was billed, never WHAT was decided. -pub const FACT_USAGE_UNITS: &str = "usage_units"; - -/// The session fact keys this plane writes. -/// -/// jev has no session-scoped identity of its own beyond the provider it was dialled against — the -/// provider name IS the session-scoped fact, because a session's provider changing mid-flight would -/// be a different priced thing, and the kernel sees that from the outside rather than inferring it. -/// The request identifier is not here: it changes on every exchange. -pub const SESSION_FACTS: &[&str] = &[FACT_PROVIDER]; - -/// The content fact keys this plane produces. -/// -/// What the record and the export path receive: which operation this was, whether it errored, and -/// how much was billed. Never the `state` a caller sent and never the `answers` a provider -/// returned — see the module note. -pub const CONTENT_FACTS: &[&str] = &[FACT_OP, FACT_HAS_ERROR, FACT_USAGE_UNITS]; - -/// The literal member names this plane forbids itself from ever resolving a pointer at. -/// -/// Not read anywhere in this crate's decode/encode path — listed here once, so the PII witness test -/// (`tests/jev.rs::pii_witness_never_surfaces_state_or_answers_in_any_fact`) and a reviewer both -/// have one place that names the two members a -/// `decision` plane's whole design exists to keep off the fact/record surface. -pub const NEVER_READ_MEMBERS: &[&str] = &["state", "answers"]; - -#[cfg(test)] -#[path = "tests/facts.rs"] -mod tests; diff --git a/crates/busbar-plane-decisions/src/lib.rs b/crates/busbar-plane-decisions/src/lib.rs deleted file mode 100644 index f7565fd355..0000000000 --- a/crates/busbar-plane-decisions/src/lib.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! The decisions plane: what bytes mean, for the `jev` dialect. -//! -//! ## What this crate is -//! -//! An ADAPTER, over a protocol this crate speaks for itself rather than borrowing from a codec -//! crate one hop away: jev is two small HTTP+JSON operations (`POST /v1/systemone`, -//! `GET /v1/models`), byte-identity passthrough to `api.typesafe.ai`, and there is no separate -//! wire vocabulary crate to adapt over the way `busbar-llm-codec` still exists for -//! their planes. The signed design (DECISION #39, jev v5 sign-off) rules this deliberately: ONE -//! crate, not a pure-plane-plus-impure-host split. See `Cargo.toml`'s header for why that also -//! means this crate names `busbar-substrate` — a widening its pure siblings do not carry. -//! -//! ## What this crate is not -//! -//! It holds no governance, no breaker, no hook seat, no signing key and no arithmetic over a -//! metered quantity. Those are units, and a unit is on the far side of the kernel from a plane. The -//! metering method here returns LOCATORS — the class, and the quantity this codec already read off -//! a SUCCESS response — and never a price, never a hold and never a decision. The routing method -//! returns a plan and never a connection. Nothing in this crate opens a socket, reads a file or -//! reads a clock other than the one the context hands it. -//! -//! ## What it holds across calls -//! -//! Nothing. The plane is a value with no interior mutability, asserted by a test rather than by a -//! comment. jev's two operations are each complete in one request/response pair — there is no -//! streamed answer and no task-like durable state — so this plane keeps no per-connection codec -//! state beyond what every plane's session machinery already provides. -//! -//! ## The PII witness, stated once -//! -//! A `systemone` exchange carries a caller-supplied `state` and the provider's `answers` — the -//! decision content itself. This plane never reads either: every pointer it resolves is declared in -//! `codec.rs`, and neither `state` nor `answers` is on that list. The body is still forwarded -//! byte-identically (the point of a passthrough dialect), but nothing this plane READS — no fact, -//! no record leg, no content fact — can ever carry those bytes, because it never looked at them. -//! `tests/jev.rs::pii_witness_never_surfaces_state_or_answers_in_any_fact` drives a fixture whose -//! body echoes both and asserts no fact this plane emits contains either value. - -#![forbid(unsafe_code)] -#![deny(missing_docs)] - -pub mod claims; -pub mod codec; -pub mod config; -pub mod driven; -pub mod facts; -pub mod meta; -pub mod ops; -pub mod plane; -pub mod plane_door; -pub mod records; - -use busbar_contract::plugin::{AbiVersion, Kind, Plugin}; - -/// One configured decision provider this plane may name. -/// -/// Every string is borrowed for the life of the program, because a plane's declarations are read at -/// registration and sealed. The seam a configured name must reach here through is -/// [`busbar_contract::ids::Registration`]: whoever builds this plane from a `decisions:` block -/// interns every config-derived key through it exactly once and hands over names that outlive it. -/// -/// Nothing does that: the plane is served through its door ([`plane_door`]), which answers the plane -/// driver from [`driven`] and never builds a `DecisionPlane`, so outside this crate's own tests no -/// `DecisionProvider` is constructed. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct DecisionProvider { - /// The name the operator gave this provider, and the resource the scope unit judges. - pub id: &'static str, - /// The priced lane this provider is reached on. - pub lane: busbar_contract::ids::LaneId, - /// The host to dial. - pub host: &'static str, - /// The transport the hop is made over. jev rides plain HTTP. - pub transport: &'static str, -} - -/// The decisions plane. -/// -/// The one field is a borrowed, immutable list. There is no cell here, no lock and no atomic: the -/// purity test asserts that by walking the type, not by trusting this sentence. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct DecisionPlane { - providers: &'static [DecisionProvider], -} - -impl DecisionPlane { - /// A plane with a configured provider set. - #[must_use] - pub const fn new(providers: &'static [DecisionProvider]) -> Self { - Self { providers } - } - - /// A plane with nothing configured. - /// - /// It answers every question the loop asks, and its answer to "where does this go" is a - /// destination the trust unit refuses. That is the honest answer for a plane with no provider — - /// not a panic, and not a fabricated host. - pub const EMPTY: Self = Self::new(&[]); - - /// The configured providers, in declaration order. - #[must_use] - pub const fn providers(&self) -> &'static [DecisionProvider] { - self.providers - } -} - -impl Default for DecisionPlane { - fn default() -> Self { - Self::EMPTY - } -} - -impl Plugin for DecisionPlane { - fn key(&self) -> &'static str { - ::KEY - } - - fn kind(&self) -> Kind { - Kind::Plane - } - - fn abi(&self) -> AbiVersion { - AbiVersion(1) - } -} - -#[cfg(test)] -#[path = "tests/lib.rs"] -mod tests; diff --git a/crates/busbar-plane-decisions/src/meta.rs b/crates/busbar-plane-decisions/src/meta.rs deleted file mode 100644 index 23c743138f..0000000000 --- a/crates/busbar-plane-decisions/src/meta.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! What this plane declares about itself, through the contract's own `PlaneMeta` trait. -//! -//! Everything here is a constant, because everything here is read once at registration and sealed -//! into policy. This is the contract's own trait, which `plane.rs`'s `impl Plane for DecisionPlane` -//! is checked against regardless of whether anything outside this crate has been told the plane -//! exists. It is the ONLY declaration this crate makes about itself: the kernel-shaped `PlaneDecl` -//! constant that used to sit beside it in `registry.rs` is gone — it was a generation-1 registration -//! nothing ever read, and carrying it cost this plugin crate a `busbar-kernel` dependency the -//! dep-wall forbids. - -use busbar_contract::grammar::Claim; -use busbar_contract::ids::{ - AdminVerbId, ClassDirection, MeterClassDecl, MeterClassId, OpClassId, RecordSchemaId, -}; -use busbar_contract::plane::PlaneMeta; - -use crate::{claims, facts, ops, records, DecisionPlane}; - -/// The family the billable-decision class rolls up into. -const DECISION_FAMILY: &str = "decision"; - -/// The class key jev meters under. -/// -/// ONE class, priced on the provider's own success billing. The signed design's C-3 rules -/// "billable-success fee gated kernel-side, `units_decision` twin of `units_mcp`" — this plane's -/// job is the twin of `busbar-plane-mcp`'s own metering surface: a locator naming the class and the -/// quantity this codec already read off a SUCCESS response, direction `Response` because the -/// quantity is sized from what the provider answered, never from what the caller asked. A 4xx -/// answer (the provider blamed the request) has nothing extractable under this class — see -/// `plane.rs::meter` and the `billable_success_only` test. -pub const CLASS_DECISION: MeterClassId = MeterClassId::new("decision"); - -/// The meter classes this plane declares. -const METER_CLASSES: &[MeterClassDecl] = &[MeterClassDecl { - key: CLASS_DECISION, - family: DECISION_FAMILY, - direction: ClassDirection::Response, - // The provider's own usage unit is the quantity; nothing here divides it further. - default_divisor: 1, -}]; - -/// The schema of this plane's own configuration block. -/// -/// One level deep: a map from busbar-facing model name to that model's definition (`provider` -/// required, `upstream_model` optional — the shape `busbar_contract::config::ModelCfg` -/// already parses, see `config.rs`), plus the two reserved members every model-serving section -/// carries. Nothing here is a credential and nothing here is a price. -const CONFIG_SCHEMA: &str = r#"{ - "type": "object", - "additionalProperties": false, - "properties": { - "models": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": false, - "properties": { - "provider": { "type": "string" }, - "upstream_model": { "type": "string" }, - "max_requests": { "type": "integer" }, - "max_concurrent": { "type": "integer" }, - "default_max_tokens": { "type": "integer" }, - "attempt_timeout_ms": { "type": "integer" }, - "reasoning": { "type": "boolean" }, - "prompt_caching": { "type": "boolean" }, - "protocol": { "type": "string" } - }, - "required": ["provider"] - } - }, - "hooks": { "type": "array", "items": { "type": "string" } }, - "upstream_credentials": { "type": "string", "enum": ["own", "passthrough"] } - } -}"#; - -impl PlaneMeta for DecisionPlane { - const KEY: &'static str = "decision"; - const CLAIMS: &'static [Claim] = claims::CLAIMS; - const OP_CLASSES: &'static [OpClassId] = ops::OP_CLASSES; - const METER_CLASSES: &'static [MeterClassDecl] = METER_CLASSES; - const SESSION_FACTS: &'static [&'static str] = facts::SESSION_FACTS; - const CONTENT_FACTS: &'static [&'static str] = facts::CONTENT_FACTS; - const RECORD_SCHEMAS: &'static [RecordSchemaId] = records::RECORD_SCHEMAS; - // jev has no introspection verb of its own today — the provider set is a plain config read, - // not a live catalogue with state worth projecting (unlike A2A's configured-agent list). - const INTROSPECTION_VERBS: &'static [AdminVerbId] = &[]; - // jev's answers are single request/response pairs; nothing supersedes an open one. - const INTERRUPT_FACT: Option<&'static str> = None; - // Nothing paces jev's write path — no streamed answer to pace. - const EGRESS_PACING_FACT: Option<&'static str> = None; - const CONFIG_SCHEMA: &'static str = CONFIG_SCHEMA; -} - -#[cfg(test)] -#[path = "tests/meta.rs"] -mod tests; diff --git a/crates/busbar-plane-decisions/src/ops.rs b/crates/busbar-plane-decisions/src/ops.rs deleted file mode 100644 index 947b2f9045..0000000000 --- a/crates/busbar-plane-decisions/src/ops.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! The operation vocabulary: which `(method, path)` is which priced operation. -//! -//! jev serves exactly two operations, both byte-identity passthrough to `api.typesafe.ai` -//! (JEV-DESIGN-SIGNED-v4.md, "C-6 REVERSED"): the decision call and the model catalogue. Unlike -//! MCP/A2A, which name their operation in a JSON-RPC method member, jev names it in the HTTP verb -//! and path — so the table below is keyed on those, read off the transport's own reserved facts -//! (`busbar_contract::transport::facts::PATH`/`METHOD`), the same seam `busbar-plane-a2a`'s -//! `surface_of` reads. - -use busbar_contract::ids::OpClassId; - -/// `POST /v1/systemone` — the decision call. Priced on the request it carries and metered on the -/// usage the SUCCESS response reports (billable-success only, per the signed design's C-3). -pub const OP_SYSTEMONE: OpClassId = OpClassId::new("systemone"); - -/// `GET /v1/models` — the provider's own model catalogue, relayed byte-identically. No busbar -/// catalogue synthesis: this is typesafe's list, and only this plane may serve it. -pub const OP_MODELS: OpClassId = OpClassId::new("models"); - -/// `POST /api/evaluate` — the decision call of the second dialect, `nanojev` (THE DESIGN section 2: -/// "`/api/evaluate` is its second exact claim"). -pub const OP_EVALUATE: OpClassId = OpClassId::new("evaluate"); - -/// Every operation class this plane's units can be, in declaration order. -pub const OP_CLASSES: &[OpClassId] = &[OP_SYSTEMONE, OP_MODELS, OP_EVALUATE]; - -/// The path jev's decision call is mounted on. -pub const PATH_SYSTEMONE: &str = "/v1/systemone"; - -/// The path jev's model catalogue is mounted on. -pub const PATH_MODELS: &str = "/v1/models"; - -/// The path the `nanojev` dialect's decision call is mounted on. -pub const PATH_EVALUATE: &str = "/api/evaluate"; - -/// One row of the method table: which `(verb, path)` is which operation, and whether the request -/// carries a body this plane must wait for. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct MethodRow { - /// The operation class this row declares. - pub op: OpClassId, - /// The HTTP verb the request arrives as. - pub verb: &'static str, - /// The path the request arrives on. - pub path: &'static str, - /// Whether this operation's REQUEST carries a body this plane must read before it is complete. - /// `systemone` is a POST with a JSON body; `models` is a bodyless GET. - pub has_request_body: bool, -} - -/// The method table, in declaration order. -pub const METHODS: &[MethodRow] = &[ - MethodRow { - op: OP_SYSTEMONE, - verb: "POST", - path: PATH_SYSTEMONE, - has_request_body: true, - }, - MethodRow { - op: OP_MODELS, - verb: "GET", - path: PATH_MODELS, - has_request_body: false, - }, - MethodRow { - op: OP_EVALUATE, - verb: "POST", - path: PATH_EVALUATE, - has_request_body: true, - }, -]; - -/// Which row a `(verb, path)` pair names, where one does. -/// -/// The path match is exact — jev's two surfaces are both fixed paths, not a pattern family the way -/// A2A's task collection is — so there is no most-specific-wins question to settle here. -#[must_use] -pub fn row_for(verb: &str, path: &str) -> Option<&'static MethodRow> { - METHODS - .iter() - .find(|r| r.path == path && r.verb.eq_ignore_ascii_case(verb)) -} - -#[cfg(test)] -#[path = "tests/ops.rs"] -mod tests; diff --git a/crates/busbar-plane-decisions/src/plane.rs b/crates/busbar-plane-decisions/src/plane.rs deleted file mode 100644 index 152888403e..0000000000 --- a/crates/busbar-plane-decisions/src/plane.rs +++ /dev/null @@ -1,469 +0,0 @@ -//! The plane itself: the seven codec methods and the fact methods, each a few lines over `codec.rs` -//! and `ops.rs`. -//! -//! Every method here returns FACTS AND LOCATORS. Not an amount, not a decision, not a credential, -//! not a price. Nothing in this file opens a connection, reads a file, reads a clock other than the -//! one the context hands it, or keeps a byte across a call. - -use busbar_contract::bounded::{FactValue, Facts, Ir, ScratchBytes}; -use busbar_contract::dest::{DestinationFacts, EgressBody, Leg, RoutePlan, VerifiedDestination}; -use busbar_contract::grammar::{ArrivalLocation, Location}; -use busbar_contract::ids::SchemeAlt; -use busbar_contract::kinds::{ContentFacts, CredentialLocator, PlaneFacts}; -use busbar_contract::plane::{ - Ingress, Plane, PlaneSessionState, Progress, Response, SessionPlane, UnitDraft, -}; -use busbar_contract::unit::{ - AuditFacts, Ctx, FinishClass, Refusal, RefusalReason, Unit, UnitEnd, UsageLocator, - UsageLocators, -}; -use busbar_contract::wire::{Decode, Encode, Frame, FrameCursor, TransportEnvelope}; - -use crate::codec::{ - self, error_body, CONTENT_TYPE_JSON, EGRESS_SCHEME, FIELD_CONTENT_TYPE, PTR_ERROR, PTR_ID, - PTR_REQUEST_ID, PTR_USAGE_UNITS, REQUEST_PTRS, RESPONSE_PTRS, -}; -use crate::facts as f; -use crate::meta::CLASS_DECISION; -use crate::ops; -use crate::DecisionPlane; - -/// The transport fact key a request target is published under. The kernel's own reserved key. -const FACT_PATH: &str = busbar_contract::transport::facts::PATH; - -/// The transport fact key the request's own verb is published under. The kernel's own reserved -/// key. jev needs it because its two operations are told apart by verb+path, never by a body -/// member the way MCP/A2A's JSON-RPC method name is. -const FACT_METHOD: &str = busbar_contract::transport::facts::METHOD; - -impl DecisionPlane { - /// The ONE provider a unit on this plane is dialled against, judged against and named by. - /// - /// A jev request names no provider — its body is the caller's `state`, its operation is the - /// verb and path — so the plane can only answer "which provider" when exactly one is - /// configured. With none, or with several, there is no provider the request can be said to - /// target, and every per-unit answer (`verify`, `approve`, the provider fact) is the same - /// honest one [`DecisionPlane::EMPTY`] gives: no destination the trust unit admits, no provider - /// named for scope. Picking the first-declared of several would dial, and ask scope for, a - /// provider the caller never chose. - fn provider(&self) -> Option<&'static crate::DecisionProvider> { - match self.providers() { - [only] => Some(only), - _ => None, - } - } - - /// The draft facts every unit starts with: its operation, and the provider it is dialled - /// against where one resolves. - fn draft_facts(&self, row: &ops::MethodRow) -> Facts<'static> { - let mut facts = Facts::new(); - let _ = facts.set(f::FACT_OP, FactValue::Str(row.op.as_str())); - if let Some(p) = self.provider() { - let _ = facts.set(f::FACT_PROVIDER, FactValue::Str(p.id)); - } - facts - } - - /// A leg reaching the configured provider, or an unreachable one when none is configured. - /// - /// A plane with nothing configured answers honestly rather than panicking or inventing a host: - /// the empty host is refused by the trust unit against the allow-list. - fn upstream_leg(&self) -> Leg { - Leg { - destination: self.upstream_destination(), - } - } - - /// Where a hop to the configured provider goes. - fn upstream_destination(&self) -> DestinationFacts { - match self.provider() { - Some(p) => DestinationFacts::Upstream { - transport: p.transport, - address: busbar_contract::UpstreamAddress::socket(p.host), - lane: p.lane, - }, - None => DestinationFacts::Upstream { - transport: crate::claims::TRANSPORT, - address: busbar_contract::UpstreamAddress::socket(""), - lane: busbar_contract::ids::LaneId::new(""), - }, - } - } -} - -/// Which code and words this dialect answers one refusal reason with. -/// -/// jev has no error vocabulary of its own to bind against (unlike A2A's JSON-RPC codes) — it is a -/// bare HTTP+JSON passthrough, so every refusal renders as a plain JSON error object with a neutral -/// message. THE MATCH IS TOTAL — no `_` arm — so a reason with no home here is a compile error, -/// never a silent collapse to an internal fault. -fn refusal_render(reason: RefusalReason) -> (&'static str, &'static str) { - match reason { - RefusalReason::BodyTooLarge => ("invalid_request", "the request is too large"), - RefusalReason::DecodeFailed => ("invalid_request", "the request could not be read"), - RefusalReason::SchemeNotDeclared - | RefusalReason::CredentialRejected - | RefusalReason::SessionUnbound - | RefusalReason::CredentialBudget => ( - "invalid_request", - "the request did not carry usable authority", - ), - RefusalReason::ScopeMissing - | RefusalReason::Vetoed - | RefusalReason::Revoked - | RefusalReason::PoolNotPermitted - | RefusalReason::Untrusted => ( - "unsupported_operation", - "the caller may not perform this operation", - ), - RefusalReason::NoDestination => ( - "invalid_params", - "no decision provider is reachable for this request", - ), - RefusalReason::InFlightCap - | RefusalReason::CursorBudget - | RefusalReason::SessionBudget - | RefusalReason::OpenSlotBusy - | RefusalReason::OverBudget - | RefusalReason::GroupFrozen - | RefusalReason::Unpriced - | RefusalReason::OverdraftCeiling - | RefusalReason::StaleSlice - | RefusalReason::TierMismatch - | RefusalReason::SpillBudget - | RefusalReason::ScratchExhausted - | RefusalReason::RateLimited - | RefusalReason::ChallengeExhausted - | RefusalReason::NoRate - | RefusalReason::Replayed - | RefusalReason::InFlight - | RefusalReason::DestinationBudgetExhausted - | RefusalReason::BreakerOpen - | RefusalReason::DestinationUnreachable - | RefusalReason::Drain - | RefusalReason::Superseded - | RefusalReason::ClientGone - | RefusalReason::DeadlineExceeded - | RefusalReason::Stalled => ( - "unsupported_operation", - "the request could not be served at this time", - ), - RefusalReason::DurabilityUnavailable - | RefusalReason::MeterDisputed - | RefusalReason::HandoffMismatch - | RefusalReason::PlanePanic - | RefusalReason::TaskLost - | RefusalReason::SecretPlaceholder => { - ("internal", "the request could not be served at this time") - } - } -} - -/// Which operation a `(verb, path)` pair on the transport names, decoded into the row. -fn row_of(ctx: &Ctx<'_>) -> Option<&'static ops::MethodRow> { - let verb = ctx.transport().fact(FACT_METHOD)?; - let path = ctx.transport().fact(FACT_PATH)?; - ops::row_for(verb, path) -} - -impl Plane for DecisionPlane { - fn decode_ingress<'u>( - &self, - frames: &mut FrameCursor<'u>, - _st: Option<&mut PlaneSessionState>, - ctx: &Ctx<'u>, - ) -> Result, Decode> { - let Some(row) = row_of(ctx) else { - return Err(Decode::UnsupportedOperation); - }; - if !row.has_request_body { - // `GET /v1/models` carries no body. It is complete the moment the surface is - // recognised, exactly as A2A's discovery documents are. - return Ok(Ingress::OneShot(Box::new(UnitDraft { - op: row.op, - body_ir: Ir::empty(), - correlates: None, - correlation_out: None, - facts: self.draft_facts(row), - }))); - } - let Some(frame) = frames.next_frame() else { - return Ok(Ingress::NeedMore); - }; - let body = frame.bytes.as_slice(); - if body.is_empty() { - return Ok(Ingress::NeedMore); - } - let facts = self.draft_facts(row); - Ok(Ingress::OneShot(Box::new(UnitDraft { - op: row.op, - // The request body is read at the declared pointers alone (`codec::REQUEST_PTRS`, the - // top-level `model`). The view still carries the WHOLE body, because byte-identity - // forwarding reads `Ir::body()`, never a pointer. - body_ir: codec::view(body, REQUEST_PTRS, ctx)?, - correlates: None, - correlation_out: None, - facts, - }))) - } - - fn encode_egress<'u>( - &self, - u: &Unit<'u>, - dest: &VerifiedDestination, - _st: Option<&mut PlaneSessionState>, - ctx: &Ctx<'u>, - ) -> Result, Encode> { - if !matches!( - dest.facts(), - DestinationFacts::Upstream { .. } | DestinationFacts::SessionUpstream { .. } - ) { - return Err(Encode::Unrepresentable); - } - // Byte-identity passthrough: the caller's body goes to the provider UNCHANGED. jev performs - // none of A2A's task-identifier rewrite — there is no busbar-minted identifier in a - // `systemone` request to swap out. - let body = ScratchBytes::new(u.body().body()); - let mut envelope = TransportEnvelope::default(); - let content_type = ctx - .arena() - .alloc_bytes(CONTENT_TYPE_JSON) - .map_err(|_| Encode::ScratchExhausted)?; - let _ = envelope.fields.push(busbar_contract::wire::EnvelopeField { - name: FIELD_CONTENT_TYPE, - value: content_type, - }); - Ok(EgressBody { - envelope, - body, - auth: busbar_contract::ids::SchemeKey::new(EGRESS_SCHEME), - }) - } - - fn encode_ingress_frame<'u>( - &self, - _u: &Unit<'u>, - _f: &Frame, - _dest: &VerifiedDestination, - _st: Option<&mut PlaneSessionState>, - _ctx: &Ctx<'u>, - ) -> Result>, Encode> { - // Both of jev's operations are complete in one request/response pair; there is no open - // unit an extra inbound frame could belong to. - Ok(None) - } - - fn decode_response<'u>( - &self, - frames: &mut FrameCursor<'u>, - _dest: &VerifiedDestination, - _st: Option<&mut PlaneSessionState>, - ctx: &Ctx<'u>, - ) -> Result, Decode> { - let Some(frame) = frames.next_frame() else { - return Ok(Progress::NeedMore); - }; - let body = frame.bytes.as_slice(); - if body.is_empty() { - return Ok(Progress::NeedMore); - } - let ir = codec::view(body, RESPONSE_PTRS, ctx)?; - let is_error = codec::has(body, PTR_ERROR); - let mut facts = Facts::new(); - if let Some(id) = - codec::read_str(body, PTR_REQUEST_ID).or_else(|| codec::read_str(body, PTR_ID)) - { - let id = ctx.arena().alloc_str(id).map_err(|_| Decode::Oversize)?; - let _ = facts.set(f::FACT_REQUEST_ID, FactValue::Str(id)); - } - let _ = facts.set(f::FACT_HAS_ERROR, FactValue::Bool(is_error)); - // The usage figure is read ONLY on a response that did not error — a 4xx has nothing - // extractable under the billable-decision class (see `meter` below and the signed design's - // C-3 "billable-success" rule). Only a whole count the fact can hold EXACTLY is set here; a - // member that is not one (fractional, negative, not a number, past `i64`) is left to - // `meter`, which locates it rather than letting a cast wrap it or a parse drop it. - if !is_error { - if let Some(units) = - codec::read_u64(body, PTR_USAGE_UNITS).and_then(|u| i64::try_from(u).ok()) - { - let _ = facts.set(f::FACT_USAGE_UNITS, FactValue::Int(units)); - } - } - let r = Response { - ir, - finish: if is_error { - FinishClass::Error - } else { - FinishClass::Complete - }, - facts, - }; - Ok(Progress::Terminal { - for_: None, - r: Box::new(r), - }) - } - - fn encode_response<'u>( - &self, - r: &Response<'u>, - _st: Option<&mut PlaneSessionState>, - ctx: &Ctx<'u>, - ) -> Result, Encode> { - // Byte-identity passthrough on the way back too: the provider's own bytes reach the caller - // unchanged. jev composes no answer of its own — both operations are always a relay. - ctx.arena() - .alloc_bytes(r.ir.body()) - .map_err(|_| Encode::ScratchExhausted) - } - - fn encode_refusal<'u>( - &self, - refusal: &Refusal, - _draft: Option<&UnitDraft<'u>>, - _st: Option<&PlaneSessionState>, - ctx: &Ctx<'u>, - ) -> Result, Encode> { - let (code, message) = refusal_render(refusal.reason); - ctx.arena() - .alloc_bytes(&error_body(code, message)) - .map_err(|_| Encode::ScratchExhausted) - } - - fn encode_end<'u>( - &self, - _u: &Unit<'u>, - _end: &UnitEnd, - _st: Option<&mut PlaneSessionState>, - _ctx: &Ctx<'u>, - ) -> Result>, Encode> { - // Neither jev operation writes anything to end a unit: the answer's own document IS the - // end. - Ok(None) - } - - fn authenticate<'u>(&self, _u: &Unit<'u>, ctx: &Ctx<'u>) -> CredentialLocator { - // The core gateway credential, always: jev has no anonymous surface (unlike A2A's discovery - // documents), because even `/v1/models` is billed per the provider's own account scoping. - CredentialLocator { - narrowing: Some(SchemeAlt::new(crate::claims::ALT_TOKEN)), - from_session: ctx - .session() - .is_some_and(busbar_contract::unit::SessionView::is_bound), - } - } - - fn verify<'u>(&self, _u: &Unit<'u>, _ctx: &Ctx<'u>) -> DestinationFacts { - // Both operations are provider/account-scoped hops — jev owns no record of its own a unit - // could resolve against instead (see records.rs's module note). - self.upstream_destination() - } - - fn route<'u>(&self, u: &Unit<'u>, _ctx: &Ctx<'u>) -> RoutePlan { - let mut plan = RoutePlan::default(); - // Both operations are a bare hop to the configured provider — jev owns no record leg (see - // records.rs's module note): `systemone` mints no busbar-side identifier a later call would - // need to look back up, and `models` is never cached. - if u.op() == ops::OP_SYSTEMONE || u.op() == ops::OP_MODELS { - let _ = plan.legs.push(self.upstream_leg()); - } - plan - } - - fn meter<'u>(&self, _u: &Unit<'u>, r: &Response<'u>, _ctx: &Ctx<'u>) -> UsageLocators { - let mut locators = UsageLocators::default(); - // Billable-success only (signed design C-3): a response that carried an `/error` member has - // NOTHING extractable under the billable-decision class — this is not "meter zero", it is - // "meter nothing at all", so a 4xx never posts a usage line. - if let Some(FactValue::Bool(true)) = r.facts.get(f::FACT_HAS_ERROR) { - return locators; - } - let whole = match r.facts.get(f::FACT_USAGE_UNITS) { - Some(FactValue::Int(units)) => u64::try_from(units).ok(), - _ => None, - }; - let line = match whole { - // The quantity was already read out of the response by `decode_response`, so the - // locator carries the value and no location — the same shape A2A's byte-class locator - // uses when the plane already has the number in hand. - Some(units) => Some((None, Some(units))), - // The provider REPORTED a usage member this plane cannot carry as a whole count — `7.5`, - // `-3`, `"7"`, `null`, a figure past `i64`. That is a measurement, not an absence (#81), - // and judging it is not this plane's job (#42): the locator names WHERE it is and - // carries no value, so the step that folds locators reads the exact decimal text or - // refuses it. Dropping it would record "reported nothing" for an exchange that reported - // a figure. - None if codec::has(r.ir.body(), PTR_USAGE_UNITS) => Some(( - Some(Location::Arrival(ArrivalLocation::FirstFrameJsonPointer( - PTR_USAGE_UNITS, - ))), - None, - )), - // No usage member at all: the provider reported nothing, and no line is the record of - // that — the settlement's "destination reported no usage" row decides what it bills. - None => None, - }; - if let Some((location, quantity)) = line { - let _ = locators.lines.push(UsageLocator { - class: CLASS_DECISION, - location, - quantity, - // jev's response never names a lane of its own; the lane is the provider's, - // sealed by the trust unit. - lane: None, - }); - } - locators - } - - fn audit<'u>(&self, u: &Unit<'u>, out: &UnitEnd, _ctx: &Ctx<'u>) -> AuditFacts { - AuditFacts { - op_class: u.op(), - finish: busbar_contract::unit::finish_class_of(out, FinishClass::Complete), - } - } - - fn plane_facts<'u>( - &self, - _verb: busbar_contract::ids::AdminVerbId, - _subject: Option<&'u str>, - _ctx: &Ctx<'u>, - ) -> Result, Decode> { - // jev declares no introspection verb (see meta.rs), so any verb asked here is one this - // plane never claimed. - Err(Decode::UnsupportedOperation) - } - - fn content_facts<'u>( - &self, - _u: &Unit<'u>, - r: &Response<'u>, - _ctx: &Ctx<'u>, - ) -> ContentFacts<'u> { - // Only the declared metadata keys, copied off what `decode_response` already read — never a - // re-read of the body, and never `state`/`answers`. - let mut facts = Facts::new(); - for key in [f::FACT_OP, f::FACT_HAS_ERROR, f::FACT_USAGE_UNITS] { - if let Some(v) = r.facts.get(key) { - let _ = facts.set(key, v); - } - } - ContentFacts { facts } - } -} - -impl SessionPlane for DecisionPlane { - fn open_session<'u>(&self, _ctx: &Ctx<'u>) -> PlaneSessionState { - // jev keeps no per-connection codec state: every exchange is a complete request/response - // pair with nothing to carry to the next one. - PlaneSessionState::new(()) - } - - fn open_upstream<'u>(&self, _dest: &VerifiedDestination, _ctx: &Ctx<'u>) -> PlaneSessionState { - PlaneSessionState::new(()) - } -} - -#[cfg(test)] -#[path = "tests/plane.rs"] -mod tests; diff --git a/crates/busbar-plane-decisions/src/plane_door.rs b/crates/busbar-plane-decisions/src/plane_door.rs deleted file mode 100644 index 4c4ab49c36..0000000000 --- a/crates/busbar-plane-decisions/src/plane_door.rs +++ /dev/null @@ -1,996 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE DECISIONS PLANE'S DOOR: the plane kind's memory-ABI table (`busbar_contract::abi::plane`) -//! built with the SDK's `plugin_door!` on its safe surface, over this plane's answers to the plane -//! driver ([`crate::driven`], `BUSBAR-1.6.0.md` Part 3, section 12). [`door`] is the LINKED door; -//! the same function is the DROPPED door once a `cdylib` exports it (`examples/decisions_door.rs`, -//! through `busbar_contract::export_door!`), so the two cannot answer differently. -//! `tests/conformance.rs` loads both through the one loader and requires one transcript. -//! -//! The composition root links [`door`] on its `plane-door` axis under the plane's one switch -//! `plane-decisions` (`BUSBAR-1.6.0.md` Part 3, section 12, "The switch": the fold is complete and -//! its development-only switch is gone) and binds it through the loader's one load beside the -//! dropped-in plane doors. The plane's registry row is this door's Statement, folded by the kernel; -//! the serve path composes the bound door and hands it its arrivals through the plane driver. -//! -//! * The Statement: the plane's key and version, `decisions:` declared and `providers:` consumed, -//! its one outbound need, and the tail [`TAIL`] (every list read off [`crate::driven::tail`]). -//! * `validate`, `open`, `refresh`: the settings blob read as the `decisions:` section -//! ([`crate::config::DecisionsSection`]), each model's dialect resolved ([`routes`]: its own -//! `protocol`, else its provider's as the host stated it at `open`); a generation's snapshot -//! claims [`crate::driven::served_claims`] for its models' dialects, lists every model the -//! section configures (the kernel filters them by scope and `/v1/models` appends them), and -//! binds no audience (the operation is served on the plain data plane to a keyed caller). -//! `retire` drops a generation. -//! * `arrive`: [`crate::driven::arrive`], routed over the model the request names -//! ([`crate::driven::route_model`]); an unclaimed request is refused at 404, and one that routes -//! over no model at its [`crate::driven::Unrouted`] status. -//! * `on_piece`: the ATTEMPT's request head ([`crate::driven::attempt_request`]), the caller's body to the -//! far end unchanged ([`crate::driven::caller_piece`]) but for the routed model's `upstream_model` -//! ([`crate::driven::spliced`]), and the far end's answer relayed unchanged with its count read at -//! the last piece ([`crate::driven::FarEndReading`]). -//! * `refusal`: [`crate::driven::refusal_body`]; the refused unit's state is dropped. -//! * `cancel`: the unit on the cancelled ticket is dropped, `CANCEL_ABORTED` before its far end -//! answered and `CANCEL_OK_PARTIAL` after. -//! * `serve` and `project` are REFUSED: the plane publishes no admin route and binds no hook view. -//! `hydrate`, `start`, `tick`, `drive`, `release` and `close` hold nothing. -//! -//! A unit's state lives from its arrival to its end (its last far-end piece, its refusal or its -//! cancel), and at most [`MAX_UNITS`] live at once: an arrival past that is refused, and a live -//! unit is never dropped to make room (THE DESIGN section 11.2: memory is plugin-owned, and no -//! state grows without bound on the hot path). - -use std::collections::BTreeMap; -use std::mem::size_of; -use std::ptr; - -use busbar_contract::abi::host::conn::connector::{ - Need, DIRECTION_OUTBOUND, EGRESS_PROVIDER, KEEP_NAMED, -}; -use busbar_contract::abi::mechanism::call::{AbiStr, Blob, InHead, OutHead, Outcome, BLOB_ABSENT}; -use busbar_contract::abi::mechanism::door::{ - KindTailHead, Section, Statement, SECTION_CONSUMED, SECTION_DECLARING, -}; -use busbar_contract::abi::mechanism::lifecycle::{ - GenIn, RefreshIn, ReleaseIn, TickIn, TickOut, ValidateIn, -}; -use busbar_contract::abi::mechanism::ticket::Ticket; -use busbar_contract::abi::plane::{ - ArriveIn, ArriveOut, BillableClass, DialectAuth, OnPieceIn, OnPieceOut, OpClass, OutField, - PlaneDriveIn, PlaneDriveOut, PlaneOpenIn, PlaneOpenOut, PlaneRefreshOut, PlaneSnapshot, - PlaneTail, ProjectIn, ProjectOut, RefusalIn, RefusalOut, ServeIn, ServeOut, UnitCount, - CANCEL_ABORTED, CANCEL_OK_PARTIAL, CLAIM_EXACT, EMIT_DONE, EMIT_TO_FAR_END, FROM_CALLER, - FROM_FAR_END, FROM_KERNEL, INGRESS_REQUEST_RESPONSE, PIECE_HAS_STATUS, PIECE_LAST, - PRINCIPAL_NONE, PRINCIPAL_OPTIONAL, PRINCIPAL_REQUIRED, ROUTE_DIRECT, SHAPE_WHOLE, - UNITS_REPORTED, -}; -use busbar_contract::abi::plane::{PlaneCancelIn, PlaneCancelOut}; -use busbar_contract::abi::sdk::door::{abi_str, statement}; -use busbar_contract::abi::sdk::life::Refusal; -use busbar_contract::abi::sdk::publish::{ClaimSpec, SnapshotSpec}; -use busbar_contract::abi::sdk::{ - open_failed, Generations, HostBuf, Instance, Keyed, Lent, Out, Safe, SafeSlot, -}; -use busbar_contract::plane::PlaneMeta; - -use crate::codec::{CONTENT_TYPE_JSON, FIELD_CONTENT_TYPE, FIELD_RETRY_AFTER}; -use crate::config::DecisionsSection; -use crate::driven::{self, tail, CallerAnswer, FarEndReading, PrincipalNeed}; -use crate::{claims, DecisionPlane}; - -/// The version the Statement names: the crate's (a test pins the two equal). -pub const VERSION: &str = "1.6.0"; - -/// The most calls the kernel keeps in flight on one instance. -const MAX_INFLIGHT: u32 = 64; - -/// The most units the instance keeps state for at once; past it, a new arrival is refused -/// ([`BUSY`]) and no live unit is dropped. -pub const MAX_UNITS: usize = 4096; - -/// [`ArriveOut::refusal`]: the request names no operation this plane claims. -pub const UNCLAIMED: u32 = 1; - -/// The status an unclaimed request is refused at. -const STATUS_NOT_FOUND: u32 = 404; - -/// [`ArriveOut::refusal`]: the request routes over no configured model ([`driven::Unrouted`]). -pub const UNROUTED: u32 = 2; - -/// [`ArriveOut::refusal`]: the instance holds [`MAX_UNITS`] live units; the arrival is refused at -/// [`STATUS_BUSY`], about the entry it routed over, before anything is charged. -pub const BUSY: u32 = 3; - -/// The status a [`BUSY`] arrival is refused at. -const STATUS_BUSY: u32 = 503; - -/// The human label. -const LABEL: &str = "Decisions"; - -/// An absent string. -const NONE: AbiStr = AbiStr { - ptr: ptr::null(), - len: 0, -}; - -/// `decisions:` declared, `providers:` consumed ([`tail::SECTION_DECLARING`], -/// [`tail::SECTIONS_CONSUMED`]). -const SECTIONS: &[Section] = &[ - Section { - name: abi_str(tail::SECTION_DECLARING), - flags: SECTION_DECLARING, - _reserved: 0, - }, - Section { - name: abi_str(tail::SECTIONS_CONSUMED[0]), - flags: SECTION_CONSUMED, - _reserved: 0, - }, -]; - -const DIALECTS: &[AbiStr] = &[abi_str(tail::DIALECTS[0]), abi_str(tail::DIALECTS[1])]; - -/// Each dialect's default outbound style ([`tail::DIALECT_AUTH`]). -const DIALECT_AUTH: &[DialectAuth] = &[dialect_auth(0), dialect_auth(1)]; - -/// [`tail::DIALECT_AUTH`]'s row `i`, on the ABI. -const fn dialect_auth(i: usize) -> DialectAuth { - DialectAuth { - dialect: tail::DIALECT_AUTH[i].0, - _reserved: 0, - style: abi_str(tail::DIALECT_AUTH[i].1), - params: Blob::ABSENT, - } -} - -const SCOPE_KINDS: &[AbiStr] = &[abi_str(tail::SCOPE_KINDS[0])]; - -const OP_CLASSES: &[OpClass] = &[op_class(0), op_class(1)]; - -/// [`tail::OP_CLASSES`]' class `i`, on the ABI. -const fn op_class(i: usize) -> OpClass { - OpClass { - op: abi_str(tail::OP_CLASSES[i].as_str()), - name: abi_str(tail::OP_CLASSES[i].as_str()), - } -} - -const BILLABLE_CLASSES: &[BillableClass] = &[billable(0), billable(1), billable(2), billable(3)]; - -/// [`tail::BILLABLE_CLASSES`]' class `i`, on the ABI. -const fn billable(i: usize) -> BillableClass { - BillableClass { - class: abi_str(tail::BILLABLE_CLASSES[i].0.as_str()), - family: abi_str(tail::BILLABLE_CLASSES[i].1), - } -} - -/// The fee unit the plane counts ([`tail::FEE_UNITS`]). -const FEE_UNITS: &[AbiStr] = &[abi_str(tail::FEE_UNITS[0])]; - -/// The far end's one response field the plane relays: the document type of the answer. -const KEEP_RESPONSE_HEADERS: &[AbiStr] = &[abi_str(FIELD_CONTENT_TYPE)]; - -/// THE PLANE'S ONE NEED ([`tail::NEEDS`]): outbound to a configured provider, over the claim's -/// transport, decorated under the egress scheme the kernel holds the credential for. -pub const NEEDS: &[Need] = &[Need { - direction: DIRECTION_OUTBOUND, - egress_class: EGRESS_PROVIDER, - transport: abi_str(tail::NEEDS[0].0), - auth: abi_str(tail::NEEDS[0].1), - target_from: NONE, - trust_from: NONE, - details: Blob { - ptr: ptr::null(), - len: 0, - fmt: BLOB_ABSENT, - flags: 0, - }, - keep_response_headers: KEEP_RESPONSE_HEADERS.as_ptr(), - keep_response_headers_len: KEEP_RESPONSE_HEADERS.len(), - timeout_ms: 0, - keep_mode: KEEP_NAMED, - _reserved: 0, - deny_response_headers: core::ptr::null(), - deny_response_headers_len: 0, -}]; - -/// THE STATEMENT TAIL: the plane's static facts, every list [`crate::driven::tail`]'s. -pub const TAIL: &PlaneTail = &PlaneTail { - head: KindTailHead { - size: size_of::() as u32, - _reserved: 0, - }, - flags: 0, - ingress: INGRESS_REQUEST_RESPONSE, - // The kernel gathers the caller's whole body and pushes it as one piece. - dispatch_shape: SHAPE_WHOLE, - _reserved: 0, - scope: abi_str(tail::SCOPE_KINDS[0]), - label: abi_str(LABEL), - subject_noun: abi_str(tail::SUBJECT_NOUN), - admin_noun: abi_str(tail::ADMIN_NOUN), - audit_kind: abi_str(tail::AUDIT_KIND), - signing_domain: NONE, - signing_kid_prefix: NONE, - cli_help: NONE, - dialects: DIALECTS.as_ptr(), - dialects_len: DIALECTS.len(), - dialect_auth: DIALECT_AUTH.as_ptr(), - dialect_auth_len: DIALECT_AUTH.len(), - scope_kinds: SCOPE_KINDS.as_ptr(), - scope_kinds_len: SCOPE_KINDS.len(), - op_classes: OP_CLASSES.as_ptr(), - op_classes_len: OP_CLASSES.len(), - billable_classes: BILLABLE_CLASSES.as_ptr(), - billable_classes_len: BILLABLE_CLASSES.len(), - route_cost: ptr::null(), - route_cost_len: 0, - fee_units: FEE_UNITS.as_ptr(), - fee_units_len: FEE_UNITS.len(), - record_kinds: ptr::null(), - record_kinds_len: 0, - egress_targets: ptr::null(), - egress_targets_len: 0, - record_chains: ptr::null(), - record_chains_len: 0, - trust_keys: ptr::null(), - trust_keys_len: 0, - refusal_statuses: ptr::null(), - refusal_statuses_len: 0, - caller_credential_refusal: NONE, - admin_routes: ptr::null(), - admin_routes_len: 0, - admin_openapi: Blob::ABSENT, - stream_ceiling_secs: 0, -}; - -/// THE STATEMENT: the plane's key and version, its sections, its need and its tail. -pub const STATEMENT: Statement = Statement { - kind_tail: ptr::from_ref(TAIL).cast::(), - sections: SECTIONS.as_ptr(), - sections_len: SECTIONS.len(), - needs: NEEDS.as_ptr(), - needs_len: NEEDS.len(), - ..statement(::KEY, VERSION, MAX_INFLIGHT) -}; - -/// The settings blob read as the `decisions:` section; an empty blob is the empty section. -/// -/// The blob is ONE JSON object `{decisions:

}` (`BUSBAR-1.6.0.md` section 4: "a plane's -/// settings reach it as ONE validated JSON object `{section: value}`"), the shape the boot's deal -/// hands every door; the bare section is read too, since a section never carries a member named -/// after its own verb (its grammar refuses unknown members), so the two cannot be confused. -/// -/// # Errors -/// -/// The section's first broken rule, in the grammar's words. -pub fn read_settings(settings: &[u8]) -> Result { - if settings.is_empty() { - return Ok(DecisionsSection::default()); - } - let mut value: serde_json::Value = - serde_json::from_slice(settings).map_err(|e| e.to_string())?; - let section = match value.as_object_mut() { - Some(blob) if blob.len() == 1 && blob.contains_key(tail::SECTION_DECLARING) => blob - .remove(tail::SECTION_DECLARING) - .unwrap_or(serde_json::Value::Null), - _ => value, - }; - if section.is_null() { - return Ok(DecisionsSection::default()); - } - serde_json::from_value(section).map_err(|e| e.to_string()) -} - -/// `validate`'s blob: what stage 3g deals a plane (`{:
}`; the kernel's own judges hand -/// the same shape), read at the `decisions:` section. An empty blob, or one that writes no -/// `decisions:`, is the empty section. -/// -/// # Errors -/// -/// A blob that is not a map of sections, or the section's first broken rule. -pub fn read_dealt(settings: &[u8]) -> Result { - if settings.is_empty() { - return Ok(DecisionsSection::default()); - } - let value: serde_json::Value = serde_json::from_slice(settings).map_err(|e| e.to_string())?; - let serde_json::Value::Object(mut verbs) = value else { - return Err("the dealt settings are not a map of the plane's sections".to_string()); - }; - match verbs.remove(tail::SECTION_DECLARING) { - None | Some(serde_json::Value::Null) => Ok(DecisionsSection::default()), - Some(section) => serde_json::from_value(section).map_err(|e| e.to_string()), - } -} - -/// ONE GENERATION'S SNAPSHOT for a section whose models are of `dialects`: the claims -/// [`driven::served_claims`] serves, each an exact target over the claim's transport, wearing its -/// own dialect for a refusal before `arrive`. It binds no audience: the operation is served on the -/// plain data plane to a keyed caller, as the llm plane's is. -#[must_use] -pub fn snapshot_spec(dialects: &[u32]) -> SnapshotSpec { - SnapshotSpec { - claims: driven::served_claims(dialects) - .into_iter() - .map(|(verb, target, dialect)| ClaimSpec { - refusal_dialect: u16::try_from(dialect).unwrap_or(0), - ..ClaimSpec::new(verb, target, claims::TRANSPORT, CLAIM_EXACT) - }) - .collect(), - ..SnapshotSpec::default() - } -} - -/// What a claimed arrival routes over: a model's dialect, and its `upstream_model`. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Routed { - /// Its index in [`tail::DIALECTS`]. - pub dialect: u32, - /// Its `upstream_model`. - pub upstream: Option, -} - -/// EVERY MODEL'S ROUTE under `section`: its dialect, its own `protocol` override or else its -/// provider's protocol (`protocols`, the provider facts the host handed at `open`; THE DESIGN -/// section 2: "a model's dialect is its override or its provider's protocol"), `jev` for a provider -/// the host stated no protocol for. -/// -/// # Errors -/// -/// A model whose dialect is none this plane speaks, named with the dialects it does (#51: an -/// unknown dialect fails closed). -pub fn routes( - section: &DecisionsSection, - protocols: &BTreeMap, -) -> Result, String> { - section - .models - .iter() - .map(|(name, model)| { - let protocol = model - .protocol - .as_deref() - .or_else(|| protocols.get(&model.provider).map(String::as_str)) - .unwrap_or(tail::DIALECTS[0]); - let dialect = driven::dialect_of(protocol).ok_or_else(|| { - format!( - "decisions.models.{name} resolves to protocol '{protocol}', but the decisions \ - plane speaks only: {}.", - tail::DIALECTS.join(", ") - ) - })?; - Ok(( - name.clone(), - Routed { - dialect, - upstream: model.upstream_model.clone(), - }, - )) - }) - .collect() -} - -/// ONE GENERATION'S SNAPSHOT for `section`'s `routes`: [`snapshot_spec`]'s claims for its models' -/// dialects, and every model it configures LISTED (THE DESIGN section 2: all configured decisions -/// models are listed, scope-filtered by the kernel; `/v1/models` appends them), in ascending name -/// order. -#[must_use] -pub fn generation_spec(routes: &BTreeMap) -> SnapshotSpec { - let dialects: Vec = routes.values().map(|r| r.dialect).collect(); - SnapshotSpec { - listed: routes.keys().cloned().collect(), - ..snapshot_spec(&dialects) - } -} - -/// One unit the instance keeps, from its ATTEMPT to the far end's last piece. -#[derive(Debug, Default)] -struct Unit { - /// An ATTEMPT opened the request bound for the far end; the caller's body follows it there. - attempt: bool, - /// The far end's answer, read as it is relayed. - reading: FarEndReading, - /// The piece answered short: its re-call carries the same piece, which is not read twice. - recall: bool, - /// What a `more = 1` answer still owes the host, and how much of it is written. - owed: Vec, - /// How much of [`Unit::owed`] is written. - at: usize, - /// The `EMIT_*` bits the owed bytes carry. - owed_flags: u32, - /// The caller's fields the far end receives ([`driven::relayed_fields`]), kept from `arrive` - /// (the caller's head crosses once) for every ATTEMPT of the unit. - caller: driven::Relayed, - /// The ticket the unit's last piece crossed on: what a `cancel` names it by. - ticket: Option, - /// Its place in the instance's arrivals ([`DecisionsDoor::arrivals`]). - seq: u64, - /// The far end's status, read off its first piece; the request fee is judged on it. - status: Option, - /// The routed model's `upstream_model`, kept from `arrive`: the caller's top-level `model` is - /// spliced to it on the way to the far end where the two differ ([`driven::spliced`]). - upstream: Option, -} - -/// One instance: every live generation's snapshot, and the units in flight. -#[derive(Debug)] -pub struct DecisionsDoor { - generations: Generations, - units: Keyed, - /// The models a claimed arrival routes over, directly, by name: every model the latest - /// generation configures, each with its dialect and `upstream_model`. - models: Keyed, - /// How many arrivals the instance has kept: each unit's [`Unit::seq`]. The count is the - /// SDK's per-instance keyed state, its one entry under `()`: the plane holds no interior - /// state of its own. - arrivals: Keyed<(), u64>, - /// Each provider's protocol, as the host stated it at `open` (`PlaneOpenIn::providers`). - protocols: BTreeMap, -} - -impl DecisionsDoor { - /// Route claimed arrivals over `routes`. - fn route_over(&self, routes: BTreeMap) { - self.models.with_all(|models| *models = routes); - } -} - -/// One slot body on the SDK's safe surface, over this plane's [`DecisionsDoor`]. -macro_rules! slot { - ($(#[$doc:meta])* $name:ident, $in:ty, $out:ty, - |$inst:pat_param, $input:pat_param, $o:pat_param| $body:block) => { - $(#[$doc])* - #[derive(Debug)] - pub struct $name; - impl SafeSlot for $name { - type In = $in; - type Out = $out; - type State = DecisionsDoor; - fn call($inst: Instance<'_, DecisionsDoor>, $input: Lent<'_, $in>, $o: Out<'_, $out>) - -> Outcome $body - } - }; -} - -slot!( - /// `validate`: the settings read as the section, or refused in the grammar's words. - Validate, ValidateIn, OutHead, |_, input, mut out| { - // A model's own `protocol` must be a dialect this plane speaks; its provider's is judged - // by the kernel against the dialects the Statement states. - match read_dealt(input.field(|i| &i.settings).bytes()) - .and_then(|section| routes(§ion, &BTreeMap::new())) - { - Ok(_) => Outcome::Ready, - Err(words) => out.fail(Refusal::refused(words)), - } - } -); - -slot!( - /// `open`: the instance, and the first generation's snapshot. - Open, PlaneOpenIn, PlaneOpenOut, |instance, input, mut out| { - let open = input.field(|i| &i.open); - let section = match read_settings(open.field(|o| &o.settings).bytes()) { - Ok(section) => section, - Err(words) => return open_failed(open, &mut out, |o| &o.open.err_len, &words), - }; - let protocols: BTreeMap = input - .providers() - .iter() - .filter_map(|p| { - Some(( - p.field(|p| &p.name).as_str().ok()?.to_string(), - p.field(|p| &p.protocol).as_str().ok()?.to_string(), - )) - }) - .collect(); - let routed = match routes(§ion, &protocols) { - Ok(routed) => routed, - Err(words) => return open_failed(open, &mut out, |o| &o.open.err_len, &words), - }; - let plane = DecisionsDoor { - generations: Generations::new(), - units: Keyed::new(), - models: Keyed::new(), - arrivals: Keyed::new(), - protocols, - }; - let spec = generation_spec(&routed); - plane.route_over(routed); - out.publish(|o| &o.snapshot, &plane.generations, open.generation, &spec); - instance.open(plane); - Outcome::Ready - } -); - -slot!( - /// `refresh`: the new section judged, and the next generation's snapshot. - Refresh, RefreshIn, PlaneRefreshOut, |instance, input, mut out| { - let Some(plane) = instance.get() else { - return Outcome::Failed; - }; - let section = match read_settings(input.field(|i| &i.settings).bytes()) { - Ok(section) => section, - Err(words) => return out.fail(Refusal::refused(words)), - }; - let routed = match routes(§ion, &plane.protocols) { - Ok(routed) => routed, - Err(words) => return out.fail(Refusal::refused(words)), - }; - let spec = generation_spec(&routed); - plane.route_over(routed); - out.publish(|o| &o.snapshot, &plane.generations, input.generation, &spec); - Outcome::Ready - } -); - -slot!( - /// `retire`: the generation's snapshot is dropped. - Retire, GenIn, OutHead, |instance, input, _| { - if let Some(plane) = instance.get() { - plane.generations.retire(input.generation); - } - Outcome::Ready - } -); - -slot!( - /// `tick`: none wanted. - Tick, TickIn, TickOut, |_, _, mut out| { - out.set(|o| &o.next_tick_ns, 0); - Outcome::Ready - } -); - -slot!( - /// `drive`: no session has unsolicited output. - Drive, PlaneDriveIn, PlaneDriveOut, |_, _, _| { Outcome::Ready } -); - -slot!( - /// `cancel`: the unit on the cancelled ticket is dropped. Before its far end answered it was - /// aborted (nothing billed); after, it was cut partway. - Cancel, PlaneCancelIn, PlaneCancelOut, |instance, input, mut out| { - let ticket = input.get().cancel.ticket; - let answered = instance.get().is_some_and(|plane| { - plane.units.with_all(|units| { - let key = units - .iter() - .find(|(_, unit)| unit.ticket == Some(ticket)) - .map(|(key, _)| *key); - key.and_then(|key| units.remove(&key)) - .is_some_and(|unit| unit.status.is_some()) - }) - }); - out.set( - |o| &o.cancel.disposition, - if answered { CANCEL_OK_PARTIAL } else { CANCEL_ABORTED }, - ); - Outcome::Ready - } -); - -slot!( - /// `release`: this plane answers under no lease. - Release, ReleaseIn, OutHead, |_, _, _| { Outcome::Ready } -); - -slot!( - /// `close`: the SDK drops the instance and every snapshot it still holds. - Close, InHead, OutHead, |_, _, _| { Outcome::Ready } -); - -slot!( - /// `arrive`: a claimed request's op class, principal need, dialect and route (the model it - /// names, or the only one configured, directly: ARCHITECT Q-SW6/Q-FL3); an unclaimed one is - /// refused at 404, and one that routes over no model at its [`driven::Unrouted`] status. - Arrive, ArriveIn, ArriveOut, |instance, input, mut out| { - let verb = input.field(|i| &i.method).as_str().unwrap_or_default(); - let target = input.field(|i| &i.target).as_str().unwrap_or_default(); - let Some(arrived) = driven::arrive(verb, target) else { - out.set(|o| &o.refusal, UNCLAIMED); - out.set(|o| &o.refusal_status, STATUS_NOT_FOUND); - return out.fail(Refusal::bare()); - }; - out.set(|o| &o.op_class, arrived.op_class); - out.set(|o| &o.dialect, arrived.dialect); - out.set( - |o| &o.principal_need, - match arrived.principal { - PrincipalNeed::None => PRINCIPAL_NONE, - PrincipalNeed::Required => PRINCIPAL_REQUIRED, - PrincipalNeed::Optional => PRINCIPAL_OPTIONAL, - }, - ); - if let Some(plane) = instance.get() { - let body = input.field(|i| &i.body).bytes(); - let routed = plane.models.with_all(|models| { - driven::route_model(body, models, |r| r.dialect == arrived.dialect) - .map(|(name, routed)| (name.to_string(), routed.upstream.clone())) - }); - let upstream = match routed { - Ok((model, upstream)) => { - out.route(ROUTE_DIRECT, &model); - upstream - } - Err(unrouted) => { - out.set(|o| &o.refusal, UNROUTED); - out.set(|o| &o.refusal_status, u32::from(unrouted.status())); - return out.fail(Refusal::refused(unrouted.text())); - } - }; - // The caller's head crosses here once: keep what the far end receives of it. - let caller = driven::relayed_fields(input.fields().iter().map(|f| { - ( - f.field(|f| &f.name).bytes(), - f.field(|f| &f.value).bytes(), - ) - })); - let unit = input.get().unit; - let seq = plane.arrivals.with_all(|n| { - let next = n.entry(()).or_insert(0); - let seq = *next; - *next = seq.wrapping_add(1); - seq - }); - let kept = plane.units.with_all(|units| { - reclaim_stale(units, unit, seq); - let Some(kept) = unit_of(units, unit) else { - return false; - }; - kept.caller = caller; - kept.upstream = upstream; - kept.seq = seq; - true - }); - if !kept { - // Full: the arrival is refused, about the entry it routed over, and no live unit - // is dropped to make room. - out.set(|o| &o.refusal, BUSY); - out.set(|o| &o.refusal_status, STATUS_BUSY); - return out.fail(Refusal::refused( - "the decisions plane is serving as many requests as it holds; retry", - )); - } - } - Outcome::Ready - } -); - -/// `(written, needed)` of every buffer an answer fills, short as a whole when one is short. -fn settle( - out: &mut Out<'_, OnPieceOut>, - fields: &HostBuf<'_, OutField>, - units: &HostBuf<'_, UnitCount>, - arena: &HostBuf<'_, u8>, -) -> bool { - let short = !(fields.fits() && units.fits() && arena.fits()); - let (fw, fnd) = fields.settle(short); - let (uw, und) = units.settle(short); - let (aw, and) = arena.settle(short); - out.set(|o| &o.fields_written, fw as u32); - out.set(|o| &o.fields_needed, fnd as u32); - out.set(|o| &o.units_written, uw as u32); - out.set(|o| &o.units_needed, und as u32); - out.set(|o| &o.arena_written, aw as u64); - out.set(|o| &o.arena_needed, and as u64); - short -} - -/// Write what `unit` owes into the reply buffer, as much as fits; `more = 1` while any is left, -/// and its flags (with `EMIT_DONE` held back until the last byte) on every write. Answers whether -/// the unit is done. -fn pay(unit: &mut Unit, input: Lent<'_, OnPieceIn>, out: &mut Out<'_, OnPieceOut>) -> bool { - let n = input.reply_buf().stream(&unit.owed[unit.at..]); - unit.at += n; - let more = unit.at < unit.owed.len(); - let done = !more && unit.owed_flags & EMIT_DONE != 0; - out.set(|o| &o.emitted, n as u64); - out.set(|o| &o.more, u32::from(more)); - out.set( - |o| &o.flags, - if more { - unit.owed_flags & !EMIT_DONE - } else { - unit.owed_flags - }, - ); - if !more { - unit.owed.clear(); - unit.at = 0; - } - done -} - -/// Owe `bytes` under `flags`: write what fits straight from `bytes`, and keep only what does not -/// fit for the re-calls `more = 1` asks for. Answers whether the unit is done. -fn owe( - unit: &mut Unit, - bytes: &[u8], - flags: u32, - input: Lent<'_, OnPieceIn>, - out: &mut Out<'_, OnPieceOut>, -) -> bool { - let n = input.reply_buf().stream(bytes); - unit.owed.clear(); - unit.at = 0; - unit.owed_flags = flags; - let more = n < bytes.len(); - if more { - unit.owed.extend_from_slice(&bytes[n..]); - } - out.set(|o| &o.emitted, n as u64); - out.set(|o| &o.more, u32::from(more)); - out.set(|o| &o.flags, if more { flags & !EMIT_DONE } else { flags }); - !more && flags & EMIT_DONE != 0 -} - -/// An ATTEMPT: the request bound for the far end, its verb, target and dialect fields. A new -/// attempt starts a new far-end answer. Named `attempt_piece`, not `attempt`, so it is not a -/// cross-plane re-spelling of llm/a2a's `attempt` (structure-lint plane-dup; ARCHITECT ruling 2b -/// 2026-10-04). -fn attempt_piece( - unit: &mut Unit, - input: Lent<'_, OnPieceIn>, - out: &mut Out<'_, OnPieceOut>, -) -> Outcome { - let dialect = input.get().dialect; - let request = driven::attempt_request(dialect, &[]); - let (mut fields, units, mut arena) = (input.fields_buf(), input.units_buf(), input.arena_buf()); - let verb = arena.span(request.verb.as_bytes()); - let target = arena.span(request.target.as_bytes()); - for (name, value) in request.fields { - fields.push(OutField { - name: arena.span(name.as_bytes()), - value: arena.span(value), - }); - } - for (name, value) in unit.caller.iter() { - fields.push(OutField { - name: arena.span(name), - value: arena.span(value), - }); - } - if settle(out, &fields, &units, &arena) { - return Outcome::Failed; - } - *unit = Unit { - attempt: true, - caller: std::mem::take(&mut unit.caller), - upstream: unit.upstream.take(), - ticket: unit.ticket, - seq: unit.seq, - owed: std::mem::take(&mut unit.owed), - reading: FarEndReading::for_dialect(dialect), - ..Unit::default() - }; - unit.owed.clear(); - out.set(|o| &o.verb, verb); - out.set(|o| &o.target, target); - out.set(|o| &o.flags, EMIT_TO_FAR_END); - Outcome::Ready -} - -/// One piece of the far end's answer: read, and relayed to the caller unchanged with its status -/// and document type on the first piece and its count on the last. Answers the outcome and -/// whether the unit is done. -fn far_end( - unit: &mut Unit, - input: Lent<'_, OnPieceIn>, - out: &mut Out<'_, OnPieceOut>, -) -> (Outcome, bool) { - let given = input.get(); - let bytes = input.field(|i| &i.bytes).bytes(); - let first = given.flags & PIECE_HAS_STATUS != 0; - let last = given.flags & PIECE_LAST != 0; - if !unit.recall { - unit.reading.piece(bytes); - if first { - unit.status = Some(given.status_code); - } - } - let (mut fields, mut units, mut arena) = - (input.fields_buf(), input.units_buf(), input.arena_buf()); - if first { - for f in input.head_fields().iter() { - let name = f.field(|f| &f.name).bytes(); - if name.eq_ignore_ascii_case(FIELD_CONTENT_TYPE.as_bytes()) { - fields.push(OutField { - name: arena.span(FIELD_CONTENT_TYPE.as_bytes()), - value: arena.span(f.field(|f| &f.value).bytes()), - }); - } - } - } - if last { - // THE MONEY RULE (THE DESIGN section 7): a success bills its decision count, its tokens and - // its fee; a usage member that is not a whole count fails the unit loudly, never a silent 0. - let counts = match unit.reading.counts(unit.status) { - Ok(counts) => counts, - Err(driven::Unreadable(pointer)) => { - return ( - out.fail(Refusal::failed(format!( - "the far end's answer carries `{pointer}`, which is not a whole count" - ))), - true, - ); - } - }; - for count in counts.into_iter().flatten() { - units.push(UnitCount { - class: count.class, - source: UNITS_REPORTED, - amount: count.amount, - }); - } - } - if settle(out, &fields, &units, &arena) { - unit.recall = true; - return (Outcome::Failed, false); - } - unit.recall = false; - if first { - out.set(|o| &o.reply_status, given.status_code); - } - let done = owe(unit, bytes, if last { EMIT_DONE } else { 0 }, input, out); - (Outcome::Ready, done) -} - -/// At [`MAX_UNITS`], the oldest unit that arrived [`MAX_UNITS`] or more arrivals before `seq` and -/// never crossed a piece is reclaimed: the kernel never started it, and keeping it would leave the -/// instance refusing every arrival for good. A unit that crossed a piece is never reclaimed. -fn reclaim_stale(units: &mut BTreeMap, key: u64, seq: u64) { - if units.len() < MAX_UNITS || units.contains_key(&key) { - return; - } - let stale = units - .iter() - .filter(|(_, u)| u.ticket.is_none() && seq.saturating_sub(u.seq) >= MAX_UNITS as u64) - .min_by_key(|(_, u)| u.seq) - .map(|(k, _)| *k); - if let Some(stale) = stale { - units.remove(&stale); - } -} - -/// `unit`'s state in `units`, a new entry when it has none; `None` when it has none and -/// [`MAX_UNITS`] live already (a live unit is never dropped to make room). -fn unit_of(units: &mut BTreeMap, key: u64) -> Option<&mut Unit> { - if units.len() >= MAX_UNITS && !units.contains_key(&key) { - return None; - } - Some(units.entry(key).or_default()) -} - -slot!( - /// `on_piece`: the ATTEMPT's request head, the caller's body to the far end, and the far - /// end's answer to the caller. A finished unit is forgotten. - OnPiece, OnPieceIn, OnPieceOut, |instance, input, mut out| { - let Some(plane) = instance.get() else { - return Outcome::Failed; - }; - let given = input.get(); - let bytes = input.field(|i| &i.bytes).bytes(); - plane.units.with_all(|units| { - let Some(unit) = unit_of(units, given.unit) else { - return Outcome::Refused; - }; - unit.ticket = Some(given.head.ticket); - // A re-call after `more = 1` carries no bytes, no flags and no attempt: it is paid from - // what is owed. - let continues = bytes.is_empty() && given.flags == 0 && given.attempt_no == 0; - let (outcome, done) = if continues && unit.at < unit.owed.len() { - (Outcome::Ready, pay(unit, input, &mut out)) - } else { - match given.from { - FROM_KERNEL if given.attempt_no != 0 => { - (attempt_piece(unit, input, &mut out), false) - } - FROM_CALLER => match driven::caller_piece(bytes, given.flags & PIECE_LAST != 0) { - CallerAnswer::Empty => (Outcome::Refused, true), - CallerAnswer::Keep if unit.attempt => { - let spliced = - unit.upstream.as_deref().and_then(|up| driven::spliced(bytes, up)); - owe(unit, spliced.as_deref().unwrap_or(bytes), EMIT_TO_FAR_END, input, &mut out); - (Outcome::Ready, false) - } - CallerAnswer::Keep => (Outcome::Ready, false), - }, - FROM_FAR_END => far_end(unit, input, &mut out), - _ => (Outcome::Refused, false), - } - }; - if done { - units.remove(&given.unit); - } - outcome - }) - } -); - -slot!( - /// `refusal`: the kernel's status and text in this dialect's error shape, as JSON, and the - /// wait the kernel handed beside it (`retry_after_s`, a limit's rolling window or the walk's - /// floor) as `Retry-After`, as every plane answers it. - RefusalSlot, RefusalIn, RefusalOut, |instance, input, mut out| { - let given = input.get(); - // The refusal ends the unit: its state goes with it. - if let Some(plane) = instance.get() { - if given.unit != 0 { - plane.units.remove(&given.unit); - } - } - let status = u16::try_from(given.status).unwrap_or(0); - let text = input.field(|i| &i.text).as_str().unwrap_or_default(); - let body = driven::refusal_body(status, text); - let (mut reply, mut fields, mut arena) = - (input.reply_buf(), input.fields_buf(), input.arena_buf()); - reply.extend(&body); - fields.push(OutField { - name: arena.span(FIELD_CONTENT_TYPE.as_bytes()), - value: arena.span(CONTENT_TYPE_JSON), - }); - if given.retry_after_s > 0 { - let wait = given.retry_after_s.to_string(); - fields.push(OutField { - name: arena.span(FIELD_RETRY_AFTER.as_bytes()), - value: arena.span(wait.as_bytes()), - }); - } - let short = !(reply.fits() && fields.fits() && arena.fits()); - let (rw, rnd) = reply.settle(short); - let (fw, fnd) = fields.settle(short); - let (aw, and) = arena.settle(short); - out.set(|o| &o.reply_written, rw as u64); - out.set(|o| &o.reply_needed, rnd as u64); - out.set(|o| &o.fields_written, fw as u32); - out.set(|o| &o.fields_needed, fnd as u32); - out.set(|o| &o.arena_written, aw as u64); - out.set(|o| &o.arena_needed, and as u64); - if short { - Outcome::Failed - } else { - Outcome::Ready - } - } -); - -slot!( - /// `serve`: the plane publishes no admin route. - Serve, ServeIn, ServeOut, |_, _, _| { Outcome::Refused } -); - -slot!( - /// `hydrate`: the plane keeps no durable state. - Hydrate, GenIn, OutHead, |_, _, _| { Outcome::Ready } -); - -slot!( - /// `start`: the plane runs nothing of its own. - Start, GenIn, OutHead, |_, _, _| { Outcome::Ready } -); - -slot!( - /// `project`: the plane binds no hook view; it never reads the decision content. - Project, ProjectIn, ProjectOut, |_, _, _| { Outcome::Refused } -); - -busbar_contract::plugin_door! { - ops: busbar_contract::abi::plane::Ops, - statement: STATEMENT, - lifecycle: { - validate: Safe, open: Safe, refresh: Safe, retire: Safe, - tick: Safe, drive: Safe, cancel: Safe, release: Safe, - close: Safe, - }, - kind_ops: { - arrive: Safe, on_piece: Safe, refusal: Safe, - serve: Safe, hydrate: Safe, start: Safe, project: Safe, - }, -} - -#[cfg(test)] -#[path = "tests/plane_door.rs"] -mod tests; diff --git a/crates/busbar-plane-decisions/src/records.rs b/crates/busbar-plane-decisions/src/records.rs deleted file mode 100644 index ad617d09c4..0000000000 --- a/crates/busbar-plane-decisions/src/records.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! This plane's durable state — which is none. -//! -//! MCP and A2A keep kernel-held records because their exchanges are stateful across calls: a tool -//! catalogue, a task and its event history, a push-notification pin. jev is neither: each of its -//! two operations is a single request/response pair against a provider/account-scoped upstream, the -//! provider's `/v1/models` answer is never cached (the design's C-6 ruling is explicit — no busbar -//! catalogue synthesis, the provider's list is relayed as-is on every call), and `systemone` mints -//! no busbar-side identifier a later call would need to look back up. So this plane declares no -//! record schemas and `plane.rs::route` names no record leg — every leg it returns is a hop to the -//! configured provider. -//! -//! This is a real design choice, not an omission: if a future decision vendor's dialect needed -//! durable state (a multi-turn decision session, say), that vendor's dialect would declare the -//! schema it needs here, exactly as A2A declares its four. - -use busbar_contract::ids::RecordSchemaId; - -/// The record schemas this plane keeps kernel-held durable records under — empty, see the module -/// note. -pub const RECORD_SCHEMAS: &[RecordSchemaId] = &[]; - -#[cfg(test)] -#[path = "tests/records.rs"] -mod tests; diff --git a/crates/busbar-plane-decisions/src/tests/claims.rs b/crates/busbar-plane-decisions/src/tests/claims.rs deleted file mode 100644 index f029a886da..0000000000 --- a/crates/busbar-plane-decisions/src/tests/claims.rs +++ /dev/null @@ -1,26 +0,0 @@ -use super::*; -use busbar_contract::grammar::Selector; - -#[test] -fn both_claims_are_exact_paths_on_http() { - assert_eq!(CLAIMS.len(), 2); - for claim in CLAIMS { - assert_eq!(claim.transport, TRANSPORT); - assert!(matches!(claim.selector, Selector::ExactPath(_))); - assert_eq!(claim.scheme, Some(SCHEME)); - assert_eq!(claim.scheme_alternatives, &[ALT_TOKEN]); - } -} - -#[test] -fn claims_cover_both_declared_ops_paths() { - let paths: Vec<&str> = CLAIMS - .iter() - .map(|c| match c.selector { - Selector::ExactPath(p) => p, - _ => panic!("every claim here is an exact path"), - }) - .collect(); - assert!(paths.contains(&crate::ops::PATH_SYSTEMONE)); - assert!(paths.contains(&crate::ops::PATH_MODELS)); -} diff --git a/crates/busbar-plane-decisions/src/tests/codec.rs b/crates/busbar-plane-decisions/src/tests/codec.rs deleted file mode 100644 index d5943b1e50..0000000000 --- a/crates/busbar-plane-decisions/src/tests/codec.rs +++ /dev/null @@ -1,58 +0,0 @@ -use super::*; - -const SUCCESS: &[u8] = - br#"{"request_id":"req_1","usage":{"units":42},"answers":{"decision":"approve"}}"#; - -const FAILURE: &[u8] = - br#"{"error":{"code":"invalid_state","message":"bad"},"state":{"secret":"caller-state"},"answers":{"x":1}}"#; - -#[test] -fn reads_the_declared_pointers_off_a_success_body() { - assert!(!has(SUCCESS, PTR_ERROR)); - assert_eq!(read_str(SUCCESS, PTR_REQUEST_ID), Some("req_1")); - assert_eq!(read_u64(SUCCESS, PTR_USAGE_UNITS), Some(42)); -} - -#[test] -fn reads_the_declared_pointers_off_a_failure_body() { - assert!(has(FAILURE, PTR_ERROR)); - // No request_id/id in this fixture, and no usage under an error — both read back as absent. - assert_eq!(read_str(FAILURE, PTR_REQUEST_ID), None); - assert_eq!(read_str(FAILURE, PTR_ID), None); - assert_eq!(read_u64(FAILURE, PTR_USAGE_UNITS), None); -} - -/// A request is read at its top-level `model` alone (DECISIONS D8b), never at `state`. -#[test] -fn request_ptrs_is_the_model_alone() { - assert_eq!(REQUEST_PTRS, &[PTR_MODEL]); - assert_eq!( - read_str(br#"{"model":"jev","state":{}}"#, PTR_MODEL), - Some("jev") - ); -} - -/// No declared pointer passes through a `state` or `answers` member (a member merely spelled with -/// those letters, as NanoJev's `/execution/states`, is another member). -#[test] -fn response_ptrs_never_names_state_or_answers() { - for p in RESPONSE_PTRS { - assert!( - !p.split('/').any(|seg| seg == "state" || seg == "answers"), - "{p}" - ); - } -} - -#[test] -fn read_raw_never_resolves_a_pointer_this_module_does_not_declare() { - // `/state` and `/answers` are real pointers in this fixture — `read_raw` CAN resolve them if - // asked, because it is a general primitive. What matters is that nothing in `RESPONSE_PTRS` or - // `REQUEST_PTRS` ever asks: the guarantee lives in the declared list, not in the primitive - // refusing. This test documents that split rather than asserting a false safety property of - // `read_raw` itself. - assert!(read_raw(FAILURE, "/state").is_some()); - assert!(read_raw(FAILURE, "/answers").is_some()); - assert!(!RESPONSE_PTRS.contains(&"/state")); - assert!(!RESPONSE_PTRS.contains(&"/answers")); -} diff --git a/crates/busbar-plane-decisions/src/tests/config.rs b/crates/busbar-plane-decisions/src/tests/config.rs deleted file mode 100644 index 5b70a68ccc..0000000000 --- a/crates/busbar-plane-decisions/src/tests/config.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::collections::{HashMap, HashSet}; - -use super::*; - -fn providers(pairs: &[(&str, &str)]) -> HashMap { - pairs - .iter() - .map(|(name, protocol)| (name.to_string(), protocol.to_string())) - .collect() -} - -fn hooks(names: &[&str]) -> HashSet { - names.iter().map(|n| n.to_string()).collect() -} - -/// CONTROL: a config naming a real `jev`-protocol provider and a real hook boots clean — zero -/// errors. Proves the three refusals below are each triggered by their OWN defect, not by the -/// mere presence of a `decisions:` section. -#[test] -fn a_good_decisions_config_passes_clean() { - let json = r#"{"models":{"jev":{"provider":"typesafe"}},"hooks":["redact"]}"#; - let section: DecisionsSection = serde_json::from_str(json).expect("valid shape parses"); - let errors = validate_cross_refs( - §ion, - &providers(&[("typesafe", PROTOCOL)]), - &hooks(&["redact"]), - ); - assert!(errors.is_empty(), "good config refused: {errors:?}"); -} - -/// DEFECT 1: `decisions.models..provider` names a provider absent from `providers:`. -#[test] -fn refuses_a_model_naming_an_undefined_provider() { - let json = r#"{"models":{"jev":{"provider":"ghost"}}}"#; - let section: DecisionsSection = serde_json::from_str(json).expect("valid shape parses"); - let errors = validate_cross_refs(§ion, &providers(&[("typesafe", PROTOCOL)]), &hooks(&[])); - assert_eq!(errors.len(), 1, "{errors:?}"); - assert!( - errors[0].contains("decisions.models.jev.provider"), - "{errors:?}" - ); - assert!(errors[0].contains("ghost"), "{errors:?}"); - assert!( - errors[0].contains("typesafe"), - "{errors:?}: must name the valid choices" - ); -} - -/// DEFECT 2: `decisions.hooks` names a hook absent from the top-level `hooks:` registry. -#[test] -fn refuses_a_hook_naming_an_undefined_hook() { - let json = r#"{"models":{},"hooks":["ghost-hook"]}"#; - let section: DecisionsSection = serde_json::from_str(json).expect("valid shape parses"); - let errors = validate_cross_refs(§ion, &providers(&[]), &hooks(&["redact"])); - assert_eq!(errors.len(), 1, "{errors:?}"); - assert!(errors[0].contains("decisions.hooks"), "{errors:?}"); - assert!(errors[0].contains("ghost-hook"), "{errors:?}"); - assert!( - errors[0].contains("redact"), - "{errors:?}: must name the valid choices" - ); -} - -/// DEFECT 3 (#51, OWNER-LOCKED): a model whose provider resolves to a dialect the decisions plane -/// does not speak (non-jev) must FAIL CLOSED — the exact row #51 cites: "the decisions plane (only -/// jev) handed `anthropic` fails". -#[test] -fn refuses_a_model_whose_provider_speaks_a_non_jev_dialect() { - let json = r#"{"models":{"jev":{"provider":"claude"}}}"#; - let section: DecisionsSection = serde_json::from_str(json).expect("valid shape parses"); - let errors = validate_cross_refs( - §ion, - &providers(&[("claude", "anthropic")]), - &hooks(&[]), - ); - assert_eq!(errors.len(), 1, "{errors:?}"); - assert!( - errors[0].contains("decisions.models.jev.provider"), - "{errors:?}" - ); - assert!(errors[0].contains("anthropic"), "{errors:?}"); - assert!(errors[0].contains(PROTOCOL), "{errors:?}"); -} - -/// An undefined provider is reported ONCE, as the "unknown provider" defect — not also re-reported -/// as a dialect mismatch (there is no protocol to compare when the provider does not exist). -#[test] -fn an_undefined_provider_is_not_also_reported_as_a_dialect_mismatch() { - let json = r#"{"models":{"jev":{"provider":"ghost"}}}"#; - let section: DecisionsSection = serde_json::from_str(json).expect("valid shape parses"); - let errors = validate_cross_refs(§ion, &providers(&[]), &hooks(&[])); - assert_eq!(errors.len(), 1, "{errors:?}"); -} - -#[test] -fn parses_the_ruling_canonical_shape() { - let json = r#"{"models":{"jev":{"provider":"typesafe","upstream_model":"jev-1.13.0"}}}"#; - let section: DecisionsSection = serde_json::from_str(json).expect("canonical shape parses"); - let model = section.models.get("jev").expect("the jev model is present"); - assert_eq!(model.provider, "typesafe"); - assert_eq!(model.upstream_model.as_deref(), Some("jev-1.13.0")); - assert!(section.hooks.is_empty()); - assert!(section.upstream_credentials.is_none()); -} - -#[test] -fn refuses_an_unknown_top_level_member() { - let json = r#"{"models":{},"not_a_real_member":1}"#; - let err = serde_json::from_str::(json) - .expect_err("deny_unknown_fields refuses a typo'd member"); - assert!(err.to_string().contains("not_a_real_member") || err.to_string().contains("unknown")); -} - -#[test] -fn upstream_credentials_accepts_own_and_passthrough() { - for (word, expect_passthrough) in [("own", false), ("passthrough", true)] { - let json = format!(r#"{{"models":{{}},"upstream_credentials":"{word}"}}"#); - let section: DecisionsSection = - serde_json::from_str(&json).expect("a reserved value parses"); - assert_eq!( - section.upstream_credentials, - Some(if expect_passthrough { - busbar_contract::config::UpstreamCreds::Passthrough - } else { - busbar_contract::config::UpstreamCreds::Own - }) - ); - } -} - -#[test] -fn hooks_is_a_plain_name_list() { - let json = r#"{"models":{},"hooks":["redact","budget-gate"]}"#; - let section: DecisionsSection = serde_json::from_str(json).expect("hooks list parses"); - assert_eq!( - section.hooks, - vec!["redact".to_string(), "budget-gate".to_string()] - ); -} - -/// RED, A MODEL'S DIALECT OVERRIDE (#51; ARCHITECT Q9(a), `ModelCfg += protocol`): a -/// `decisions.models..protocol` parses into the shared `ModelCfg` (it was refused as an unknown -/// member), absent reads as none, and an unknown member is still refused, naming `protocol` last. -#[test] -fn a_models_protocol_override_parses() { - let json = r#"{"models":{"nano":{"provider":"typesafe","protocol":"nanojev"},"jev":{"provider":"typesafe"}}}"#; - let section: DecisionsSection = serde_json::from_str(json).expect("the override parses"); - assert_eq!(section.models["nano"].protocol.as_deref(), Some("nanojev")); - assert_eq!(section.models["jev"].protocol, None); - let err = serde_json::from_str::( - r#"{"models":{"m":{"provider":"p","bogus_key":1}}}"#, - ) - .expect_err("an unknown member is still refused") - .to_string(); - assert!(err.contains("`prompt_caching`, `protocol`"), "{err}"); -} diff --git a/crates/busbar-plane-decisions/src/tests/driven.rs b/crates/busbar-plane-decisions/src/tests/driven.rs deleted file mode 100644 index fe073ef39c..0000000000 --- a/crates/busbar-plane-decisions/src/tests/driven.rs +++ /dev/null @@ -1,583 +0,0 @@ -use super::*; -use crate::meta::CLASS_DECISION; - -#[test] -fn systemone_arrives_as_the_first_op_class_with_a_principal() { - let a = arrive("POST", ops::PATH_SYSTEMONE).expect("systemone is claimed"); - assert_eq!(a.op, ops::OP_SYSTEMONE); - assert_eq!(a.op_class, 0); - assert_eq!(tail::OP_CLASSES[a.op_class as usize], ops::OP_SYSTEMONE); - assert_eq!(a.principal, PrincipalNeed::Required); - assert_eq!(tail::DIALECTS[a.dialect as usize], "jev"); - let e = arrive("POST", ops::PATH_EVALUATE).expect("evaluate is claimed"); - assert_eq!(e.op, ops::OP_EVALUATE); - assert_eq!(tail::OP_CLASSES[e.op_class as usize], ops::OP_EVALUATE); - assert_eq!(tail::DIALECTS[e.dialect as usize], "nanojev"); -} - -#[test] -fn models_is_not_claimed() { - assert_eq!(arrive("GET", ops::PATH_MODELS), None); - assert!(!tail::CLAIMS.iter().any(|(_, t, _)| *t == ops::PATH_MODELS)); - assert!(!tail::OP_CLASSES.contains(&ops::OP_MODELS)); -} - -#[test] -fn an_unknown_request_is_not_claimed() { - assert_eq!(arrive("GET", ops::PATH_SYSTEMONE), None); - assert_eq!(arrive("POST", "/v1/elsewhere"), None); -} - -#[test] -fn every_claim_arrives() { - for (verb, target, dialect) in tail::CLAIMS { - let a = arrive(verb, target).unwrap_or_else(|| panic!("{verb} {target}")); - assert_eq!(a.dialect, *dialect, "{verb} {target}"); - assert_eq!(attempt_request(*dialect, b"{}").target, *target); - } -} - -/// RED (audit plane-decisions#5): THE DESIGN section 7, "its card prices `decision`, `input_tokens` -/// and `output_tokens` (family `decision`, all three required, 0 = free)". -#[test] -fn the_tail_names_the_decision_class_at_its_index() { - let (class, family) = tail::BILLABLE_CLASSES[tail::CLASS_DECISION_INDEX as usize]; - assert_eq!(class, CLASS_DECISION); - assert_eq!(family, "decision"); - for (index, name) in [ - (tail::CLASS_INPUT_TOKENS_INDEX, "input_tokens"), - (tail::CLASS_OUTPUT_TOKENS_INDEX, "output_tokens"), - ] { - let (class, family) = tail::BILLABLE_CLASSES[index as usize]; - assert_eq!((class.as_str(), family), (name, "decision")); - } - assert_eq!(tail::BILLABLE_CLASSES.len(), 4); - // The request fee is the one fee unit, and it is a class at its own index. - assert_eq!(tail::FEE_UNITS, &[busbar_contract::plane::PER_REQUEST]); - let (fee, fee_family) = tail::BILLABLE_CLASSES[tail::CLASS_FEE_INDEX as usize]; - assert_eq!(fee.as_str(), tail::FEE_PER_REQUEST); - assert_eq!(fee_family, tail::FEE_FAMILY); - assert_eq!(tail::SECTION_DECLARING, crate::config::SECTION); - assert_eq!(tail::NEEDS, &[(crate::claims::TRANSPORT, "bearer")]); - assert_eq!(tail::DIALECT_AUTH, &[(0, "bearer"), (1, "bearer")]); - assert_eq!(tail::DIALECTS, &["jev", "nanojev"]); -} - -#[test] -fn the_caller_body_is_kept_and_an_empty_one_is_refused() { - assert_eq!(caller_piece(b"{\"state\":{}}", true), CallerAnswer::Keep); - assert_eq!(caller_piece(b"", false), CallerAnswer::Keep); - assert_eq!(caller_piece(b"", true), CallerAnswer::Empty); -} - -#[test] -fn an_attempt_forwards_the_caller_body_unchanged() { - let body = br#"{"state":{"session":"s"},"context":{}}"#; - let r = attempt_request(tail::DIALECT_JEV, body); - assert_eq!(r.verb, "POST"); - assert_eq!(r.target, "/v1/systemone"); - assert_eq!(r.fields, [("content-type", &b"application/json"[..])]); - assert_eq!(r.auth, "decision-egress"); - assert!( - std::ptr::eq(r.body, &body[..]), - "the body is borrowed, not copied" - ); -} - -/// The answer's counts as `(class, amount)`, for a far end answering `status` with `body`. -fn billed(status: u32, body: &[u8]) -> Result, Unreadable> { - let mut reading = FarEndReading::new(); - reading.piece(body); - reading.counts(Some(status)).map(|c| { - c.into_iter() - .flatten() - .map(|u| (u.class, u.amount)) - .collect() - }) -} - -const DECISION: u32 = tail::CLASS_DECISION_INDEX; -const FEE: u32 = tail::CLASS_FEE_INDEX; -const INPUT: u32 = tail::CLASS_INPUT_TOKENS_INDEX; -const OUTPUT: u32 = tail::CLASS_OUTPUT_TOKENS_INDEX; - -#[test] -fn far_end_pieces_are_relayed_unchanged_and_the_count_read_over_the_whole_answer() { - let mut reading = FarEndReading::new(); - let a = br#"{"request_id":"req_1","usage":{"un"#; - let b = br#"its":42},"answers":{"decision":"approve"}}"#; - assert_eq!(reading.piece(a), &a[..]); - assert_eq!(reading.piece(b), &b[..]); - let counts = reading.counts(Some(200)).expect("a whole count"); - assert_eq!( - counts[0], - Some(Units { - class: DECISION, - reported: true, - amount: 42 - }) - ); -} - -#[test] -fn an_error_answer_bills_nothing() { - assert_eq!( - billed( - 200, - br#"{"error":{"code":"invalid_state"},"usage":{"units":3}}"# - ), - Ok(vec![]) - ); -} - -/// RED (audit plane-decisions#4): THE DESIGN section 7, "bills one `decision` per successful, settled -/// answer from every provider ... the hosted provider keeps 1.5.5's `/usage/units` as the decision -/// count". A success with no count bills one decision; one with a whole count bills the count. -#[test] -fn a_success_bills_one_decision_or_the_count_it_reports() { - assert_eq!(billed(200, br#"{}"#), Ok(vec![(DECISION, 1), (FEE, 1)])); - assert_eq!( - billed(200, br#"{"usage":{}}"#), - Ok(vec![(DECISION, 1), (FEE, 1)]) - ); - assert_eq!( - billed(201, br#"{"usage":{"units":3}}"#), - Ok(vec![(DECISION, 3), (FEE, 1)]) - ); - assert_eq!( - billed(200, br#"{"usage":{"units":3},"error":null}"#), - Ok(vec![(DECISION, 3), (FEE, 1)]), - "a null error is no error" - ); - let max = format!(r#"{{"usage":{{"units":{}}}}}"#, i64::MAX); - assert_eq!( - billed(200, max.as_bytes()), - Ok(vec![(DECISION, i64::MAX as u64), (FEE, 1)]) - ); -} - -/// RED (audit plane-decisions#4): one success rule for every count: a non-2xx answer bills nothing, -/// whatever usage it carries. -#[test] -fn a_non_success_bills_nothing() { - for status in [400, 422, 500, 503] { - assert_eq!( - billed(status, br#"{"usage":{"units":2}}"#), - Ok(vec![]), - "{status}" - ); - } - let mut reading = FarEndReading::new(); - reading.piece(br#"{"usage":{"units":2}}"#); - assert_eq!( - reading.counts(None), - Ok([None; 4]), - "no status read, no bill" - ); -} - -/// RED (audit plane-decisions#4): a success whose usage member is there but is not a whole count -/// fails loudly, naming the member (finding 395 kept: "LOCATED, never dropped"), never a silent 0. -#[test] -fn a_usage_member_that_is_not_a_whole_count_fails_loudly() { - for units in [ - "-1".to_string(), - "1.5".to_string(), - "7.0".to_string(), - "1e3".to_string(), - "\"7\"".to_string(), - "null".to_string(), - "{}".to_string(), - (i64::MAX as u64 + 1).to_string(), - u64::MAX.to_string(), - ] { - let body = format!(r#"{{"usage":{{"units":{units}}}}}"#); - assert_eq!( - billed(200, body.as_bytes()), - Err(Unreadable(crate::codec::PTR_USAGE_UNITS)), - "{body}" - ); - } - assert_eq!( - billed(200, br#"{"usage":{"units":1,"output_tokens":-2}}"#), - Err(Unreadable(crate::codec::PTR_USAGE_OUTPUT_TOKENS)) - ); -} - -/// RED (audit plane-decisions#5): the token counts a success reports are billed in their classes. -#[test] -fn a_success_bills_the_tokens_it_reports() { - assert_eq!( - billed( - 200, - br#"{"usage":{"units":2,"input_tokens":12,"output_tokens":5}}"# - ), - Ok(vec![(DECISION, 2), (INPUT, 12), (OUTPUT, 5), (FEE, 1)]) - ); - assert_eq!( - billed(200, br#"{"usage":{"input_tokens":9}}"#), - Ok(vec![(DECISION, 1), (INPUT, 9), (FEE, 1)]) - ); -} - -#[test] -fn the_counts_never_read_state_or_answers() { - assert_eq!( - billed( - 200, - br#"{"usage":{"units":7},"state":{"usage":{"units":999}},"answers":{"units":888}}"# - ), - Ok(vec![(DECISION, 7), (FEE, 1)]) - ); -} - -#[test] -fn a_refusal_carries_the_kernel_text_in_the_dialect_shape() { - assert_eq!( - refusal_body(403, "the caller may not perform this operation"), - br#"{"error":{"code":"unsupported_operation","message":"the caller may not perform this operation"}}"# - ); - assert_eq!( - refusal_body(401, "no key"), - br#"{"error":{"code":"invalid_request","message":"no key"}}"# - ); - assert_eq!( - refusal_body(404, "no provider"), - br#"{"error":{"code":"invalid_params","message":"no provider"}}"# - ); - assert_eq!( - refusal_body(500, "x"), - br#"{"error":{"code":"internal","message":"x"}}"# - ); - assert_eq!( - refusal_body(503, "x"), - br#"{"error":{"code":"unsupported_operation","message":"x"}}"# - ); -} - -/// RED (audit plane-decisions#1): every generation with a model mounts the claim (THE DESIGN -/// section 2: "more than one and none named is 400", so several models are served); none mounts -/// nothing, and the kernel's unclaimed-route bytes stand. -#[test] -fn one_or_more_models_mount_the_claim() { - let systemone = ("POST", ops::PATH_SYSTEMONE, tail::DIALECT_JEV); - let evaluate = ("POST", ops::PATH_EVALUATE, tail::DIALECT_NANOJEV); - assert!(served_claims(&[]).is_empty()); - assert_eq!(served_claims(&[0]), vec![systemone]); - assert_eq!(served_claims(&[0, 0]), vec![systemone]); - assert_eq!(served_claims(&[1]), vec![evaluate]); - assert_eq!(served_claims(&[1, 0, 1]), vec![systemone, evaluate]); - assert_eq!(dialect_of("nanojev"), Some(tail::DIALECT_NANOJEV)); - assert_eq!(dialect_of("anthropic"), None); -} - -fn models(names: &[&str]) -> std::collections::BTreeMap { - names.iter().map(|n| ((*n).to_string(), ())).collect() -} - -/// MODEL RESOLUTION (THE DESIGN section 2): the named model; one configured and none named is the -/// default; several and none named, or a `model` that is not a string, is 400; an unknown name 404. -#[test] -fn a_request_routes_over_the_model_it_names() { - let two = models(&["a", "b"]); - assert_eq!( - route_model(br#"{"model":"b"}"#, &two, |_| true).map(|r| r.0), - Ok("b") - ); - assert_eq!( - route_model(br#"{"model":"\u0061"}"#, &two, |_| true).map(|r| r.0), - Ok("a"), - "an escaped name is the name it spells" - ); - assert_eq!( - route_model(br#"{"state":{}}"#, &two, |_| true), - Err(Unrouted::Unnamed) - ); - assert_eq!( - route_model(br#"{"model":"zz"}"#, &two, |_| true), - Err(Unrouted::Unknown("zz".to_string())) - ); - assert_eq!( - route_model(br#"{"model":null}"#, &two, |_| true), - Err(Unrouted::NotAName) - ); - assert_eq!( - route_model(br#"{"model":["a"]}"#, &two, |_| true), - Err(Unrouted::NotAName) - ); - // A nested `model` is not the request's. - assert_eq!( - route_model(br#"{"state":{"model":"a"}}"#, &two, |_| true), - Err(Unrouted::Unnamed) - ); - - let one = models(&["jev"]); - assert_eq!( - route_model(br#"{"state":{}}"#, &one, |_| true).map(|r| r.0), - Ok("jev") - ); - assert_eq!( - route_model(b"not json", &one, |_| true).map(|r| r.0), - Ok("jev") - ); - assert_eq!( - route_model(br#"{"model":"other"}"#, &one, |_| true), - Err(Unrouted::Unknown("other".to_string())) - ); - assert_eq!( - route_model(br#"{}"#, &models(&[]), |_| true), - Err(Unrouted::Unnamed) - ); - - assert_eq!( - (Unrouted::Unnamed.status(), Unrouted::NotAName.status()), - (400, 400) - ); - assert_eq!(Unrouted::Unknown("zz".to_string()).status(), 404); -} - -/// THE `upstream_model` SPLICE (THE DESIGN section 2): the top-level value alone, when it differs. -#[test] -fn the_splice_rewrites_the_top_level_model_value_alone() { - let body = br#"{"model" : "jev", "state":{"model":"jev"}}"#; - assert_eq!( - spliced(body, "jev-1.13.0").as_deref(), - Some(&br#"{"model" : "jev-1.13.0", "state":{"model":"jev"}}"#[..]) - ); - assert_eq!(spliced(body, "jev"), None, "the same name passes through"); - assert_eq!( - spliced(br#"{"state":{}}"#, "x"), - None, - "no model to rewrite" - ); - assert_eq!(spliced(br#"{"model":3}"#, "x"), None, "not a name"); - assert_eq!( - spliced(br#"{"model":"a"}"#, "q\"x").as_deref(), - Some(&br#"{"model":"q\"x"}"#[..]), - "the value is written as a JSON string" - ); -} - -/// BILLABLE SUCCESS ONLY: the request fee is counted once for a far end's 2xx answer with no -/// `/error` member, and never for an error answer, a non-2xx status or an answer with no status. -#[test] -fn the_request_fee_is_counted_on_a_success_alone() { - let mut ok = FarEndReading::new(); - ok.piece(br#"{"usage":{"units":3}}"#); - let fee = ok.counts(Some(200)).expect("a success")[3].expect("a success owes its fee"); - assert_eq!((fee.class, fee.amount, fee.reported), (FEE, 1, true)); - // RED ARMS. - assert_eq!( - ok.counts(Some(422)), - Ok([None; 4]), - "a refused request owes none" - ); - assert_eq!(ok.counts(Some(500)), Ok([None; 4])); - assert_eq!(ok.counts(None), Ok([None; 4]), "no status read, no fee"); - assert_eq!( - billed(200, br#"{"error":{"code":"x","message":"y"}}"#), - Ok(vec![]), - "an error answer owes none" - ); -} - -/// THE CALLER'S FIELDS REACH THE FAR END (DEC-SERVE Q2, DIALECT-FIDELITY F2): every one, in order, -/// a repeated name keeping each value, but the governed set (the document type the plane writes, the -/// hop's `host`, the credential carriers), compared without case. -#[test] -fn every_caller_field_but_the_governed_set_is_relayed() { - let caller: [(&[u8], &[u8]); 7] = [ - (b"X-Trace", b"a"), - (b"Authorization", b"Bearer caller-key"), - (b"content-type", b"text/plain"), - (b"Host", b"busbar.example"), - (b"x-trace", b"b"), - (b"Proxy-Authorization", b"Basic x"), - (b"accept-language", b"en"), - ]; - let kept = relayed_fields(caller); - assert_eq!( - kept.iter() - .map(|(n, v)| (n.to_vec(), v.to_vec())) - .collect::>(), - vec![ - (b"x-trace".to_vec(), b"a".to_vec()), - (b"x-trace".to_vec(), b"b".to_vec()), - (b"accept-language".to_vec(), b"en".to_vec()), - ] - ); - assert!(relayed_fields(std::iter::empty()).is_empty()); -} - -/// The money rule's reading of a whole answer by its declared pointers, as the body would be read -/// kept whole: the reference the piece-by-piece reading is held to. -fn by_pointer(status: u32, body: &[u8]) -> Result, Unreadable> { - use crate::codec::{read_raw, PTR_ERROR}; - let ok = (200..300).contains(&status) && read_raw(body, PTR_ERROR).is_none_or(|e| e == b"null"); - if !ok { - return Ok(vec![]); - } - let count = |p: &'static str| match read_raw(body, p) { - None => Ok(None), - Some(raw) => whole(raw).map(Some).ok_or(Unreadable(p)), - }; - let mut out = vec![(DECISION, count(crate::codec::PTR_USAGE_UNITS)?.unwrap_or(1))]; - if let Some(n) = count(crate::codec::PTR_USAGE_INPUT_TOKENS)? { - out.push((INPUT, n)); - } - if let Some(n) = count(crate::codec::PTR_USAGE_OUTPUT_TOKENS)? { - out.push((OUTPUT, n)); - } - out.push((FEE, 1)); - Ok(out) -} - -/// The answer read in two pieces split at `at`. -fn split_billed(status: u32, body: &[u8], at: usize) -> Result, Unreadable> { - let mut reading = FarEndReading::new(); - reading.piece(&body[..at]); - reading.piece(&body[at..]); - reading.counts(Some(status)).map(|c| { - c.into_iter() - .flatten() - .map(|u| (u.class, u.amount)) - .collect() - }) -} - -/// RED (audit plane-decisions#8): the answer is read as it passes, in fixed space, wherever the -/// pieces split it, and reads exactly what the declared pointers read over the whole answer. -#[test] -fn the_reading_is_the_pointers_reading_wherever_the_pieces_split() { - let answers: [&[u8]; 14] = [ - br#"{"request_id":"req_1","usage":{"units":42},"answers":{"decision":"approve"}}"#, - br#" { "usage" : { "input_tokens" : 12 , "units" : 3, "output_tokens":5 } } "#, - br#"{"answers":{"usage":{"units":9}},"usage":{"units":2}}"#, - br#"{"state":{"error":"x","usage":{"units":"q"}},"usage":{}}"#, - br#"{"error":null,"usage":{"units":7}}"#, - br#"{"error":{"code":"x","message":"y"},"usage":{"units":3}}"#, - br#"{"error":"down"}"#, - br#"{"usage":{"units":7.5}}"#, - br#"{"usage":{"units":"7"}}"#, - br#"{"usage":{"units":[1]}}"#, - br#"{"usage":{"units":99999999999999999999999999}}"#, - br#"{"a\"usage":{"units":"no"},"usage":{"output_tokens":-1}}"#, - br#"{"usage":"n/a","x":[{"usage":{"units":1}}]}"#, - br#"[{"usage":{"units":5}}]"#, - ]; - for body in answers { - for status in [200, 500] { - let want = by_pointer(status, body); - for at in 0..=body.len() { - assert_eq!( - split_billed(status, body, at), - want, - "{status} {} split at {at}", - String::from_utf8_lossy(body) - ); - } - } - } -} - -/// An answer of any size is read in the reading's own fixed space. -#[test] -fn a_large_answer_is_read_without_keeping_it() { - let mut reading = FarEndReading::new(); - let before = std::mem::size_of_val(&reading); - reading.piece(br#"{"answers":{"blob":""#); - let filler = [b'x'; 4096]; - for _ in 0..256 { - reading.piece(&filler); - } - reading.piece(br#""},"usage":{"units":4}}"#); - assert_eq!(std::mem::size_of_val(&reading), before); - assert_eq!( - reading.counts(Some(200)).map(|c| c[0].map(|u| u.amount)), - Ok(Some(4)) - ); -} - -/// The counts of a `nanojev` answer, `(class, amount)`. -fn nanojev_billed(status: u32, body: &[u8]) -> Result, Unreadable> { - let mut reading = FarEndReading::for_dialect(tail::DIALECT_NANOJEV); - reading.piece(body); - reading.counts(Some(status)).map(|c| { - c.into_iter() - .flatten() - .map(|u| (u.class, u.amount)) - .collect() - }) -} - -/// RED (audit plane-decisions#3): THE DESIGN section 2, NanoJev "meters the upstream's own billing -/// unit, counted from the answer already read (`execution.states`; an answer with no count refuses -/// loudly". Its answer is read by its own pointer, never jev's. -#[test] -fn a_nanojev_answer_bills_its_execution_states() { - assert_eq!( - nanojev_billed(200, br#"{"execution":{"states":3},"result":{}}"#), - Ok(vec![(DECISION, 3), (FEE, 1)]) - ); - assert_eq!( - nanojev_billed( - 200, - br#"{"usage":{"units":9,"input_tokens":4},"execution":{"states":1}}"# - ), - Ok(vec![(DECISION, 1), (FEE, 1)]), - "jev's usage members are not nanojev's" - ); - for missing in [ - &br#"{"result":{}}"#[..], - br#"{"execution":{}}"#, - br#"{"usage":{"units":2}}"#, - ] { - assert_eq!( - nanojev_billed(200, missing), - Err(Unreadable(crate::codec::PTR_EXECUTION_STATES)), - "{}", - String::from_utf8_lossy(missing) - ); - } - assert_eq!( - nanojev_billed(200, br#"{"execution":{"states":2.5}}"#), - Err(Unreadable(crate::codec::PTR_EXECUTION_STATES)) - ); - assert_eq!( - nanojev_billed(500, br#"{"result":{}}"#), - Ok(vec![]), - "a failure bills nothing" - ); - // Wherever the pieces split it. - let body = br#"{"execution" : {"trace":[{"states":99}], "states" : 12}}"#; - for at in 0..=body.len() { - let mut reading = FarEndReading::for_dialect(tail::DIALECT_NANOJEV); - reading.piece(&body[..at]); - reading.piece(&body[at..]); - assert_eq!( - reading.counts(Some(200)).map(|c| c[0].map(|u| u.amount)), - Ok(Some(12)), - "split at {at}" - ); - } -} - -/// RED (audit plane-decisions#3): a request routes among its claim's dialect's models; a model of -/// the other dialect is this dialect's unknown model, and one model of the dialect is its default. -#[test] -fn a_model_of_the_other_dialect_is_this_dialects_unknown_model() { - let models: std::collections::BTreeMap = - [("j".to_string(), 0), ("n".to_string(), 1)].into(); - let jev = |d: &u32| *d == tail::DIALECT_JEV; - let nano = |d: &u32| *d == tail::DIALECT_NANOJEV; - assert_eq!( - route_model(br#"{"model":"n"}"#, &models, jev), - Err(Unrouted::Unknown("n".to_string())) - ); - assert_eq!( - route_model(br#"{"model":"n"}"#, &models, nano).map(|r| r.0), - Ok("n") - ); - assert_eq!(route_model(br#"{}"#, &models, jev).map(|r| r.0), Ok("j")); - assert_eq!(route_model(br#"{}"#, &models, nano).map(|r| r.0), Ok("n")); -} diff --git a/crates/busbar-plane-decisions/src/tests/facts.rs b/crates/busbar-plane-decisions/src/tests/facts.rs deleted file mode 100644 index 17bcf685ee..0000000000 --- a/crates/busbar-plane-decisions/src/tests/facts.rs +++ /dev/null @@ -1,27 +0,0 @@ -use super::*; - -#[test] -fn never_read_members_names_state_and_answers() { - assert_eq!(NEVER_READ_MEMBERS, &["state", "answers"]); -} - -#[test] -fn no_declared_fact_key_is_a_forbidden_member_name() { - let declared = SESSION_FACTS.iter().chain(CONTENT_FACTS.iter()); - for key in declared { - assert!( - !NEVER_READ_MEMBERS.contains(key), - "a declared fact key names a member this plane must never read: {key}" - ); - } -} - -#[test] -fn content_facts_names_no_pii_bearing_key() { - // A coarse but real guard: every content-fact key this plane declares is metadata, spelled to - // say so, never `state`/`answers`/a synonym of either. - for key in CONTENT_FACTS { - assert!(!key.contains("state")); - assert!(!key.contains("answer")); - } -} diff --git a/crates/busbar-plane-decisions/src/tests/lib.rs b/crates/busbar-plane-decisions/src/tests/lib.rs deleted file mode 100644 index 0fa7e488be..0000000000 --- a/crates/busbar-plane-decisions/src/tests/lib.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Tests that hold the crate root's own prose to the tree it describes. - -const LIB_RS: &str = include_str!("../lib.rs"); -const FACTS_RS: &str = include_str!("../facts.rs"); - -/// The crate's integration test files, by the path a doc cites them under. Read at compile time, so -/// a cited file that does not exist is a file this table cannot name, and a test it cites is one -/// whose definition can be searched for without the test reaching outside the crate at run time. -const TEST_FILES: &[(&str, &str)] = &[ - ( - "tests/alloc_gate.rs", - include_str!("../../tests/alloc_gate.rs"), - ), - ( - "tests/conformance.rs", - include_str!("../../tests/conformance.rs"), - ), - ( - "tests/invariance.rs", - include_str!("../../tests/invariance.rs"), - ), - ("tests/jev.rs", include_str!("../../tests/jev.rs")), - ("tests/purity.rs", include_str!("../../tests/purity.rs")), -]; - -/// Item 403. Every `tests/.rs` the crate root or the fact vocabulary cites as the home of a -/// guarantee is one of the crate's test files, and a cited `::test_name` is a test that file -/// defines. -#[test] -fn every_cited_test_file_exists_and_defines_the_cited_test() { - let mut cited = 0; - for source in [LIB_RS, FACTS_RS] { - for token in source.split('`').skip(1).step_by(2) { - if !token.starts_with("tests/") { - continue; - } - let (file, test) = match token.split_once("::") { - Some((file, test)) => (file, Some(test)), - None => (token, None), - }; - let Some((_, body)) = TEST_FILES.iter().find(|(path, _)| *path == file) else { - panic!("a doc cites `{token}`, and the crate has no test file {file}"); - }; - if let Some(test) = test { - assert!( - body.contains(&format!("fn {test}(")), - "a doc cites `{token}`, and {file} defines no such test" - ); - } - cited += 1; - } - } - assert!( - cited >= 2, - "the PII witness is cited from both lib.rs and facts.rs" - ); -} - -/// Item 398. The provider doc states what the served build does — the plane is served through its -/// door and nothing builds the plane — rather than a boot-time build nothing performs. -#[test] -fn the_provider_doc_does_not_claim_a_boot_build_that_does_not_exist() { - assert!(!LIB_RS.contains("The composition root builds one at boot")); - assert!(LIB_RS.contains("never builds a `DecisionPlane`")); -} diff --git a/crates/busbar-plane-decisions/src/tests/meta.rs b/crates/busbar-plane-decisions/src/tests/meta.rs deleted file mode 100644 index 359083402c..0000000000 --- a/crates/busbar-plane-decisions/src/tests/meta.rs +++ /dev/null @@ -1,108 +0,0 @@ -use super::*; -use busbar_contract::plane::PlaneMeta; - -#[test] -fn key_matches_the_plugin_key() { - assert_eq!(::KEY, "decision"); -} - -#[test] -fn one_meter_class_declared_for_billable_decisions() { - let classes = ::METER_CLASSES; - assert_eq!(classes.len(), 1); - assert_eq!(classes[0].key, CLASS_DECISION); - assert_eq!( - classes[0].direction, - busbar_contract::ids::ClassDirection::Response - ); - assert_eq!(classes[0].default_divisor, 1); -} - -#[test] -fn op_classes_are_the_ops_module_table() { - assert_eq!(::OP_CLASSES, ops::OP_CLASSES); -} - -#[test] -fn no_introspection_verb_is_declared() { - assert!(::INTROSPECTION_VERBS.is_empty()); -} - -#[test] -fn neither_interrupt_nor_pacing_fact_is_declared() { - assert_eq!(::INTERRUPT_FACT, None); - assert_eq!(::EGRESS_PACING_FACT, None); -} - -#[test] -fn config_schema_is_well_formed_json_naming_models_and_the_reserved_pair() { - let schema: serde_json::Value = - serde_json::from_str(::CONFIG_SCHEMA) - .expect("the config schema is valid JSON"); - let props = schema["properties"].as_object().expect("has properties"); - assert!(props.contains_key("models")); - assert!(props.contains_key("hooks")); - assert!(props.contains_key("upstream_credentials")); - // Never a `state` or `answers` property anywhere in the schema string — the schema describes - // busbar's OWN config shape, not jev's wire shape, so neither word belongs here at all. - let raw = ::CONFIG_SCHEMA; - assert!(!raw.contains("\"state\"")); - assert!(!raw.contains("\"answers\"")); -} - -/// The field names `ModelCfg` accepts, read off the type itself: `deny_unknown_fields` answers an -/// unknown member by listing every field it does know, so the list is the type's, not a copy. -fn model_cfg_fields() -> std::collections::BTreeSet { - let err = serde_json::from_str::( - r#"{"provider":"p","__not_a_model_cfg_field__":0}"#, - ) - .expect_err("an unknown member is refused"); - let text = err.to_string(); - let (_, listed) = text - .split_once("expected one of ") - .unwrap_or_else(|| panic!("serde names the accepted fields: {text}")); - listed - .split(',') - .filter_map(|part| part.split('`').nth(1)) - .map(str::to_owned) - .collect() -} - -/// Item 400. The model entry `CONFIG_SCHEMA` describes is `ModelCfg`, member for member: the same -/// property set, and the same required set (`ModelCfg` refuses a model with no `provider` and -/// accepts one with nothing else). A field added to, renamed in or removed from either side is red. -#[test] -fn config_schema_model_entry_is_model_cfg_field_for_field() { - let schema: serde_json::Value = - serde_json::from_str(::CONFIG_SCHEMA).expect("valid JSON"); - let entry = &schema["properties"]["models"]["additionalProperties"]; - let declared: std::collections::BTreeSet = entry["properties"] - .as_object() - .expect("the model entry declares its properties") - .keys() - .cloned() - .collect(); - let accepted = model_cfg_fields(); - assert_eq!( - accepted.len(), - 9, - "the probe read ModelCfg's list: {accepted:?}" - ); - assert_eq!(declared, accepted); - assert_eq!( - entry["additionalProperties"], - serde_json::Value::Bool(false) - ); - - let required: Vec<&str> = entry["required"] - .as_array() - .expect("the model entry names what it requires") - .iter() - .filter_map(serde_json::Value::as_str) - .collect(); - assert_eq!(required, ["provider"]); - assert!(serde_json::from_str::("{}").is_err()); - assert!( - serde_json::from_str::(r#"{"provider":"p"}"#).is_ok() - ); -} diff --git a/crates/busbar-plane-decisions/src/tests/ops.rs b/crates/busbar-plane-decisions/src/tests/ops.rs deleted file mode 100644 index d99737900a..0000000000 --- a/crates/busbar-plane-decisions/src/tests/ops.rs +++ /dev/null @@ -1,47 +0,0 @@ -use super::*; - -#[test] -fn row_for_matches_systemone() { - let row = row_for("POST", PATH_SYSTEMONE).expect("systemone is declared"); - assert_eq!(row.op, OP_SYSTEMONE); - assert!(row.has_request_body); -} - -#[test] -fn row_for_matches_models() { - let row = row_for("GET", PATH_MODELS).expect("models is declared"); - assert_eq!(row.op, OP_MODELS); - assert!(!row.has_request_body); -} - -#[test] -fn row_for_is_verb_case_insensitive() { - assert!(row_for("post", PATH_SYSTEMONE).is_some()); - assert!(row_for("get", PATH_MODELS).is_some()); -} - -#[test] -fn row_for_refuses_wrong_verb() { - assert!(row_for("GET", PATH_SYSTEMONE).is_none()); - assert!(row_for("POST", PATH_MODELS).is_none()); -} - -#[test] -fn row_for_refuses_unknown_path() { - assert!(row_for("GET", "/v1/unknown").is_none()); -} - -#[test] -fn op_classes_matches_the_method_table() { - assert_eq!(OP_CLASSES.len(), METHODS.len()); - for row in METHODS { - assert!(OP_CLASSES.contains(&row.op)); - } -} - -#[test] -fn row_for_matches_evaluate() { - let row = row_for("POST", PATH_EVALUATE).expect("evaluate is declared"); - assert_eq!(row.op, OP_EVALUATE); - assert!(row.has_request_body); -} diff --git a/crates/busbar-plane-decisions/src/tests/plane.rs b/crates/busbar-plane-decisions/src/tests/plane.rs deleted file mode 100644 index 4d977998b4..0000000000 --- a/crates/busbar-plane-decisions/src/tests/plane.rs +++ /dev/null @@ -1,266 +0,0 @@ -use super::*; - -#[test] -fn error_body_is_pinned_bytes() { - let body = error_body("invalid_request", "the request is too large"); - assert_eq!( - body, - br#"{"error":{"code":"invalid_request","message":"the request is too large"}}"# - ); -} - -#[test] -fn error_body_escapes_a_quote_in_the_message() { - let body = error_body("internal", "a \"quoted\" word"); - let parsed: serde_json::Value = serde_json::from_slice(&body).expect("still valid JSON"); - assert_eq!(parsed["error"]["message"], "a \"quoted\" word"); -} - -#[test] -fn refusal_render_is_total_and_never_leaks_the_specific_reason_for_admission_refusals() { - // Every `RefusalReason` this crate imports is matched explicitly in `refusal_render` — if the - // enum grows a variant the match becomes a compile error, which is the point. This test instead - // pins that the ADMISSION-CLASS refusals (budget, breaker, rate, drain, ...) all answer with the - // SAME neutral words, so a caller cannot distinguish "you're over budget" from "the breaker is - // open" by reading the response. - let admission_like = [ - RefusalReason::InFlightCap, - RefusalReason::OverBudget, - RefusalReason::BreakerOpen, - RefusalReason::RateLimited, - RefusalReason::Drain, - ]; - let rendered: Vec<_> = admission_like.into_iter().map(refusal_render).collect(); - let first = rendered[0]; - for r in &rendered[1..] { - assert_eq!( - *r, first, - "an admission-class refusal leaked a distinguishable answer" - ); - } -} - -#[test] -fn refusal_render_never_names_a_provider_or_billing_word() { - // Coarse source-level guard, in the spirit of `busbar-plane-a2a`'s own - // `the_plane_names_no_money_and_no_decision` test: the rendered WORDS a caller reads back must - // never carry `state`, `answers`, or a raw number. - for reason in [ - RefusalReason::BodyTooLarge, - RefusalReason::ScopeMissing, - RefusalReason::NoDestination, - RefusalReason::BreakerOpen, - RefusalReason::PlanePanic, - ] { - let (_, message) = refusal_render(reason); - assert!(!message.contains("state")); - assert!(!message.contains("answers")); - } -} - -// ---- the plane driven through its own trait, over the crate's shared test scaffold ---- - -#[path = "../../tests/common/mod.rs"] -mod common; - -use busbar_contract::plane::Progress; -use busbar_contract::wire::FrameCursor; - -use crate::DecisionProvider; - -/// Decode one provider answer through `decode_response` and meter it — the plane's whole metering -/// surface for one `systemone` exchange, as the kernel would drive it. -fn metered(plane: DecisionPlane, body: &[u8]) -> Vec { - let scaffold = common::Scaffold::new("http"); - let ctx = scaffold.ctx(); - let seal = common::TestSeal; - let unit = Unit::new( - &seal, - busbar_contract::UnitKey::new(1), - busbar_contract::unit::Origin::Client, - None, - None, - busbar_contract::wire::Direction::Inbound, - Some(common::principal()), - ops::OP_SYSTEMONE, - Ir::new(b"{}", &[]), - Facts::new(), - None, - ); - let frames = vec![common::response_frame(body)]; - let mut cursor = FrameCursor::new(&frames); - let Progress::Terminal { r, .. } = plane - .decode_response(&mut cursor, &common::sealed_destination(), None, &ctx) - .expect("a whole answer decodes") - else { - panic!("a whole answer decodes as terminal"); - }; - for (key, value) in r.facts.iter() { - if key == f::FACT_USAGE_UNITS { - assert!( - matches!(value, FactValue::Int(n) if n >= 0), - "the usage fact is a whole, non-negative count or absent, never {value:?}" - ); - } - } - plane.meter(&unit, &r, &ctx).lines.as_slice().to_vec() -} - -/// Item 395 (LEDGER). A success whose `/usage/units` member is PRESENT but is not a whole count the -/// plane can carry is a reported measurement, not an absent one. Before: no locator at all — the -/// same record as a provider that reported nothing. After: one location-only locator naming where -/// the figure is, so the kernel's decimal reader reads it exactly (#81) or refuses it (#42). -#[test] -fn a_success_whose_usage_member_is_not_a_whole_count_is_located_never_dropped() { - for body in [ - br#"{"usage":{"units":7.5}}"#.as_slice(), - br#"{"usage":{"units":-3}}"#.as_slice(), - br#"{"usage":{"units":"7"}}"#.as_slice(), - br#"{"usage":{"units":null}}"#.as_slice(), - br#"{"usage":{"units":18446744073709551615}}"#.as_slice(), - ] { - let lines = metered(DecisionPlane::EMPTY, body); - assert_eq!( - lines, - vec![UsageLocator { - class: CLASS_DECISION, - location: Some(Location::Arrival(ArrivalLocation::FirstFrameJsonPointer( - PTR_USAGE_UNITS - ))), - quantity: None, - lane: None, - }], - "{}", - String::from_utf8_lossy(body) - ); - } -} - -/// A whole count is carried exactly as before: the value in hand, no location. -#[test] -fn a_success_with_a_whole_count_carries_it() { - let lines = metered(DecisionPlane::EMPTY, br#"{"usage":{"units":42}}"#); - assert_eq!( - lines, - vec![UsageLocator { - class: CLASS_DECISION, - location: None, - quantity: Some(42), - lane: None, - }] - ); -} - -/// A success that reported no usage member at all posts no line: that is the settlement table's -/// "destination reported no usage" row (nothing billed, flagged disputed where a card prices the -/// class), and a locator pointing at a member that is not there would be a fabricated location. -/// An error answer posts nothing whatever its usage member says (billable-success, C-3). -#[test] -fn no_usage_member_and_an_error_answer_both_post_no_line() { - assert!(metered(DecisionPlane::EMPTY, br#"{"request_id":"r1"}"#).is_empty()); - assert!(metered( - DecisionPlane::EMPTY, - br#"{"error":{"code":"x"},"usage":{"units":7.5}}"# - ) - .is_empty()); -} - -static ONE: &[DecisionProvider] = &[DecisionProvider { - id: "typesafe-prod", - lane: busbar_contract::ids::LaneId::new("prod"), - host: "api.typesafe.ai", - transport: "http", -}]; - -static TWO: &[DecisionProvider] = &[ - DecisionProvider { - id: "typesafe-prod", - lane: busbar_contract::ids::LaneId::new("prod"), - host: "api.typesafe.ai", - transport: "http", - }, - DecisionProvider { - id: "typesafe-backup", - lane: busbar_contract::ids::LaneId::new("backup"), - host: "backup.typesafe.ai", - transport: "http", - }, -]; - -/// What `verify` answers for one `systemone` unit on a plane. -fn verify_of(plane: DecisionPlane) -> DestinationFacts { - let scaffold = common::Scaffold::new("http"); - let ctx = scaffold.ctx(); - let seal = common::TestSeal; - let unit = Unit::new( - &seal, - busbar_contract::UnitKey::new(1), - busbar_contract::unit::Origin::Client, - None, - None, - busbar_contract::wire::Direction::Inbound, - Some(common::principal()), - ops::OP_SYSTEMONE, - Ir::new(b"{}", &[]), - Facts::new(), - None, - ); - plane.verify(&unit, &ctx) -} - -/// Item 399. A request names no provider, so with more than one configured the plane cannot say -/// which one a caller meant. Before: it dialled, and asked scope for, whichever was declared first. -/// After: the same honest answer as a plane with none — the unreachable destination the trust unit -/// refuses. -#[test] -fn more_than_one_provider_resolves_to_no_destination_and_names_no_provider() { - assert_eq!( - verify_of(DecisionPlane::new(TWO)), - verify_of(DecisionPlane::EMPTY) - ); -} - -/// With exactly one provider, it is the one dialled. -#[test] -fn exactly_one_provider_is_the_one_dialled_and_judged() { - let dest = verify_of(DecisionPlane::new(ONE)); - assert_eq!( - dest, - DestinationFacts::Upstream { - transport: "http", - address: busbar_contract::UpstreamAddress::socket("api.typesafe.ai"), - lane: busbar_contract::ids::LaneId::new("prod"), - } - ); -} - -/// The draft facts `decode_ingress` writes for a `GET /v1/models` on a plane. -fn ingress_provider_fact(plane: DecisionPlane) -> Option { - let scaffold = common::Scaffold::new("http") - .with_method("GET") - .on_path(ops::PATH_MODELS); - let ctx = scaffold.ctx(); - let frames: Vec = Vec::new(); - let mut cursor = FrameCursor::new(&frames); - let Ok(Ingress::OneShot(draft)) = plane.decode_ingress(&mut cursor, None, &ctx) else { - panic!("a models request is complete on arrival"); - }; - match draft.facts.get(f::FACT_PROVIDER) { - Some(FactValue::Str(id)) => Some(id.to_string()), - None => None, - Some(other) => panic!("the provider fact is a name, never {other:?}"), - } -} - -/// Item 397. The provider a unit is dialled against is the session-scoped fact the declaration -/// says it is: declared in `SESSION_FACTS`, and written on every draft with the provider's id. -#[test] -fn the_provider_is_the_declared_and_written_session_fact() { - assert_eq!(f::SESSION_FACTS, &[f::FACT_PROVIDER]); - assert_eq!( - ingress_provider_fact(DecisionPlane::new(ONE)).as_deref(), - Some("typesafe-prod") - ); - assert_eq!(ingress_provider_fact(DecisionPlane::new(TWO)), None); - assert_eq!(ingress_provider_fact(DecisionPlane::EMPTY), None); -} diff --git a/crates/busbar-plane-decisions/src/tests/plane_door.rs b/crates/busbar-plane-decisions/src/tests/plane_door.rs deleted file mode 100644 index d8a77eb70d..0000000000 --- a/crates/busbar-plane-decisions/src/tests/plane_door.rs +++ /dev/null @@ -1,166 +0,0 @@ -use super::*; -use crate::ops; - -/// `section`'s routes with no provider facts stated. -fn routed(section: &DecisionsSection) -> BTreeMap { - routes(section, &BTreeMap::new()).expect("every model's dialect is spoken") -} - -/// RED (audit plane-decisions#3): each dialect's claim is published when a model of it is -/// configured, and wears its dialect for a refusal before `arrive`. -#[test] -fn each_dialects_claim_is_published_with_a_model_of_it() { - let jev = ClaimSpec::new("POST", ops::PATH_SYSTEMONE, claims::TRANSPORT, CLAIM_EXACT); - let evaluate = ClaimSpec { - refusal_dialect: 1, - ..ClaimSpec::new("POST", ops::PATH_EVALUATE, claims::TRANSPORT, CLAIM_EXACT) - }; - assert_eq!( - snapshot_spec(&[tail::DIALECT_NANOJEV]).claims, - vec![evaluate.clone()] - ); - assert_eq!( - snapshot_spec(&[tail::DIALECT_NANOJEV, tail::DIALECT_JEV]).claims, - vec![jev.clone(), evaluate] - ); - assert_eq!(snapshot_spec(&[tail::DIALECT_JEV]).claims, vec![jev]); -} - -/// RED (audit plane-decisions#3): THE DESIGN section 2, "a model's dialect is its override or its -/// provider's protocol"; a dialect the plane does not speak fails closed, naming those it does. -#[test] -fn a_models_dialect_is_its_override_or_its_providers_protocol() { - let section = read_settings( - br#"{"models":{"o":{"provider":"p","protocol":"nanojev"},"f":{"provider":"nano"},"j":{"provider":"p"}}}"#, - ) - .expect("the section reads"); - let facts = BTreeMap::from([ - ("nano".to_string(), "nanojev".to_string()), - ("p".to_string(), "jev".to_string()), - ]); - let r = routes(§ion, &facts).expect("every dialect is spoken"); - assert_eq!( - (r["o"].dialect, r["f"].dialect, r["j"].dialect), - ( - tail::DIALECT_NANOJEV, - tail::DIALECT_NANOJEV, - tail::DIALECT_JEV - ) - ); - let bogus = read_settings(br#"{"models":{"m":{"provider":"p","protocol":"bogus"}}}"#) - .expect("the section reads"); - let words = routes(&bogus, &facts).expect_err("an unspoken dialect fails closed"); - assert!( - words.contains("bogus") && words.contains("jev, nanojev"), - "{words}" - ); -} - -#[test] -fn the_statement_names_the_crate_version() { - let manifest = include_str!("../../Cargo.toml"); - assert!( - manifest.contains(&format!("\nversion = \"{VERSION}\"\n")), - "the Statement's version is the manifest's" - ); -} - -#[test] -fn an_empty_blob_is_the_empty_section() { - let section = read_settings(b"").expect("an empty blob reads"); - assert!(section.models.is_empty()); -} - -#[test] -fn the_settings_read_as_the_decisions_section() { - let section = read_settings(br#"{"models":{"jev":{"provider":"typesafe"}}}"#) - .expect("a well-formed section reads"); - assert_eq!(section.models.len(), 1); -} - -#[test] -fn a_member_the_section_does_not_declare_is_refused_in_the_grammars_words() { - let words = read_settings(br#"{"modles":{}}"#).expect_err("a typo is refused"); - assert!(words.contains("modles"), "{words}"); -} - -#[test] -fn one_or_more_models_claim_systemone_and_none_claims_nothing() { - let one = snapshot_spec(&[tail::DIALECT_JEV]); - assert_eq!( - one.claims, - vec![ClaimSpec::new( - "POST", - ops::PATH_SYSTEMONE, - claims::TRANSPORT, - CLAIM_EXACT - )] - ); - assert!(one.admin_routes.is_empty()); - // Served on the plain data plane to a keyed caller: no audience, no protected-resource document. - assert!(one.audience.is_none() && one.resource_metadata.is_none()); - assert!( - snapshot_spec(&[]).claims.is_empty(), - "no model claims nothing" - ); - for models in [2, 3] { - let jev = vec![tail::DIALECT_JEV; models]; - assert_eq!(snapshot_spec(&jev).claims, one.claims, "{models} models"); - } -} - -#[test] -fn a_generation_lists_every_configured_model_in_name_order() { - let two = - read_settings(br#"{"models":{"b":{"provider":"typesafe"},"a":{"provider":"typesafe"}}}"#) - .expect("the section reads"); - let spec = generation_spec(&routed(&two)); - assert_eq!(spec.listed, vec!["a".to_string(), "b".to_string()]); - assert_eq!(spec.claims, snapshot_spec(&[0, 0]).claims); - - let one = - read_settings(br#"{"models":{"jev":{"provider":"typesafe"}}}"#).expect("the section reads"); - let spec = generation_spec(&routed(&one)); - assert_eq!(spec.listed, vec!["jev".to_string()]); - assert_eq!(spec.claims, snapshot_spec(&[0]).claims); - - let empty = read_settings(b"").expect("an empty blob reads"); - assert!(generation_spec(&routed(&empty)).listed.is_empty()); -} - -#[test] -fn the_need_is_the_tails() { - assert_eq!(NEEDS.len(), tail::NEEDS.len()); - assert_eq!(STATEMENT.needs_len, NEEDS.len()); - assert_eq!(STATEMENT.sections_len, 1 + tail::SECTIONS_CONSUMED.len()); -} - -/// `validate` reads the blob stage 3g deals (`{decisions:
}`): the section inside it is -/// judged (a typo refused), a blob that writes no `decisions:` is the empty section, and the bare -/// section is not the dealt shape. -#[test] -fn validate_reads_the_dealt_blob_at_its_section() { - assert!(read_dealt(b"").is_ok()); - assert!(read_dealt(br#"{}"#).is_ok()); - assert!(read_dealt(br#"{"decisions":{"models":{"jev":{"provider":"typesafe"}}}}"#).is_ok()); - assert!(read_dealt(br#"{"decisions":{"modles":{}}}"#).is_err()); - assert!(read_dealt(br#"[]"#).is_err()); -} - -/// THE SPEC'S BLOB SHAPE (`BUSBAR-1.6.0.md` section 4): the section under its own verb, `{decisions: -///
}`, is read as the section; so is the bare section; and the wrapped section is judged -/// by the same grammar (a typo inside it is refused, naming the member). -#[test] -fn the_section_under_its_verb_reads_as_the_section() { - let wrapped = read_settings(br#"{"decisions":{"models":{"jev":{"provider":"typesafe"}}}}"#) - .expect("the {section: value} blob reads"); - assert_eq!(wrapped.models.len(), 1); - assert!(read_settings(br#"{"decisions":null}"#) - .expect("an absent section is the empty one") - .models - .is_empty()); - let words = read_settings(br#"{"decisions":{"modles":{}}}"#).expect_err("a typo is refused"); - assert!(words.contains("modles"), "{words}"); - // RED ARM: the verb beside another member is no wrapper; it is an unknown member. - assert!(read_settings(br#"{"decisions":{},"models":{}}"#).is_err()); -} diff --git a/crates/busbar-plane-decisions/src/tests/records.rs b/crates/busbar-plane-decisions/src/tests/records.rs deleted file mode 100644 index d8b67307cd..0000000000 --- a/crates/busbar-plane-decisions/src/tests/records.rs +++ /dev/null @@ -1,10 +0,0 @@ -use super::*; - -#[test] -fn no_record_schemas_are_declared() { - assert!( - RECORD_SCHEMAS.is_empty(), - "jev is stateless passthrough (see the module note); a schema here would need a route.rs \ - leg and a documented reason" - ); -} diff --git a/crates/busbar-plane-decisions/tests/alloc_gate.rs b/crates/busbar-plane-decisions/tests/alloc_gate.rs deleted file mode 100644 index 461d2b55a6..0000000000 --- a/crates/busbar-plane-decisions/tests/alloc_gate.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! What relaying an unchanged jev body is allowed to allocate. -//! -//! jev's `encode_egress` forwards the caller's body UNCHANGED — there is no task-identifier rewrite -//! the way A2A's relay has, because `systemone` mints no busbar-side identifier a later call would -//! need to substitute. So the honest allocation count is smaller than A2A's own pinned baseline: the -//! body is borrowed where it already lives, and the only copy this call makes is the envelope's -//! `content-type` field. - -mod common; - -use busbar_contract::plane::Plane; -use busbar_plane_decisions::{ops, DecisionPlane}; -use common::{sealed_destination, Scaffold}; -use std::alloc::{GlobalAlloc, Layout, System}; -use std::cell::Cell; - -thread_local! { - static ALLOCS: Cell = const { Cell::new(0) }; -} - -struct Counting; - -// SAFETY: every call is forwarded verbatim to the system allocator; the counter is a thread-local -// `Cell` of a plain integer, touched only on the allocating thread, and never reads or writes the -// memory being handed out. -unsafe impl GlobalAlloc for Counting { - unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - let _ = ALLOCS.try_with(|c| c.set(c.get() + 1)); - unsafe { System.alloc(layout) } - } - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - unsafe { System.dealloc(ptr, layout) } - } - unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { - let _ = ALLOCS.try_with(|c| c.set(c.get() + 1)); - unsafe { System.realloc(ptr, layout, new_size) } - } -} - -#[global_allocator] -static ALLOC: Counting = Counting; - -fn allocations_of(f: impl FnOnce()) -> u64 { - let before = ALLOCS.with(Cell::get); - f(); - ALLOCS.with(Cell::get) - before -} - -/// One well-formed `systemone` request body. -const BODY: &[u8] = br#"{"state":{"a":1},"context":{}}"#; - -/// COMMITTED BASELINE — the exact allocation count of ONE `encode_egress`. -/// -/// Two: the envelope's `content-type` field (the one arena copy this call genuinely needs) and the -/// envelope's own field list, which starts empty and takes its buffer on the first push — the same -/// two allocations `busbar-plane-a2a`'s own pinned baseline counts for the identical reason. The -/// request body itself is BORROWED where it already lives (`ScratchBytes::new`, not -/// `arena.alloc_bytes`), so relaying it costs nothing — jev has no A2A-style rewrite to spend a -/// second copy on. -const RELAY_ALLOCS: u64 = 2; - -#[test] -fn an_unchanged_body_is_forwarded_without_a_copy() { - let plane = DecisionPlane::EMPTY; - let scaffold = Scaffold::new("http"); - let ctx = scaffold.ctx(); - let seal = common::TestSeal; - let unit = busbar_contract::unit::Unit::new( - &seal, - busbar_contract::UnitKey::new(1), - busbar_contract::unit::Origin::Client, - None, - None, - busbar_contract::wire::Direction::Inbound, - Some(common::principal()), - ops::OP_SYSTEMONE, - busbar_contract::bounded::Ir::new(BODY, &[]), - busbar_contract::bounded::Facts::new(), - None, - ); - let dest = sealed_destination(); - - // One warm call outside the window, so nothing a first call sets up for the process is charged - // to the measured one. - let warm = plane - .encode_egress(&unit, &dest, None, &ctx) - .expect("an upstream destination is one this plane expresses a hop for"); - assert_eq!(warm.body.as_slice(), BODY); - - let mut measured = None; - let count = allocations_of(|| { - measured = Some( - plane - .encode_egress(&unit, &dest, None, &ctx) - .expect("an upstream destination is one this plane expresses a hop for"), - ); - }); - println!("relay allocations: {count}"); - assert_eq!(measured.expect("the call ran").body.as_slice(), BODY); - assert_eq!( - count, RELAY_ALLOCS, - "the relay allocated {count} times, not {RELAY_ALLOCS}" - ); -} diff --git a/crates/busbar-plane-decisions/tests/byte_fixtures.rs b/crates/busbar-plane-decisions/tests/byte_fixtures.rs deleted file mode 100644 index 027aa97610..0000000000 --- a/crates/busbar-plane-decisions/tests/byte_fixtures.rs +++ /dev/null @@ -1,152 +0,0 @@ -//! The plane, driven over three byte fixtures this crate authors itself. -//! -//! jev has no public conformance battery the way MCP/A2A do (no third-party suite exists for a -//! provider-scoped decision protocol this workspace does not operate). So these fixtures are -//! authored here, against the shapes `JEV-DESIGN-SIGNED-v4.md` and the ruling describe: a -//! `systemone` success carrying a usage count, a `systemone` 422 whose body echoes the caller's -//! `state` and the provider's `answers` (the PII witness case — see `pii_witness` below and -//! `tests/jev.rs`), and a `models` list. Each is driven through `decode_ingress`/`decode_response` -//! and the operation class + correlation/finish it produces is pinned; the encoder is pinned -//! byte-for-byte against the same fixture, because byte-identity passthrough IS the dialect. - -mod common; - -use busbar_contract::bounded::FactValue; -use busbar_contract::plane::{Ingress, Plane, Progress, Response}; -use busbar_contract::unit::FinishClass; -use busbar_contract::wire::FrameCursor; -use busbar_plane_decisions::{facts, ops, DecisionPlane}; -use common::{frame, response_frame, Scaffold}; - -/// `POST /v1/systemone` — a successful decision, billing 42 units. -const SYSTEMONE_SUCCESS: &[u8] = - br#"{"request_id":"req_1","usage":{"units":42},"answers":{"decision":"approve"}}"#; - -/// `POST /v1/systemone` — a 422, echoing the caller's `state` and the provider's `answers`. The -/// PII witness fixture: `tests/jev.rs::pii_witness` drives this and asserts neither value ever -/// reaches a fact this plane emits. -const SYSTEMONE_422: &[u8] = br#"{"error":{"code":"invalid_state","message":"bad transition"},"state":{"session":"caller-secret-state-xyz"},"answers":{"leaked":"should-never-surface"}}"#; - -/// `GET /v1/models` — the provider's own catalogue, relayed as-is. -const MODELS_LIST: &[u8] = - br#"{"request_id":"req_2","data":[{"id":"jev-1.13.0","object":"model"}]}"#; - -/// The request body a `systemone` call carries — this plane reads it at its top-level `model` alone -/// (see `codec::REQUEST_PTRS`), and names none here, so its shape only matters for byte-identity -/// forwarding. -const SYSTEMONE_REQUEST: &[u8] = br#"{"state":{"session":"caller-secret-state-xyz"},"context":{}}"#; - -#[test] -fn decode_ingress_names_systemone_and_waits_for_its_body() { - let plane = DecisionPlane::EMPTY; - let scaffold = Scaffold::new("http") - .with_method("POST") - .on_path("/v1/systemone"); - let ctx = scaffold.ctx(); - let frames = vec![frame(SYSTEMONE_REQUEST)]; - let mut cursor = FrameCursor::new(&frames); - let Ok(Ingress::OneShot(draft)) = plane.decode_ingress(&mut cursor, None, &ctx) else { - panic!("a systemone POST with a body decodes as one shot"); - }; - assert_eq!(draft.op, ops::OP_SYSTEMONE); - assert_eq!( - draft.facts.get(facts::FACT_OP), - Some(FactValue::Str("systemone")) - ); - // Byte-identity: the whole request body is carried, untouched. - assert_eq!(draft.body_ir.body(), SYSTEMONE_REQUEST); -} - -#[test] -fn decode_ingress_names_models_with_no_body_to_wait_for() { - let plane = DecisionPlane::EMPTY; - let scaffold = Scaffold::new("http") - .with_method("GET") - .on_path("/v1/models"); - let ctx = scaffold.ctx(); - // No frame at all — a GET carries no body, and `models` must not wait for one. - let frames: Vec = Vec::new(); - let mut cursor = FrameCursor::new(&frames); - let Ok(Ingress::OneShot(draft)) = plane.decode_ingress(&mut cursor, None, &ctx) else { - panic!("a bodyless GET /v1/models decodes as one shot with no frame read"); - }; - assert_eq!(draft.op, ops::OP_MODELS); -} - -#[test] -fn decode_ingress_refuses_an_unclaimed_surface() { - let plane = DecisionPlane::EMPTY; - let scaffold = Scaffold::new("http") - .with_method("POST") - .on_path("/v1/unknown"); - let ctx = scaffold.ctx(); - let frames = vec![frame(b"{}")]; - let mut cursor = FrameCursor::new(&frames); - assert!(plane.decode_ingress(&mut cursor, None, &ctx).is_err()); -} - -#[test] -fn decode_response_reads_success_as_complete_with_usage() { - let plane = DecisionPlane::EMPTY; - let scaffold = Scaffold::new("http"); - let ctx = scaffold.ctx(); - let frames = vec![response_frame(SYSTEMONE_SUCCESS)]; - let mut cursor = FrameCursor::new(&frames); - let Ok(Progress::Terminal { r, .. }) = - plane.decode_response(&mut cursor, &common::sealed_destination(), None, &ctx) - else { - panic!("a whole systemone answer decodes as terminal"); - }; - assert_eq!(r.finish, FinishClass::Complete); - assert_eq!( - r.facts.get(facts::FACT_HAS_ERROR), - Some(FactValue::Bool(false)) - ); - assert_eq!( - r.facts.get(facts::FACT_USAGE_UNITS), - Some(FactValue::Int(42)) - ); - assert_eq!(r.ir.body(), SYSTEMONE_SUCCESS); -} - -#[test] -fn decode_response_reads_422_as_error_with_no_usage() { - let plane = DecisionPlane::EMPTY; - let scaffold = Scaffold::new("http"); - let ctx = scaffold.ctx(); - let frames = vec![response_frame(SYSTEMONE_422)]; - let mut cursor = FrameCursor::new(&frames); - let Ok(Progress::Terminal { r, .. }) = - plane.decode_response(&mut cursor, &common::sealed_destination(), None, &ctx) - else { - panic!("a whole 422 answer decodes as terminal"); - }; - assert_eq!(r.finish, FinishClass::Error); - assert_eq!( - r.facts.get(facts::FACT_HAS_ERROR), - Some(FactValue::Bool(true)) - ); - assert_eq!(r.facts.get(facts::FACT_USAGE_UNITS), None); -} - -#[test] -fn encode_response_is_byte_identical_to_the_fixture() { - let plane = DecisionPlane::EMPTY; - for fixture in [SYSTEMONE_SUCCESS, SYSTEMONE_422, MODELS_LIST] { - let scaffold = Scaffold::new("http"); - let ctx = scaffold.ctx(); - let r = Response { - ir: busbar_contract::bounded::Ir::new(fixture, &[]), - finish: FinishClass::Complete, - facts: busbar_contract::bounded::Facts::new(), - }; - let out = plane - .encode_response(&r, None, &ctx) - .expect("every fixture re-encodes"); - assert_eq!( - out.as_slice(), - fixture, - "encode_response is not byte-identical" - ); - } -} diff --git a/crates/busbar-plane-decisions/tests/common/mod.rs b/crates/busbar-plane-decisions/tests/common/mod.rs deleted file mode 100644 index bca82c06ab..0000000000 --- a/crates/busbar-plane-decisions/tests/common/mod.rs +++ /dev/null @@ -1,288 +0,0 @@ -//! The scaffolding one plane call needs, and nothing more. -//! -//! A plane is handed a context carrying one resource — the arena — and a handful of borrowed -//! read-only views. Everything below is the smallest honest stand-in for each, mirrored from -//! `busbar-plane-a2a`'s own `tests/common/mod.rs`: an arena that hands out bytes, views that answer -//! what they were told to answer, and a seal that lets a test build the kernel-owned values the -//! loop would otherwise build. - -#![allow(dead_code)] - -use busbar_contract::bounded::{ - Labels, PlaneAlloc, PlaneAllocBudget, ScratchBytes, SlabBytes, Span, -}; -use busbar_contract::ids::{PrincipalId, SessionId}; -use busbar_contract::unit::{Clock, ConfigView, Ctx, SessionView, TransportView}; -use busbar_contract::wire::{Direction, Frame, FrameMeta}; -use std::sync::atomic::{AtomicUsize, Ordering}; - -/// An arena that hands out bytes and counts what it handed out. -pub struct TestPlaneAlloc { - used: AtomicUsize, - ceiling: usize, -} - -impl TestPlaneAlloc { - pub fn new() -> Self { - Self { - used: AtomicUsize::new(0), - ceiling: busbar_contract::bounded::SCRATCH_BASE_BYTES, - } - } -} - -impl Default for TestPlaneAlloc { - fn default() -> Self { - Self::new() - } -} - -impl PlaneAlloc for TestPlaneAlloc { - fn alloc_bytes<'a>(&'a self, src: &[u8]) -> Result, PlaneAllocBudget> { - let remaining = self - .ceiling - .saturating_sub(self.used.load(Ordering::Relaxed)); - if src.len() > remaining { - return Err(PlaneAllocBudget { - wanted: src.len(), - remaining, - }); - } - self.used.fetch_add(src.len(), Ordering::Relaxed); - let leaked: &'static [u8] = Box::leak(src.to_vec().into_boxed_slice()); - Ok(ScratchBytes::new(leaked)) - } - - fn alloc_str<'a>(&'a self, src: &str) -> Result<&'a str, PlaneAllocBudget> { - let remaining = self - .ceiling - .saturating_sub(self.used.load(Ordering::Relaxed)); - if src.len() > remaining { - return Err(PlaneAllocBudget { - wanted: src.len(), - remaining, - }); - } - self.used.fetch_add(src.len(), Ordering::Relaxed); - let leaked: &'static str = Box::leak(src.to_string().into_boxed_str()); - Ok(leaked) - } - - fn alloc_spans<'a>( - &'a self, - src: &[(&'a str, Span)], - ) -> Result<&'a [(&'a str, Span)], PlaneAllocBudget> { - let wanted = std::mem::size_of_val(src); - let remaining = self - .ceiling - .saturating_sub(self.used.load(Ordering::Relaxed)); - if wanted > remaining { - return Err(PlaneAllocBudget { wanted, remaining }); - } - self.used.fetch_add(wanted, Ordering::Relaxed); - Ok(Box::leak(src.to_vec().into_boxed_slice())) - } - - fn remaining(&self) -> usize { - self.ceiling - .saturating_sub(self.used.load(Ordering::Relaxed)) - } -} - -/// A configuration block with nothing in it. -pub struct EmptyConfig; - -impl ConfigView for EmptyConfig { - fn get_str(&self, _key: &str) -> Option<&str> { - None - } - fn get_int(&self, _key: &str) -> Option { - None - } - fn get_bool(&self, _key: &str) -> Option { - None - } -} - -/// A transport that answers with the path/verb it was given. -pub struct TestTransport { - pub key: &'static str, - pub chain: Vec<&'static str>, - pub path: Option, - pub method: Option, -} - -impl TestTransport { - pub fn new(key: &'static str) -> Self { - Self { - key, - chain: vec![key], - path: None, - method: None, - } - } -} - -impl TransportView for TestTransport { - fn key(&self) -> &'static str { - self.key - } - fn chain(&self) -> &[&'static str] { - &self.chain - } - fn fact(&self, key: &str) -> Option<&str> { - if key == busbar_contract::transport::facts::PATH { - return self.path.as_deref(); - } - if key == busbar_contract::transport::facts::METHOD { - return self.method.as_deref(); - } - None - } -} - -/// A session that answers what it was told to answer. -pub struct TestSession { - pub id: SessionId, - pub bound: bool, - pub facts: Vec<(&'static str, String)>, -} - -impl TestSession { - pub fn new() -> Self { - Self { - id: SessionId(1), - bound: false, - facts: Vec::new(), - } - } -} - -impl Default for TestSession { - fn default() -> Self { - Self::new() - } -} - -impl SessionView for TestSession { - fn id(&self) -> SessionId { - self.id - } - fn is_bound(&self) -> bool { - self.bound - } - fn session_fact(&self, key: &str) -> Option<&str> { - self.facts - .iter() - .find(|(k, _)| *k == key) - .map(|(_, v)| v.as_str()) - } - fn transport_fact(&self, _key: &str) -> Option<&str> { - None - } - fn upstream_count(&self) -> usize { - 0 - } -} - -/// The blessed TEST seal (#65). `KernelSeal` is SEALED — no crate outside `busbar-contract` can -/// implement it — so a fixture names the contract's own `test-seal` type instead of forging one. -/// The type system stops a plugin now, not the manifest allow-list alone. -pub use busbar_contract::plugin::TestKernelSeal as TestSeal; - -/// A clock frozen at a readable instant. -pub const CLOCK: Clock = Clock { - unix_secs: 1_700_000_000, - monotonic_nanos: 0, -}; - -/// One inbound frame carrying a document. -pub fn frame(bytes: &[u8]) -> Frame { - Frame { - direction: Direction::Inbound, - stream: busbar_contract::ids::StreamId(0), - bytes: SlabBytes::new(std::sync::Arc::from(bytes.to_vec().into_boxed_slice())), - meta: FrameMeta { - bytes: bytes.len() as u64, - transport_units: None, - status: None, - status_code: None, - retry_after_secs: None, - text: false, - }, - } -} - -/// One outbound frame carrying a document. -pub fn response_frame(bytes: &[u8]) -> Frame { - Frame { - direction: Direction::Outbound, - ..frame(bytes) - } -} - -/// Everything a context borrows, held together so a test can build one. -pub struct Scaffold { - pub arena: TestPlaneAlloc, - pub config: EmptyConfig, - pub transport: TestTransport, - pub session: TestSession, - pub labels: Labels<'static>, -} - -impl Scaffold { - pub fn new(transport: &'static str) -> Self { - Self { - arena: TestPlaneAlloc::new(), - config: EmptyConfig, - transport: TestTransport::new(transport), - session: TestSession::new(), - labels: Labels::new(), - } - } - - pub fn ctx(&self) -> Ctx<'_> { - Ctx::new( - CLOCK, - &self.config, - Some(&self.session), - &self.transport, - &self.labels, - &self.arena, - ) - } - - /// The same scaffold, over a stack that saw this request verb. - #[must_use] - pub fn with_method(mut self, method: &str) -> Self { - self.transport.method = Some(method.to_string()); - self - } - - /// The same scaffold, over a stack that saw this request target. - #[must_use] - pub fn on_path(mut self, path: &str) -> Self { - self.transport.path = Some(path.to_string()); - self - } -} - -/// A principal, for the units a test builds. -pub fn principal() -> PrincipalId { - PrincipalId::new("test-principal") -} - -/// A sealed upstream destination, for the calls that take one. -pub fn sealed_destination() -> busbar_contract::dest::VerifiedDestination { - let seal = TestSeal; - busbar_contract::dest::VerifiedDestination::seal( - &seal, - busbar_contract::dest::DestinationFacts::Upstream { - transport: "http", - address: busbar_contract::UpstreamAddress::socket("api.typesafe.ai"), - lane: busbar_contract::ids::LaneId::new("standard"), - }, - "http", - None, - ) -} diff --git a/crates/busbar-plane-decisions/tests/conformance.json b/crates/busbar-plane-decisions/tests/conformance.json deleted file mode 100644 index bf72d3c51a..0000000000 --- a/crates/busbar-plane-decisions/tests/conformance.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "settings": { "models": { "jev": { "provider": "typesafe" } } }, - "plane": { - "bad_settings": [{ "modles": {} }], - "refresh_settings": { - "models": { "a": { "provider": "typesafe" }, "b": { "provider": "typesafe" } } - }, - "open_claims": [["POST", "/v1/systemone"]], - "refresh_claims": [["POST", "/v1/systemone"]], - "member": "jev", - "claimed": { "unit": 7, "method": "POST", "target": "/v1/systemone" }, - "unclaimed": { "unit": 8, "method": "GET", "target": "/v1/models", "status": 404 }, - "request": "{\"state\":{\"session\":\"s\"},\"context\":{}}", - "request_out": "{\"state\":{\"session\":\"s\"},\"context\":{}}", - "far_end": { - "status": 200, - "fields": [["content-type", "application/json"]], - "answer": "{\"request_id\":\"req_1\",\"usage\":{\"units\":42},\"answers\":{}}", - "answer_out": "{\"request_id\":\"req_1\",\"usage\":{\"units\":42},\"answers\":{}}", - "units": [[0, 42], [1, 1]] - }, - "narrow": { "unit": 9, "reply_cap": 16, "more": 3 }, - "short": { "unit": 11 }, - "empty": { "unit": 10, "outcome": "Refused" }, - "refusal": { - "status": 403, - "text": "denied", - "reply": "{\"error\":{\"code\":\"unsupported_operation\",\"message\":\"denied\"}}" - } - } -} diff --git a/crates/busbar-plane-decisions/tests/conformance.rs b/crates/busbar-plane-decisions/tests/conformance.rs deleted file mode 100644 index 80ab3a6a3a..0000000000 --- a/crates/busbar-plane-decisions/tests/conformance.rs +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE PUBLISHED CONFORMANCE SUITE, run over this plane (TODO ABI-b4; OWNER 2026-10-03: plugins -//! test themselves against busbar's published suite). The linked door (`plane_door::door`) and -//! this crate's dropped-in image (the `decisions_door` example `cdylib`, which `cargo test --test -//! conformance` does not build: build it first with `cargo build -p busbar-plane-decisions -//! --example decisions_door`), each through the one loader, driven by busbar-plugin-loader's plane -//! script over `conformance.json`, every step at its pinned crossing count, the two folds equal. -//! This is the plugin's own both-ways witness at the path every plugin repo keeps it -//! (`tests/conformance.rs`, the file kind-isolation's witness grant names), so the plane repo -//! (GetBusbar/busbar-plane-decisions) takes it as it is once the plane is extracted. - -busbar_plugin_loader::conformance_suite! { - door: busbar_plane_decisions::plane_door::door, - cdylib: "decisions_door", - inputs: include_str!("conformance.json"), -} diff --git a/crates/busbar-plane-decisions/tests/door.rs b/crates/busbar-plane-decisions/tests/door.rs deleted file mode 100644 index f38bfd2135..0000000000 --- a/crates/busbar-plane-decisions/tests/door.rs +++ /dev/null @@ -1,367 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE DOOR'S ANSWERS, DRIVEN OVER ITS ABI as the kernel's plane driver drives them. -//! -//! * MODEL RESOLUTION (THE DESIGN section 2, the decisions bullet; DECISIONS D8b): "a request routes -//! by its model; one configured model and none named is the default (byte-identical); more than -//! one and none named is 400; an unknown model is 404." The claim is published whenever a model is -//! configured. -//! * `upstream_model` (the same bullet): "rewrites only the top-level model value, by span splice, -//! when it is set and differs; otherwise the body passes through." - -#![allow(unsafe_code)] - -mod door_harness; - -use busbar_contract::abi::mechanism::call::Outcome; -use busbar_contract::abi::plane::ROUTE_DIRECT; -use door_harness::{open, Bufs, Given}; - -const SYSTEMONE: (&str, &str) = ("POST", "/v1/systemone"); - -const TWO: &str = r#"{"models":{"a":{"provider":"typesafe"},"b":{"provider":"typesafe"}}}"#; -const ONE: &str = r#"{"models":{"jev":{"provider":"typesafe"}}}"#; - -fn claims(c: &[(&str, &str)]) -> Vec<(String, String)> { - c.iter() - .map(|(v, t)| ((*v).to_string(), (*t).to_string())) - .collect() -} - -/// RED (audit plane-decisions#1): several configured models publish the claim; none publishes none. -#[test] -fn every_generation_with_a_model_claims_systemone() { - assert_eq!(open(TWO).claims, claims(&[SYSTEMONE]), "two models"); - assert_eq!(open(ONE).claims, claims(&[SYSTEMONE]), "one model"); - let none = open(r#"{"models":{}}"#); - assert!(none.claims.is_empty(), "no model claims nothing"); - // A refresh onto several models keeps the claim, and onto none drops it. - assert_eq!(none.refresh(TWO, 2), claims(&[SYSTEMONE])); - assert!(none.refresh("{}", 3).is_empty()); -} - -/// RED (audit plane-decisions#1): a request routes over the model it names; several configured and -/// none named is 400; a name no model has is 404; one configured and none named is the default. -#[test] -fn a_request_routes_by_its_model() { - let two = open(TWO); - let b = two.arrive(1, br#"{"model":"b","state":{}}"#); - assert_eq!( - (b.outcome, b.route, b.entry.as_str()), - (Outcome::Ready, ROUTE_DIRECT, "b"), - "{b:?}" - ); - let a = two.arrive(2, br#"{"state":{},"model":"a"}"#); - assert_eq!( - (a.outcome, a.entry.as_str()), - (Outcome::Ready, "a"), - "{a:?}" - ); - - let unnamed = two.arrive(3, br#"{"state":{}}"#); - assert_eq!( - (unnamed.outcome, unnamed.status), - (Outcome::Refused, 400), - "{unnamed:?}" - ); - assert!( - unnamed.refusal != 0 && !unnamed.text.is_empty(), - "{unnamed:?}" - ); - - let unknown = two.arrive(4, br#"{"model":"zz","state":{}}"#); - assert_eq!( - (unknown.outcome, unknown.status), - (Outcome::Refused, 404), - "{unknown:?}" - ); - assert!(unknown.text.contains("'zz'"), "{unknown:?}"); - - let not_a_name = two.arrive(5, br#"{"model":7}"#); - assert_eq!( - (not_a_name.outcome, not_a_name.status), - (Outcome::Refused, 400), - "{not_a_name:?}" - ); - - let one = open(ONE); - let default = one.arrive(6, br#"{"state":{}}"#); - assert_eq!( - (default.outcome, default.route, default.entry.as_str()), - (Outcome::Ready, ROUTE_DIRECT, "jev"), - "{default:?}" - ); - let named = one.arrive(7, br#"{"model":"jev"}"#); - assert_eq!( - (named.outcome, named.entry.as_str()), - (Outcome::Ready, "jev") - ); - let elsewhere = one.arrive(8, br#"{"model":"other"}"#); - assert_eq!( - (elsewhere.outcome, elsewhere.status), - (Outcome::Refused, 404), - "{elsewhere:?}" - ); -} - -/// A refused arrival is answered in jev's error shape, the arrival's words and status. -#[test] -fn an_unrouted_arrival_renders_in_the_dialect_shape() { - let two = open(TWO); - let unknown = two.arrive(1, br#"{"model":"zz"}"#); - let (outcome, body) = two.refusal(1, unknown.status, &unknown.text, true); - assert_eq!(outcome, Outcome::Ready); - let v: serde_json::Value = serde_json::from_slice(&body).expect("JSON"); - assert_eq!(v["error"]["message"], unknown.text.as_str(), "{v}"); - assert!(v["error"]["code"].is_string(), "{v}"); -} - -/// The caller's body as the far end receives it, for a unit routed over `settings` by `body`. -fn sent(settings: &str, body: &[u8]) -> String { - let door = open(settings); - let arrived = door.arrive(1, body); - assert_eq!(arrived.outcome, Outcome::Ready, "{arrived:?}"); - let mut bufs = Bufs::new(1 << 16); - let attempt = door.piece(1, Given::attempt(1), &mut bufs); - assert_eq!(attempt.request, "POST /v1/systemone"); - let caller = door.piece(1, Given::caller(body), &mut bufs); - assert!(caller.far, "the caller's body goes to the far end"); - String::from_utf8(caller.bytes).expect("UTF-8") -} - -/// RED (audit plane-decisions#2): a set `upstream_model` that differs replaces the top-level model -/// value alone; every other byte (member order, whitespace) is the caller's. -#[test] -fn upstream_model_is_spliced_into_the_top_level_model() { - let upstream = r#"{"models":{"jev":{"provider":"typesafe","upstream_model":"jev-1.13.0"}}}"#; - let body = "{ \"state\" : {\"model\":\"inner\"},\n \"model\" : \"jev\" , \"n\":[1, 2] }"; - assert_eq!( - sent(upstream, body.as_bytes()), - "{ \"state\" : {\"model\":\"inner\"},\n \"model\" : \"jev-1.13.0\" , \"n\":[1, 2] }" - ); - // The same name, or none set, or a body that names no model: the body passes through. - let same = r#"{"models":{"jev":{"provider":"typesafe","upstream_model":"jev"}}}"#; - assert_eq!(sent(same, body.as_bytes()), body); - assert_eq!(sent(ONE, body.as_bytes()), body); - let unnamed = "{\"state\":{}}"; - assert_eq!(sent(upstream, unnamed.as_bytes()), unnamed); -} - -/// One unit served over [`ONE`]: its ATTEMPT, the caller's body, and the far end's whole answer, -/// `status` with `answer`. Answers the far end's piece. -fn answered(status: u32, answer: &[u8]) -> door_harness::Answered { - let door = open(ONE); - assert_eq!(door.arrive(1, br#"{"state":{}}"#).outcome, Outcome::Ready); - let mut bufs = Bufs::new(1 << 16); - door.piece(1, Given::attempt(1), &mut bufs); - door.piece(1, Given::caller(br#"{"state":{}}"#), &mut bufs); - door.piece(1, Given::whole(status, answer), &mut bufs) -} - -/// The class index the tail names `name` at. -fn class(name: &str) -> u32 { - let tail = busbar_plane_decisions::plane_door::TAIL; - // SAFETY: the tail's billable classes are a `'static` list of `billable_classes_len` entries, - // each naming `'static` strings. - let classes = - unsafe { std::slice::from_raw_parts(tail.billable_classes, tail.billable_classes_len) }; - let named = |c: &busbar_contract::abi::plane::BillableClass| { - // SAFETY: as above. - unsafe { std::slice::from_raw_parts(c.class.ptr, c.class.len) == name.as_bytes() } - }; - let at = classes - .iter() - .position(named) - .unwrap_or_else(|| panic!("the tail declares no class `{name}`")); - u32::try_from(at).unwrap() -} - -/// `(class, amount)` of every count an answer reported, each REPORTED. -fn counts(a: &door_harness::Answered) -> Vec<(u32, u64)> { - assert!( - a.units - .iter() - .all(|(_, source, _)| *source == busbar_contract::abi::plane::UNITS_REPORTED), - "{a:?}" - ); - a.units.iter().map(|(c, _, n)| (*c, *n)).collect() -} - -/// RED (audit plane-decisions#4): THE DESIGN section 7, "bills one `decision` per successful, settled -/// answer from every provider": a success with no count bills one decision, beside its fee. -#[test] -fn a_success_with_no_count_bills_one_decision() { - let fee = class(busbar_contract::plane::PER_REQUEST); - let a = answered(200, br#"{"answers":{}}"#); - assert_eq!(a.outcome, Some(Outcome::Ready), "{a:?}"); - assert_eq!(counts(&a), vec![(class("decision"), 1), (fee, 1)]); - let reported = answered(200, br#"{"usage":{"units":42}}"#); - assert_eq!(counts(&reported), vec![(class("decision"), 42), (fee, 1)]); -} - -/// RED (audit plane-decisions#4): one success rule: a non-2xx answer bills nothing, whatever count -/// it carries. -#[test] -fn a_non_success_bills_nothing() { - let a = answered(500, br#"{"usage":{"units":2}}"#); - assert_eq!(a.outcome, Some(Outcome::Ready), "{a:?}"); - assert!(a.units.is_empty(), "{a:?}"); - assert_eq!(a.bytes, br#"{"usage":{"units":2}}"#, "relayed unchanged"); -} - -/// RED (audit plane-decisions#4): a success whose count is not a whole count fails the unit loudly -/// (finding 395, kept: "LOCATED, never dropped"), never a silent zero. -#[test] -fn a_success_with_an_unreadable_count_fails_loudly() { - for answer in [ - &br#"{"usage":{"units":7.5}}"#[..], - br#"{"usage":{"units":"7"}}"#, - br#"{"usage":{"units":-3}}"#, - ] { - let a = answered(200, answer); - assert_eq!( - (a.outcome, a.short), - (Some(Outcome::Failed), false), - "{}: {a:?}", - String::from_utf8_lossy(answer) - ); - assert!(a.units.is_empty() && a.bytes.is_empty(), "{a:?}"); - } -} - -/// RED (audit plane-decisions#5): THE DESIGN section 7, "its card prices `decision`, `input_tokens` -/// and `output_tokens` (family `decision`, all three required, 0 = free)": the tail declares the -/// three in family `decision`, and a success's reported tokens are billed in them. -#[test] -fn the_tokens_a_success_reports_are_billed_in_family_decision() { - let tail = busbar_plane_decisions::plane_door::TAIL; - // SAFETY: as in `class`. - let classes = - unsafe { std::slice::from_raw_parts(tail.billable_classes, tail.billable_classes_len) }; - let family = |name: &str| { - let c = &classes[class(name) as usize]; - // SAFETY: as in `class`. - String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(c.family.ptr, c.family.len) }) - .into_owned() - }; - for name in ["decision", "input_tokens", "output_tokens"] { - assert_eq!(family(name), "decision", "{name}"); - } - let a = answered( - 200, - br#"{"usage":{"units":2,"input_tokens":12,"output_tokens":5}}"#, - ); - let got = counts(&a); - for want in [ - (class("decision"), 2), - (class("input_tokens"), 12), - (class("output_tokens"), 5), - ] { - assert!(got.contains(&want), "{want:?} in {got:?}"); - } -} - -const MAX_UNITS: u64 = busbar_plane_decisions::plane_door::MAX_UNITS as u64; - -/// A live unit over [`ONE`]: arrived, its ATTEMPT crossed. -fn live(door: &door_harness::Door, unit: u64, bufs: &mut Bufs) { - let arrived = door.arrive(unit, br#"{"state":{}}"#); - assert_eq!(arrived.outcome, Outcome::Ready, "unit {unit}: {arrived:?}"); - let attempt = door.piece(unit, Given::attempt(1), bufs); - assert_eq!(attempt.request, "POST /v1/systemone", "unit {unit}"); -} - -/// Whether `unit`'s body still reaches the far end: its ATTEMPT's state is still held. -fn still_live(door: &door_harness::Door, unit: u64, bufs: &mut Bufs) -> bool { - door.piece(unit, Given::caller(br#"{"state":{}}"#), bufs) - .far -} - -/// RED (audit plane-decisions#6): a refusal ends its unit and drops its state, so refused traffic -/// never crowds out a live unit. -#[test] -fn a_refused_unit_is_dropped_and_a_live_unit_kept() { - let door = open(ONE); - let mut bufs = Bufs::new(1 << 16); - live(&door, 1, &mut bufs); - for unit in 2..2 + MAX_UNITS { - assert_eq!( - door.arrive(unit, br#"{}"#).outcome, - Outcome::Ready, - "unit {unit}" - ); - door.refusal(unit, 429, "the key's limit is spent", false); - } - assert!(still_live(&door, 1, &mut bufs), "the live unit was dropped"); -} - -/// RED (audit plane-decisions#6): a cancel ends the unit on its ticket and drops its state. -#[test] -fn a_cancelled_unit_is_dropped_and_a_live_unit_kept() { - let door = open(ONE); - let mut bufs = Bufs::new(1 << 16); - live(&door, 1, &mut bufs); - for unit in 2..2 + MAX_UNITS { - live(&door, unit, &mut bufs); - let (outcome, disposition) = door.cancel(door_harness::ticket_of(unit)); - assert_eq!( - (outcome, disposition), - (Outcome::Ready, busbar_contract::abi::plane::CANCEL_ABORTED), - "unit {unit}" - ); - } - assert!(still_live(&door, 1, &mut bufs), "the live unit was dropped"); -} - -/// RED (audit plane-decisions#6): a unit cut after its far end answered was not aborted before it. -#[test] -fn a_unit_cancelled_after_its_far_end_answered_is_cut_partway() { - let door = open(ONE); - let mut bufs = Bufs::new(1 << 16); - live(&door, 1, &mut bufs); - assert!(still_live(&door, 1, &mut bufs)); - let first = door.piece(1, Given::far(Some(200), false, br#"{"usage":"#), &mut bufs); - assert_eq!(first.outcome, Some(Outcome::Ready)); - assert_eq!( - door.cancel(door_harness::ticket_of(1)), - ( - Outcome::Ready, - busbar_contract::abi::plane::CANCEL_OK_PARTIAL - ) - ); -} - -/// RED (audit plane-decisions#6): past [`MAX_UNITS`] live units a new arrival is refused, about -/// the entry it routed over, and no live unit is dropped to make room. -#[test] -fn past_the_cap_an_arrival_is_refused_and_no_live_unit_dropped() { - let door = open(ONE); - let mut bufs = Bufs::new(1 << 16); - for unit in 1..=MAX_UNITS { - live(&door, unit, &mut bufs); - } - let over = door.arrive(MAX_UNITS + 1, br#"{"state":{}}"#); - assert_eq!( - (over.outcome, over.status, over.route, over.entry.as_str()), - (Outcome::Refused, 503, ROUTE_DIRECT, "jev"), - "{over:?}" - ); - assert!( - still_live(&door, 1, &mut bufs), - "the first live unit was dropped" - ); - assert!(still_live(&door, MAX_UNITS, &mut bufs)); -} - -/// A unit that arrived and never crossed a piece while [`MAX_UNITS`] arrivals followed is -/// reclaimed, so the instance never refuses every arrival for good. -#[test] -fn an_arrival_never_started_is_reclaimed_once_the_cap_is_reached() { - let door = open(ONE); - for unit in 1..=MAX_UNITS { - assert_eq!(door.arrive(unit, br#"{}"#).outcome, Outcome::Ready); - } - let next = door.arrive(MAX_UNITS + 1, br#"{}"#); - assert_eq!(next.outcome, Outcome::Ready, "{next:?}"); -} diff --git a/crates/busbar-plane-decisions/tests/door_alloc.rs b/crates/busbar-plane-decisions/tests/door_alloc.rs deleted file mode 100644 index 4570d98400..0000000000 --- a/crates/busbar-plane-decisions/tests/door_alloc.rs +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! WHAT RELAYING A FAR-END ANSWER ALLOCATES (THE DESIGN section 11.2: "no allocation and no -//! blocking on the hot path"): a piece of the far end's answer is read as it passes and written -//! straight into the host's reply buffer, so relaying piece after piece of an answer allocates -//! nothing, however long the answer runs. - -#![allow(unsafe_code)] - -mod door_harness; - -use std::alloc::{GlobalAlloc, Layout, System}; -use std::cell::Cell; - -use busbar_contract::abi::mechanism::call::Outcome; -use door_harness::{open, Bufs, Given}; - -thread_local! { - static ALLOCS: Cell = const { Cell::new(0) }; -} - -struct Counting; - -// SAFETY: every call is forwarded verbatim to the system allocator; the counter is a thread-local -// `Cell` of a plain integer, touched only on the allocating thread, and never reads or writes the -// memory being handed out. -unsafe impl GlobalAlloc for Counting { - unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - let _ = ALLOCS.try_with(|c| c.set(c.get() + 1)); - // SAFETY: forwarded as given. - unsafe { System.alloc(layout) } - } - unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { - // SAFETY: forwarded as given. - unsafe { System.dealloc(ptr, layout) } - } - unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { - let _ = ALLOCS.try_with(|c| c.set(c.get() + 1)); - // SAFETY: forwarded as given. - unsafe { System.realloc(ptr, layout, new_size) } - } -} - -#[global_allocator] -static ALLOC: Counting = Counting; - -fn allocations_of(f: impl FnOnce()) -> u64 { - let before = ALLOCS.with(Cell::get); - f(); - ALLOCS.with(Cell::get) - before -} - -/// RED (audit plane-decisions#8): 256 pieces of a 1 MiB answer, each 4 KiB, relayed through the -/// door: no allocation (predev kept the whole answer, growing a buffer, and copied each piece -/// again before writing it). -#[test] -fn relaying_an_answer_piece_by_piece_allocates_nothing() { - let door = open(r#"{"models":{"jev":{"provider":"typesafe"}}}"#); - let mut bufs = Bufs::new(1 << 16); - assert_eq!(door.arrive(1, br#"{"state":{}}"#).outcome, Outcome::Ready); - door.piece(1, Given::attempt(1), &mut bufs); - door.piece(1, Given::caller(br#"{"state":{}}"#), &mut bufs); - let head = door.piece( - 1, - Given::far(Some(200), false, br#"{"answers":{"blob":""#), - &mut bufs, - ); - assert_eq!(head.outcome, Some(Outcome::Ready)); - let filler = [b'x'; 4096]; - let piece = Given::far(None, false, &filler); - // Warm: one piece outside the window. - door.piece(1, piece, &mut bufs); - let mut relayed = 0; - let count = allocations_of(|| { - for _ in 0..256 { - relayed += door.piece_bytes(1, piece, &mut bufs); - } - }); - assert_eq!(relayed, 256 * filler.len(), "every byte is relayed"); - assert_eq!(count, 0, "relaying 256 pieces allocated {count} times"); - let last = door.piece( - 1, - Given::far(None, true, br#""},"usage":{"units":4}}"#), - &mut bufs, - ); - assert_eq!(last.units.first().map(|u| u.2), Some(4), "{last:?}"); -} diff --git a/crates/busbar-plane-decisions/tests/door_harness/mod.rs b/crates/busbar-plane-decisions/tests/door_harness/mod.rs deleted file mode 100644 index a99d20c939..0000000000 --- a/crates/busbar-plane-decisions/tests/door_harness/mod.rs +++ /dev/null @@ -1,545 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE DOOR, DRIVEN OVER ITS OWN ABI as the kernel's plane driver drives it: `open`, `refresh`, -//! `arrive`, `on_piece`, `refusal` and `cancel`, each a frame of plain C data crossing the door's -//! table. The decisions door calls no host service, so the host tables it is opened with are empty. - -#![allow(dead_code)] - -use std::os::raw::c_void; - -use busbar_contract::abi::mechanism::call::{ - AbiStr, Blob, Field, InHead, OutHead, Outcome, Span, BLOB_OCTETS, -}; -use busbar_contract::abi::mechanism::lifecycle::{ - slot as life, CancelIn, CancelOut, OpenIn, OpenOut, OpsHead, RefreshIn, ValidateIn, -}; -use busbar_contract::abi::mechanism::ticket::{HostCtx, HostTables, Ticket}; -use busbar_contract::abi::plane::{ - slot, ArriveIn, ArriveOut, OnPieceIn, OnPieceOut, Ops, OutField, PlaneCancelIn, PlaneCancelOut, - PlaneOpenIn, PlaneOpenOut, PlaneRefreshOut, PlaneSnapshot, ProviderFacts, RecordWrite, - RefusalIn, RefusalOut, UnitCount, EMIT_DONE, EMIT_TO_FAR_END, FROM_CALLER, FROM_FAR_END, - FROM_KERNEL, PIECE_HAS_STATUS, PIECE_LAST, REFUSAL_ARRIVE, REFUSAL_KERNEL, -}; -use busbar_plane_decisions::plane_door::door; - -pub fn z() -> T { - // SAFETY: every `in`/`out` here is plain C data; all-zero is a valid value of each. - unsafe { std::mem::zeroed() } -} - -fn str_of(s: &[u8]) -> AbiStr { - AbiStr { - ptr: s.as_ptr(), - len: s.len(), - } -} - -fn octets(b: &[u8]) -> Blob { - Blob { - ptr: b.as_ptr(), - len: b.len(), - fmt: BLOB_OCTETS, - flags: 0, - } -} - -fn read(s: AbiStr) -> String { - if s.ptr.is_null() || s.len == 0 { - return String::new(); - } - // SAFETY: the door's memory, valid until the instance's next call. - String::from_utf8_lossy(unsafe { std::slice::from_raw_parts(s.ptr, s.len) }).into_owned() -} - -fn head(op: u32, ticket: Ticket) -> InHead { - InHead { - size: std::mem::size_of::() as u32, - op, - ticket, - ..z() - } -} - -fn out_head() -> OutHead { - OutHead { - size: std::mem::size_of::() as u32, - ..z() - } -} - -fn ops() -> &'static Ops { - // SAFETY: the door is a `'static` table whose ops are the plane kind's. - unsafe { &*(*door()).ops.cast::() } -} - -fn lifecycle() -> &'static OpsHead { - &ops().head -} - -struct Tables(HostTables); -// SAFETY: the tables name no function and no memory. -unsafe impl Sync for Tables {} - -static TABLES: Tables = Tables(HostTables { - size: std::mem::size_of::() as u32, - _reserved: 0, - ctx: HostCtx { - ptr: std::ptr::null_mut(), - }, - wake: None, - conns: std::ptr::null(), - io: std::ptr::null(), - services: std::ptr::null(), -}); - -/// The claims a snapshot publishes, `(verb, target)`. -fn claims_of(snapshot: *const PlaneSnapshot) -> Vec<(String, String)> { - if snapshot.is_null() { - return Vec::new(); - } - // SAFETY: the door's snapshot, valid until its generation retires. - let s = unsafe { &*snapshot }; - if s.claims.is_null() { - return Vec::new(); - } - // SAFETY: as above, `claims_len` claims. - unsafe { std::slice::from_raw_parts(s.claims, s.claims_len) } - .iter() - .map(|c| (read(c.verb), read(c.target))) - .collect() -} - -/// The ticket a unit's pieces are pushed on: one per unit. -pub fn ticket_of(unit: u64) -> Ticket { - Ticket { - slot: u32::try_from(unit).expect("a small unit"), - generation: 1, - } -} - -/// One opened instance of the door. -pub struct Door { - instance: *mut c_void, - /// The first generation's claims. - pub claims: Vec<(String, String)>, -} - -/// Open the door over `settings` (the `decisions:` section, as written). -pub fn open(settings: &str) -> Door { - open_with(settings, &[]) -} - -/// Open the door over `settings`, the host stating each referenced provider's `(name, protocol)`. -pub fn open_with(settings: &str, providers: &[(&str, &str)]) -> Door { - let facts: Vec = providers - .iter() - .map(|(name, protocol)| ProviderFacts { - name: str_of(name.as_bytes()), - protocol: str_of(protocol.as_bytes()), - error_map: Blob::ABSENT, - }) - .collect(); - let mut err = [0_u8; 1024]; - let mut i: PlaneOpenIn = z(); - i.open = OpenIn { - head: head::(life::OPEN, Ticket::NONE), - host: &TABLES.0, - settings: octets(settings.as_bytes()), - generation: 1, - err_buf: err.as_mut_ptr(), - err_cap: err.len(), - ..z() - }; - (i.providers, i.providers_len) = (facts.as_ptr(), facts.len()); - let mut o: PlaneOpenOut = z(); - o.open = OpenOut { - head: out_head::(), - ..z() - }; - let f = lifecycle().open.expect("the door opens"); - let ret = f( - std::ptr::null_mut(), - std::ptr::from_ref(&i).cast(), - std::ptr::from_mut(&mut o).cast(), - ); - assert_eq!( - ret.outcome(), - Outcome::Ready, - "the door opens over {settings}" - ); - Door { - instance: o.open.instance, - claims: claims_of(o.snapshot), - } -} - -/// `validate` of the dealt blob `{decisions: }`: the outcome and its words. -pub fn validate(settings: &str) -> (Outcome, String) { - let dealt = format!("{{\"decisions\":{settings}}}"); - let mut err = [0_u8; 1024]; - let mut i: ValidateIn = z(); - i.head = head::(life::VALIDATE, Ticket::NONE); - i.settings = octets(dealt.as_bytes()); - (i.err_buf, i.err_cap) = (err.as_mut_ptr(), err.len()); - let mut o: OutHead = out_head::(); - let f = lifecycle().validate.expect("validate"); - let ret = f( - std::ptr::null_mut(), - std::ptr::from_ref(&i).cast(), - std::ptr::from_mut(&mut o).cast(), - ); - (ret.outcome(), read(o.error)) -} - -/// What `arrive` answered. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Arrived { - pub outcome: Outcome, - /// The plane's own refusal code, `0` unless refused. - pub refusal: u32, - /// The status a refusal wears. - pub status: u32, - /// The refusal's words (`head.error`). - pub text: String, - /// The route class and the entry it names. - pub route: u8, - pub entry: String, -} - -/// What one `on_piece` answered, re-called while it said `more`. -#[derive(Debug, Clone, Default)] -pub struct Answered { - pub outcome: Option, - /// The bytes emitted, over every re-call. - pub bytes: Vec, - /// The bytes were bound for the far end. - pub far: bool, - /// The answer said done. - pub done: bool, - /// The reply status. - pub status: u32, - /// The request line of an ATTEMPT, `" "`. - pub request: String, - /// The head fields written, `(name, value)`. - pub fields: Vec<(String, String)>, - /// The units written, `(class, source, amount)`. - pub units: Vec<(u32, u32, u64)>, - /// Short-answer needs, when the answer was short. - pub short: bool, -} - -/// One piece as the kernel pushes it. -#[derive(Debug, Clone, Copy)] -pub struct Given<'a> { - pub from: u32, - pub flags: u32, - pub status: u32, - pub attempt: u32, - pub bytes: &'a [u8], - /// The dialect `arrive` answered, carried on every piece. - pub dialect: u32, -} - -impl<'a> Given<'a> { - /// The ATTEMPT piece of attempt `n`. - pub const fn attempt(n: u32) -> Self { - Self { - from: FROM_KERNEL, - flags: 0, - status: 0, - attempt: n, - bytes: b"", - dialect: 0, - } - } - - /// The caller's whole body. - pub const fn caller(bytes: &'a [u8]) -> Self { - Self { - from: FROM_CALLER, - flags: PIECE_LAST, - status: 0, - attempt: 0, - bytes, - dialect: 0, - } - } - - /// The far end's whole answer, its status on it. - pub const fn whole(status: u32, bytes: &'a [u8]) -> Self { - Self { - from: FROM_FAR_END, - flags: PIECE_HAS_STATUS | PIECE_LAST, - status, - attempt: 0, - bytes, - dialect: 0, - } - } - - /// One piece of the far end's answer: the first carries `status`, the last says so. - pub const fn far(first: Option, last: bool, bytes: &'a [u8]) -> Self { - let mut flags = 0; - let mut status = 0; - if let Some(s) = first { - flags |= PIECE_HAS_STATUS; - status = s; - } - if last { - flags |= PIECE_LAST; - } - Self { - from: FROM_FAR_END, - flags, - status, - attempt: 0, - bytes, - dialect: 0, - } - } -} - -/// The host buffers one `on_piece` is lent, reused across calls so a measured call allocates only -/// what the door does. -pub struct Bufs { - reply: Vec, - fields: Vec, - arena: Vec, - records: Vec, - units: Vec, -} - -impl Bufs { - pub fn new(reply_cap: usize) -> Self { - Self { - reply: vec![0; reply_cap], - fields: vec![z(); 16], - arena: vec![0; 1 << 14], - records: vec![z(); 4], - units: vec![z(); 8], - } - } -} - -impl Door { - /// `refresh` onto `generation` over `settings`: the new generation's claims. - pub fn refresh(&self, settings: &str, generation: u64) -> Vec<(String, String)> { - let mut i: RefreshIn = z(); - i.head = head::(life::REFRESH, Ticket::NONE); - i.generation = generation; - i.settings = octets(settings.as_bytes()); - let mut o: PlaneRefreshOut = z(); - o.head = out_head::(); - let f = lifecycle().refresh.expect("refresh"); - let ret = f( - self.instance, - std::ptr::from_ref(&i).cast(), - std::ptr::from_mut(&mut o).cast(), - ); - assert_eq!(ret.outcome(), Outcome::Ready, "the door refreshes"); - claims_of(o.snapshot) - } - - /// `arrive` of `unit` on `POST /v1/systemone` with `body`. - pub fn arrive(&self, unit: u64, body: &[u8]) -> Arrived { - self.arrive_on(unit, "POST", "/v1/systemone", body, &[]) - } - - /// `arrive` of `unit` on `verb target` with `body` and the caller's `fields`. - pub fn arrive_on( - &self, - unit: u64, - verb: &str, - target: &str, - body: &[u8], - fields: &[(&str, &str)], - ) -> Arrived { - let fields: Vec = fields - .iter() - .map(|(n, v)| Field { - name: str_of(n.as_bytes()), - value: str_of(v.as_bytes()), - }) - .collect(); - let mut i: ArriveIn = z(); - i.head = head::(slot::ARRIVE, Ticket::NONE); - i.unit = unit; - i.target = str_of(target.as_bytes()); - i.method = str_of(verb.as_bytes()); - i.body = octets(body); - (i.fields, i.fields_len) = (fields.as_ptr(), fields.len()); - let mut units = [z::(); 4]; - (i.units_buf, i.units_cap) = (units.as_mut_ptr(), units.len()); - let mut o: ArriveOut = z(); - o.head = out_head::(); - let f = ops().arrive.expect("arrive"); - let ret = f( - self.instance, - std::ptr::from_ref(&i).cast(), - std::ptr::from_mut(&mut o).cast(), - ); - Arrived { - outcome: ret.outcome(), - refusal: o.refusal, - status: o.refusal_status, - text: read(o.head.error), - route: o.route, - entry: read(o.pool), - } - } - - /// One piece of `unit` on its ticket, re-called while the door says `more`. - pub fn piece(&self, unit: u64, given: Given<'_>, bufs: &mut Bufs) -> Answered { - let ticket = ticket_of(unit); - let mut got = Answered::default(); - let mut first = true; - for _ in 0..1024 { - let mut i: OnPieceIn = z(); - i.head = head::(slot::ON_PIECE, ticket); - i.unit = unit; - i.from = given.from; - i.dialect = given.dialect; - if first { - i.flags = given.flags; - i.bytes = octets(given.bytes); - i.status_code = given.status; - i.attempt_no = given.attempt; - } else { - i.bytes = octets(b""); - } - (i.reply_buf, i.reply_cap) = (bufs.reply.as_mut_ptr(), bufs.reply.len()); - (i.fields_buf, i.fields_cap) = (bufs.fields.as_mut_ptr(), bufs.fields.len()); - (i.arena_buf, i.arena_cap) = (bufs.arena.as_mut_ptr(), bufs.arena.len()); - (i.records_buf, i.records_cap) = (bufs.records.as_mut_ptr(), bufs.records.len()); - (i.units_buf, i.units_cap) = (bufs.units.as_mut_ptr(), bufs.units.len()); - let mut o: OnPieceOut = z(); - o.head = out_head::(); - let f = ops().on_piece.expect("on_piece"); - let ret = f( - self.instance, - std::ptr::from_ref(&i).cast(), - std::ptr::from_mut(&mut o).cast(), - ); - first = false; - got.outcome = Some(ret.outcome()); - got.short |= o.units_needed != 0 || o.fields_needed != 0 || o.arena_needed != 0; - if o.reply_status != 0 { - got.status = o.reply_status; - } - let arena = &bufs.arena; - let at = |s: Span| { - let from = s.offset as usize; - String::from_utf8_lossy(&arena[from..from + s.len as usize]).into_owned() - }; - if ret.outcome() == Outcome::Ready { - got.fields.extend( - bufs.fields[..o.fields_written as usize] - .iter() - .map(|f| (at(f.name), at(f.value))), - ); - got.units.extend( - bufs.units[..o.units_written as usize] - .iter() - .map(|u| (u.class, u.source, u.amount)), - ); - if o.verb.len != 0 { - got.request = format!("{} {}", at(o.verb), at(o.target)); - } - } - got.far |= o.flags & EMIT_TO_FAR_END != 0; - got.done |= o.flags & EMIT_DONE != 0; - got.bytes - .extend_from_slice(&bufs.reply[..usize::try_from(o.emitted).unwrap()]); - if ret.outcome() != Outcome::Ready || o.more == 0 { - break; - } - } - got - } - - /// One crossing of `unit`'s piece that keeps nothing of the answer (so it allocates nothing - /// of its own): the bytes emitted. The answer must fit and be READY. - pub fn piece_bytes(&self, unit: u64, given: Given<'_>, bufs: &mut Bufs) -> usize { - let mut i: OnPieceIn = z(); - i.head = head::(slot::ON_PIECE, ticket_of(unit)); - i.unit = unit; - i.from = given.from; - i.dialect = given.dialect; - i.flags = given.flags; - i.bytes = octets(given.bytes); - i.status_code = given.status; - i.attempt_no = given.attempt; - (i.reply_buf, i.reply_cap) = (bufs.reply.as_mut_ptr(), bufs.reply.len()); - (i.fields_buf, i.fields_cap) = (bufs.fields.as_mut_ptr(), bufs.fields.len()); - (i.arena_buf, i.arena_cap) = (bufs.arena.as_mut_ptr(), bufs.arena.len()); - (i.records_buf, i.records_cap) = (bufs.records.as_mut_ptr(), bufs.records.len()); - (i.units_buf, i.units_cap) = (bufs.units.as_mut_ptr(), bufs.units.len()); - let mut o: OnPieceOut = z(); - o.head = out_head::(); - let f = ops().on_piece.expect("on_piece"); - let ret = f( - self.instance, - std::ptr::from_ref(&i).cast(), - std::ptr::from_mut(&mut o).cast(), - ); - assert!(ret.outcome() == Outcome::Ready && o.more == 0); - usize::try_from(o.emitted).unwrap() - } - - /// `refusal` of `unit` (`0`: none arrived) at `status` with the kernel's `text`; `arrived` - /// says the refusal is the plane's own arrival's. Answers the outcome and the rendered body. - pub fn refusal(&self, unit: u64, status: u32, text: &str, arrived: bool) -> (Outcome, Vec) { - let (mut reply, mut arena) = (vec![0_u8; 4096], vec![0_u8; 1024]); - let mut fields = [z::(); 4]; - let mut i: RefusalIn = z(); - i.head = head::(slot::REFUSAL, Ticket::NONE); - i.cause = if arrived { - REFUSAL_ARRIVE - } else { - REFUSAL_KERNEL - }; - i.status = status; - i.text = str_of(text.as_bytes()); - i.unit = unit; - (i.reply_buf, i.reply_cap) = (reply.as_mut_ptr(), reply.len()); - (i.fields_buf, i.fields_cap) = (fields.as_mut_ptr(), fields.len()); - (i.arena_buf, i.arena_cap) = (arena.as_mut_ptr(), arena.len()); - let mut o: RefusalOut = z(); - o.head = out_head::(); - let f = ops().refusal.expect("refusal"); - let ret = f( - self.instance, - std::ptr::from_ref(&i).cast(), - std::ptr::from_mut(&mut o).cast(), - ); - let n = usize::try_from(o.reply_written).unwrap().min(reply.len()); - (ret.outcome(), reply[..n].to_vec()) - } - - /// `cancel` of the op on `ticket`: the outcome and the disposition. - pub fn cancel(&self, ticket: Ticket) -> (Outcome, u32) { - let mut records = [z::(); 4]; - let mut arena = [0_u8; 256]; - let mut i: PlaneCancelIn = z(); - i.cancel = CancelIn { - head: head::(life::CANCEL, ticket), - ticket, - }; - (i.records_buf, i.records_cap) = (records.as_mut_ptr(), records.len()); - (i.arena_buf, i.arena_cap) = (arena.as_mut_ptr(), arena.len()); - let mut o: PlaneCancelOut = z(); - o.cancel = CancelOut { - head: out_head::(), - ..z() - }; - let f = lifecycle().cancel.expect("cancel"); - let ret = f( - self.instance, - std::ptr::from_ref(&i).cast(), - std::ptr::from_mut(&mut o).cast(), - ); - (ret.outcome(), o.cancel.disposition) - } -} diff --git a/crates/busbar-plane-decisions/tests/durable_strings.rs b/crates/busbar-plane-decisions/tests/durable_strings.rs deleted file mode 100644 index e1aeb8f8e9..0000000000 --- a/crates/busbar-plane-decisions/tests/durable_strings.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! THE DURABLE-STRING PIN (1.6.0-TODO KERNEL<>PLUGINS step 31; spec #17/#48; THE DESIGN, the plane -//! verbs and the end-state crate list). -//! -//! The owner renamed the plane `decisions`: this crate is `busbar-plane-decisions`, its feature -//! `plane-decisions`, its config verb `decisions`. What the rename does NOT touch is the plane's -//! `KEY` and its meter class: both stay the durable string `"decision"`, because a stored meter -//! row, a rate-card class and a registry key written under that string must keep meaning this -//! plane. A change to either string, or a crate that is no longer the renamed one, is red here. - -use busbar_contract::plane::PlaneMeta; -use busbar_plane_decisions::{meta::CLASS_DECISION, DecisionPlane}; - -#[test] -fn the_crate_is_the_renamed_one() { - assert_eq!(env!("CARGO_PKG_NAME"), "busbar-plane-decisions"); -} - -#[test] -fn the_plane_key_stays_the_durable_string() { - assert_eq!(::KEY, "decision"); -} - -#[test] -fn the_meter_class_stays_the_durable_string() { - assert_eq!(CLASS_DECISION.as_str(), "decision"); - let classes = ::METER_CLASSES; - assert!(!classes.is_empty()); - assert!(classes.iter().all(|c| c.key.as_str() == "decision")); -} diff --git a/crates/busbar-plane-decisions/tests/invariance.rs b/crates/busbar-plane-decisions/tests/invariance.rs deleted file mode 100644 index 08e96dd501..0000000000 --- a/crates/busbar-plane-decisions/tests/invariance.rs +++ /dev/null @@ -1,196 +0,0 @@ -//! This plane is the same plane everywhere it is compiled. -//! -//! Mirrors `busbar-plane-a2a`/`busbar-plane-mcp`'s own `invariance.rs`, and mirrors it exactly now: -//! the ONE deliberate difference it used to carry — this manifest named the legacy plugin-facing -//! crate for `UpstreamCreds`, which its pure siblings never did — is gone, and that crate has -//! joined the forbidden list rather than the allowed one. The exception was not free. It depends on -//! `sha2`, so the manifest edge put `sha2 -> cpufeatures -> libc` in a pure plane's closure, which -//! the transitive source denylist bans outright, and this was the only plane of the five with such -//! an edge (1 occurrence against 0 for the other four). `UpstreamCreds` moved to -//! `busbar_contract::config::UpstreamCreds` verbatim, the same route `ModelCfg` took out of -//! `busbar-substrate`/`busbar-kernel` (DECISIONS #40/#38), so both reserved model-serving shapes -//! now arrive from the crate a plugin may name and the manifest names exactly one busbar crate. The -//! forbidden list is that crate, `busbar-core`, `busbar-caps`, `busbar-kernel`, `busbar-unit-*`, -//! and the sibling PLANE crates (naming another plane would be a plane reading another plane's -//! private surface, never legitimate for any plane). -//! -//! `the_manifest_names_only_what_this_plane_may_name` was RED for as long as this manifest named -//! `busbar-kernel`, and it is GREEN now because the manifest does not. The edge existed for ONE -//! file — `src/registry.rs`, a `PlaneDecl` constant declared against a FUTURE root registration -//! that never arrived — and that file was deleted, not relocated: nothing outside this crate ever -//! named `PLANE_DECL`, and the binary does not even depend on this crate. This test is the #40 -//! witness for that closure, so it must never be relaxed to keep it green; if `busbar-kernel` -//! returns to the manifest, this is the line that is supposed to fail. - -use std::path::{Path, PathBuf}; - -fn src_dir() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("src") -} - -fn manifest() -> String { - std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml")) - .expect("the manifest is readable") -} - -/// Every OTHER plane crate directory under `crates/`, read live off the tree rather than spelled by -/// name — the forbidden list below forbids by SHAPE (`busbar-plane-*`, minus this crate itself) -/// instead of a plane naming its siblings, which is the broker-law violation DECISION #8 forbids in -/// the first place. It also stays true automatically if the plane roster (#48) ever changes. -fn sibling_plane_dirs() -> Vec { - let crates_dir = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("this crate sits under crates/") - .to_path_buf(); - let this = Path::new(env!("CARGO_MANIFEST_DIR")) - .file_name() - .and_then(|n| n.to_str()) - .expect("this crate has a directory name") - .to_string(); - let mut out: Vec = std::fs::read_dir(&crates_dir) - .expect("crates/ is readable") - .flatten() - .filter(|e| e.path().is_dir()) - .filter_map(|e| e.file_name().to_str().map(str::to_string)) - .filter(|name| name.starts_with("busbar-plane-") && *name != this) - .collect(); - out.sort(); - out -} - -fn walk(dir: &Path, f: &mut impl FnMut(&Path, &str)) { - for entry in std::fs::read_dir(dir) - .expect("the source directory is readable") - .flatten() - { - let path = entry.path(); - if path.is_dir() { - walk(&path, f); - } else if path.extension().is_some_and(|e| e == "rs") { - let text = std::fs::read_to_string(&path).expect("a source file is readable"); - f(&path, &text); - } - } -} - -/// The manifest declares no features at all. -#[test] -fn the_crate_declares_no_features() { - let manifest = manifest(); - assert!( - !manifest.contains("[features]"), - "the plane declares cargo features, so it is not one plane" - ); - assert!( - !manifest.contains("optional = true"), - "an optional dependency is a feature by another name" - ); -} - -/// No item on the surface is behind a conditional-compilation attribute (test modules exempted). -#[test] -fn no_item_is_conditionally_compiled() { - let mut offenders = Vec::new(); - walk(&src_dir(), &mut |path, text| { - for (n, line) in text.lines().enumerate() { - let line = line.trim_start(); - if line.starts_with("#[cfg(test)]") { - continue; - } - if line.starts_with("#[cfg(") || line.starts_with("#![cfg(") { - offenders.push(format!("{}:{}", path.display(), n + 1)); - } - } - }); - assert!( - offenders.is_empty(), - "conditionally compiled items: {offenders:?}" - ); -} - -/// The crate carries the two lint gates the design requires of a plugin. -#[test] -fn the_crate_forbids_unsafe_and_undocumented_items() { - let lib = std::fs::read_to_string(src_dir().join("lib.rs")).expect("the root is readable"); - assert!(lib.contains("#![forbid(unsafe_code)]")); - assert!(lib.contains("#![deny(missing_docs)]")); -} - -/// No source file contains the word the lint gate forbids. -#[test] -fn the_source_contains_no_unsafe_block() { - let mut offenders = Vec::new(); - walk(&src_dir(), &mut |path, text| { - for (n, line) in text.lines().enumerate() { - let t = line.trim_start(); - if t.starts_with("//") { - continue; - } - if line.contains("unsafe ") { - offenders.push(format!("{}:{}", path.display(), n + 1)); - } - } - }); - assert!( - offenders.is_empty(), - "unsafe code on the surface: {offenders:?}" - ); -} - -/// The manifest names only the contract (which holds both explicitly-ruled model-serving config -/// types, `ModelCfg` and `UpstreamCreds`) and the serializer — never a genuinely kernel-side crate, -/// never the legacy plugin-facing one, never a sibling plane. -#[test] -fn the_manifest_names_only_what_this_plane_may_name() { - let manifest = manifest(); - let deps = manifest - .split("[dependencies]") - .nth(1) - .expect("the manifest has a dependency section"); - for line in deps.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') || line.starts_with('[') { - continue; - } - let name = line.split_whitespace().next().unwrap_or_default(); - let mut forbidden: Vec = vec![ - // The edge this plane alone had, and the one that reached banned source. It is a ban, - // not an exception, since `UpstreamCreds` moved to `busbar-contract`. - "busbar-api".to_string(), - "busbar-core".to_string(), - "busbar-caps".to_string(), - "busbar-kernel".to_string(), - "busbar-unit-".to_string(), - "busbar-admin".to_string(), - ]; - forbidden.extend(sibling_plane_dirs()); - for forbidden in &forbidden { - assert!( - !name.starts_with(forbidden.as_str()), - "the plane depends on {name}, which is kernel-side or a sibling plane" - ); - } - } -} - -/// The comments cite the design in words, not in section numbers or binding identifiers. -#[test] -fn the_comments_cite_the_design_in_words() { - let mut offenders = Vec::new(); - let mut check = |path: &Path, text: &str| { - for (n, line) in text.lines().enumerate() { - if line.contains('\u{a7}') { - offenders.push(format!("{}:{}: section sign", path.display(), n + 1)); - } - } - }; - walk(&src_dir(), &mut check); - walk( - &Path::new(env!("CARGO_MANIFEST_DIR")).join("tests"), - &mut check, - ); - assert!( - offenders.is_empty(), - "the source cites the design by section-sign rather than in words: {offenders:?}" - ); -} diff --git a/crates/busbar-plane-decisions/tests/jev.rs b/crates/busbar-plane-decisions/tests/jev.rs deleted file mode 100644 index 035915f459..0000000000 --- a/crates/busbar-plane-decisions/tests/jev.rs +++ /dev/null @@ -1,238 +0,0 @@ -//! jev-specific evidence the generic conformance/purity/invariance/alloc-gate suites do not cover: -//! round-trip byte-identity on both operations, the billable-success-only metering surface, and the -//! PII witness — the whole reason this plane exists in the shape it does. - -mod common; - -use busbar_contract::bounded::{FactValue, Facts, Ir}; -use busbar_contract::plane::{Plane, Response}; -use busbar_contract::unit::FinishClass; -use busbar_plane_decisions::{facts, meta::CLASS_DECISION, ops, DecisionPlane}; -use common::Scaffold; - -/// A successful `systemone` answer, billing 7 units. -const SYSTEMONE_SUCCESS: &[u8] = - br#"{"request_id":"req_7","usage":{"units":7},"answers":{"decision":"approve"}}"#; - -/// A 422 whose body echoes the caller's `state` and the provider's `answers` — the PII witness -/// fixture. Both values are deliberately distinctive strings so a substring search cannot miss a -/// leak. -const SYSTEMONE_422_WITH_STATE: &[u8] = br#"{"error":{"code":"invalid_state","message":"bad transition"},"state":{"session":"CALLER-SECRET-STATE-9f3a"},"answers":{"leaked":"PROVIDER-ANSWER-CONTENT-77bd"}}"#; - -/// The two distinctive substrings the fixture above carries, and that must never surface anywhere -/// this plane emits a fact. -const SECRET_STATE_MARKER: &str = "CALLER-SECRET-STATE-9f3a"; -const SECRET_ANSWERS_MARKER: &str = "PROVIDER-ANSWER-CONTENT-77bd"; - -/// The `models` catalogue response, relayed as-is. -const MODELS_LIST: &[u8] = - br#"{"request_id":"req_9","data":[{"id":"jev-1.13.0","object":"model"}]}"#; - -/// Every `FactValue::Str`/`FactValue::Bytes` value a `Facts` map carries, as owned strings — the -/// search surface the PII witness scans. -fn string_values(facts: &Facts<'_>) -> Vec { - facts - .iter() - .filter_map(|(_, v)| match v { - FactValue::Str(s) => Some(s.to_string()), - FactValue::Bytes(b) => Some(String::from_utf8_lossy(b).into_owned()), - _ => None, - }) - .collect() -} - -/// Drive `decode_response` on a body and hand the `Facts` it emitted to a closure — the boundary -/// the PII witness checks. A macro rather than a function: the decoded `Facts` borrows from a -/// `Vec` this helper owns, and that borrow cannot outlive a function's return, so the whole -/// decode-and-inspect step has to happen in the caller's own scope. -macro_rules! with_decoded_facts { - ($plane:expr, $body:expr, $scaffold:expr, |$facts:ident| $body_block:block) => {{ - let ctx = $scaffold.ctx(); - let frames = vec![common::response_frame($body)]; - let mut cursor = busbar_contract::wire::FrameCursor::new(&frames); - let busbar_contract::plane::Progress::Terminal { r: response, .. } = $plane - .decode_response(&mut cursor, &common::sealed_destination(), None, &ctx) - .expect("a whole answer decodes as terminal") - else { - panic!("a whole answer decodes as terminal, not a partial frame"); - }; - let $facts = response.facts; - $body_block - }}; -} - -#[test] -fn dialect_round_trip_is_byte_identical_for_both_ops() { - let plane = DecisionPlane::EMPTY; - for fixture in [SYSTEMONE_SUCCESS, MODELS_LIST] { - let scaffold = Scaffold::new("http"); - let ctx = scaffold.ctx(); - let r = Response { - ir: Ir::new(fixture, &[]), - finish: FinishClass::Complete, - facts: Facts::new(), - }; - let out = plane - .encode_response(&r, None, &ctx) - .expect("it round-trips"); - assert_eq!(out.as_slice(), fixture, "round trip changed the bytes"); - } -} - -#[test] -fn two_op_passthrough_forwards_both_request_bodies_unchanged() { - let plane = DecisionPlane::EMPTY; - let seal = common::TestSeal; - let dest = common::sealed_destination(); - for (op, body) in [ - (ops::OP_SYSTEMONE, br#"{"state":{"a":1}}"#.as_slice()), - (ops::OP_MODELS, br#"{}"#.as_slice()), - ] { - let scaffold = Scaffold::new("http"); - let ctx = scaffold.ctx(); - let unit = busbar_contract::unit::Unit::new( - &seal, - busbar_contract::UnitKey::new(1), - busbar_contract::unit::Origin::Client, - None, - None, - busbar_contract::wire::Direction::Inbound, - Some(common::principal()), - op, - Ir::new(body, &[]), - Facts::new(), - None, - ); - let egress = plane - .encode_egress(&unit, &dest, None, &ctx) - .expect("both ops express a hop to the configured provider"); - assert_eq!( - egress.body.as_slice(), - body, - "op {op:?} was not passed through unchanged" - ); - } -} - -#[test] -fn billable_success_metering_surface_reports_usage_on_success_and_nothing_on_error() { - let plane = DecisionPlane::EMPTY; - let seal = common::TestSeal; - let scaffold = Scaffold::new("http"); - let ctx = scaffold.ctx(); - let unit = busbar_contract::unit::Unit::new( - &seal, - busbar_contract::UnitKey::new(1), - busbar_contract::unit::Origin::Client, - None, - None, - busbar_contract::wire::Direction::Inbound, - Some(common::principal()), - ops::OP_SYSTEMONE, - Ir::new(b"{}", &[]), - Facts::new(), - None, - ); - - // Success: the class is reported, with the exact quantity the response carried. - with_decoded_facts!(plane, SYSTEMONE_SUCCESS, scaffold, |success_facts| { - let success_r = Response { - ir: Ir::new(SYSTEMONE_SUCCESS, &[]), - finish: FinishClass::Complete, - facts: success_facts, - }; - let locators = plane.meter(&unit, &success_r, &ctx); - assert_eq!(locators.lines.len(), 1); - let line = &locators.lines.as_slice()[0]; - assert_eq!(line.class, CLASS_DECISION); - assert_eq!(line.quantity, Some(7)); - }); - - // Error: nothing extractable under the billable-decision class at all — not a zero-quantity - // line, no line. - let error_scaffold = Scaffold::new("http"); - with_decoded_facts!( - plane, - SYSTEMONE_422_WITH_STATE, - error_scaffold, - |error_facts| { - let error_r = Response { - ir: Ir::new(SYSTEMONE_422_WITH_STATE, &[]), - finish: FinishClass::Error, - facts: error_facts, - }; - let locators = plane.meter(&unit, &error_r, &ctx); - assert!( - locators.lines.is_empty(), - "a 422 must yield nothing extractable under the billable-decision class" - ); - } - ); -} - -#[test] -fn pii_witness_never_surfaces_state_or_answers_in_any_fact() { - let plane = DecisionPlane::EMPTY; - let scaffold = Scaffold::new("http"); - let seal = common::TestSeal; - let unit = busbar_contract::unit::Unit::new( - &seal, - busbar_contract::UnitKey::new(2), - busbar_contract::unit::Origin::Client, - None, - None, - busbar_contract::wire::Direction::Inbound, - Some(common::principal()), - ops::OP_SYSTEMONE, - Ir::new(b"{}", &[]), - Facts::new(), - None, - ); - - with_decoded_facts!( - plane, - SYSTEMONE_422_WITH_STATE, - scaffold, - |response_facts| { - // decode_response's own Facts map: the primary boundary. - for value in string_values(&response_facts) { - assert!( - !value.contains(SECRET_STATE_MARKER), - "decode_response's facts leaked the caller's `state`: {value}" - ); - assert!( - !value.contains(SECRET_ANSWERS_MARKER), - "decode_response's facts leaked the provider's `answers`: {value}" - ); - } - - // content_facts, the record/export-path boundary. - let r = Response { - ir: Ir::new(SYSTEMONE_422_WITH_STATE, &[]), - finish: FinishClass::Error, - facts: response_facts, - }; - let ctx = scaffold.ctx(); - let content = plane.content_facts(&unit, &r, &ctx); - for value in string_values(&content.facts) { - assert!(!value.contains(SECRET_STATE_MARKER)); - assert!(!value.contains(SECRET_ANSWERS_MARKER)); - } - } - ); - - // The declared pointer lists themselves never name the two members — the structural guarantee - // behind every assertion above. - assert!(!busbar_plane_decisions::codec::RESPONSE_PTRS.contains(&"/state")); - assert!(!busbar_plane_decisions::codec::RESPONSE_PTRS.contains(&"/answers")); - assert_eq!( - busbar_plane_decisions::codec::REQUEST_PTRS, - &[busbar_plane_decisions::codec::PTR_MODEL] - ); - assert!(!facts::CONTENT_FACTS - .iter() - .any(|k| *k == "state" || *k == "answers")); - assert!(!facts::SESSION_FACTS - .iter() - .any(|k| *k == "state" || *k == "answers")); -} diff --git a/crates/busbar-plane-decisions/tests/nanojev.rs b/crates/busbar-plane-decisions/tests/nanojev.rs deleted file mode 100644 index 6f188f5905..0000000000 --- a/crates/busbar-plane-decisions/tests/nanojev.rs +++ /dev/null @@ -1,130 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE SECOND DIALECT, `nanojev`, DRIVEN OVER THE DOOR'S ABI (THE DESIGN section 2, the decisions -//! bullet): "NanoJev is the one, dialect `nanojev`: `/api/evaluate` is its second exact claim; a -//! model's dialect is its override or its provider's protocol; a model of the wrong dialect gets -//! that dialect's existing unknown-model refusal; it meters the upstream's own billing unit, -//! counted from the answer already read (`execution.states`; an answer with no count refuses -//! loudly ...), with no extra egress". - -#![allow(unsafe_code)] - -mod door_harness; - -use busbar_contract::abi::mechanism::call::Outcome; -use busbar_contract::abi::plane::{ROUTE_DIRECT, UNITS_REPORTED}; -use door_harness::{open, open_with, validate, Bufs, Given}; - -const SYSTEMONE: (&str, &str) = ("POST", "/v1/systemone"); -const EVALUATE: (&str, &str) = ("POST", "/api/evaluate"); - -/// One model of each dialect: `nano` by its own `protocol`, `jev` by its provider's. -const BOTH: &str = - r#"{"models":{"nano":{"provider":"np","protocol":"nanojev"},"jev":{"provider":"typesafe"}}}"#; - -fn claims(c: &[(&str, &str)]) -> Vec<(String, String)> { - c.iter() - .map(|(v, t)| ((*v).to_string(), (*t).to_string())) - .collect() -} - -/// RED (audit plane-decisions#3): `/api/evaluate` is claimed when a `nanojev` model is configured, -/// by its own `protocol` or its provider's as the host stated it at `open`, and only then. -#[test] -fn evaluate_is_claimed_with_a_nanojev_model() { - assert_eq!(open(BOTH).claims, claims(&[SYSTEMONE, EVALUATE])); - let by_provider = open_with( - r#"{"models":{"n":{"provider":"np"}}}"#, - &[("np", "nanojev")], - ); - assert_eq!(by_provider.claims, claims(&[EVALUATE])); - let jev_only = open_with( - r#"{"models":{"j":{"provider":"typesafe"}}}"#, - &[("typesafe", "jev")], - ); - assert_eq!(jev_only.claims, claims(&[SYSTEMONE])); - // A refresh keeps the provider facts `open` was handed. - assert_eq!( - by_provider.refresh(r#"{"models":{"n2":{"provider":"np"}}}"#, 2), - claims(&[EVALUATE]) - ); -} - -/// RED (audit plane-decisions#3): a request routes among its claim's dialect's models; a model of -/// the other dialect is that dialect's unknown model (404). -#[test] -fn a_model_of_the_wrong_dialect_is_an_unknown_model() { - let door = open(BOTH); - let nano = door.arrive_on(1, EVALUATE.0, EVALUATE.1, br#"{"input":{}}"#, &[]); - assert_eq!( - (nano.outcome, nano.route, nano.entry.as_str()), - (Outcome::Ready, ROUTE_DIRECT, "nano"), - "{nano:?}" - ); - let wrong = door.arrive_on(2, EVALUATE.0, EVALUATE.1, br#"{"model":"jev"}"#, &[]); - assert_eq!( - (wrong.outcome, wrong.status), - (Outcome::Refused, 404), - "{wrong:?}" - ); - let wrong = door.arrive_on(3, SYSTEMONE.0, SYSTEMONE.1, br#"{"model":"nano"}"#, &[]); - assert_eq!( - (wrong.outcome, wrong.status), - (Outcome::Refused, 404), - "{wrong:?}" - ); - let jev = door.arrive(4, br#"{"state":{}}"#); - assert_eq!( - (jev.outcome, jev.entry.as_str()), - (Outcome::Ready, "jev"), - "{jev:?}" - ); -} - -/// A `nanojev` unit served: its ATTEMPT, its body, and the far end's whole answer. -fn served(answer: &[u8]) -> (door_harness::Answered, door_harness::Answered) { - let door = open(BOTH); - let arrived = door.arrive_on(1, EVALUATE.0, EVALUATE.1, br#"{"input":{}}"#, &[]); - assert_eq!(arrived.outcome, Outcome::Ready); - let mut bufs = Bufs::new(1 << 16); - let mut attempt = Given::attempt(1); - attempt.dialect = 1; - let request = door.piece(1, attempt, &mut bufs); - let mut body = Given::caller(br#"{"input":{}}"#); - body.dialect = 1; - assert!(door.piece(1, body, &mut bufs).far); - let mut far = Given::whole(200, answer); - far.dialect = 1; - (request, door.piece(1, far, &mut bufs)) -} - -/// RED (audit plane-decisions#3): the far end is asked at the dialect's own path, and the answer's -/// `execution.states` is the decision count; an answer with none fails loudly. -#[test] -fn a_nanojev_unit_is_sent_to_evaluate_and_bills_its_states() { - let (request, answer) = served(br#"{"execution":{"states":3},"result":{}}"#); - assert_eq!(request.request, "POST /api/evaluate"); - assert_eq!(answer.outcome, Some(Outcome::Ready), "{answer:?}"); - assert_eq!(answer.units[0], (0, UNITS_REPORTED, 3), "{answer:?}"); - let (_, none) = served(br#"{"result":{}}"#); - assert_eq!( - (none.outcome, none.short), - (Some(Outcome::Failed), false), - "{none:?}" - ); - assert!(none.units.is_empty() && none.bytes.is_empty(), "{none:?}"); -} - -/// A model's own `protocol` that names no dialect the plane speaks is refused at validate (#51: -/// fail closed), naming the dialects it does. -#[test] -fn an_unspoken_protocol_is_refused_at_validate() { - let (outcome, words) = validate(r#"{"models":{"m":{"provider":"p","protocol":"bogus"}}}"#); - assert_eq!(outcome, Outcome::Refused, "{words}"); - assert!( - words.contains("bogus") && words.contains("jev, nanojev"), - "{words}" - ); - assert_eq!(validate(BOTH).0, Outcome::Ready); -} diff --git a/crates/busbar-plane-decisions/tests/purity.rs b/crates/busbar-plane-decisions/tests/purity.rs deleted file mode 100644 index 5704785123..0000000000 --- a/crates/busbar-plane-decisions/tests/purity.rs +++ /dev/null @@ -1,327 +0,0 @@ -//! The plane is pure, and this is what says so. -//! -//! Mirrors `busbar-plane-a2a`'s own `purity.rs`, and no longer carries the ONE difference it used -//! to. The legacy plugin-facing crate was exempted from the "names no kernel-side crate" scan here, -//! because this crate named it for `UpstreamCreds`; it is on the forbidden list now, like every -//! other non-contract busbar crate. The exemption had a price nobody was paying attention to: that -//! crate depends on `sha2`, and `sha2 -> cpufeatures -> libc` is banned source in a pure plugin -//! kind. `UpstreamCreds` moved to `busbar_contract::config` verbatim and the edge is gone, so the -//! exemption has nothing left to excuse. `ModelCfg` needed no exception either — it is -//! `busbar_contract::config::ModelCfg` (DECISIONS #40/#38), the crate's ordinary contract -//! dependency. `busbar_kernel` stays ON the forbidden list and this crate no longer names it -//! anywhere: `src/registry.rs` and its sibling `src/tests/registry.rs` were the only two lines of -//! source that did, and both files are gone — an unreachable generation-1 `PlaneDecl` constant that -//! nothing outside this crate ever read. - -use busbar_contract::plane::{Ingress, Plane, PlaneMeta}; -use busbar_contract::wire::FrameCursor; -use busbar_plane_decisions::DecisionPlane; -use std::path::{Path, PathBuf}; - -mod common; -use common::{frame, Scaffold}; - -fn src_dir() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("src") -} - -fn walk(dir: &Path, f: &mut impl FnMut(&Path, &str)) { - for entry in std::fs::read_dir(dir) - .expect("the source directory is readable") - .flatten() - { - let path = entry.path(); - if path.is_dir() { - walk(&path, f); - } else if path.extension().is_some_and(|e| e == "rs") { - let text = std::fs::read_to_string(&path).expect("a source file is readable"); - f(&path, &text); - } - } -} - -fn is_comment(line: &str) -> bool { - let t = line.trim_start(); - t.starts_with("//") || t.starts_with('*') || t.starts_with("/*") -} - -/// Every OTHER plane crate's module spelling (`busbar_plane_`), read live off the `crates/` -/// directory rather than spelled by name — the forbidden list below forbids by SHAPE -/// (`busbar-plane-*`, minus this crate itself) instead of a plane naming its siblings, which is the -/// broker-law violation DECISION #8 forbids in the first place. It also stays true automatically if -/// the plane roster (#48) ever changes. -fn sibling_plane_modules() -> Vec { - let crates_dir = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("this crate sits under crates/") - .to_path_buf(); - let this = Path::new(env!("CARGO_MANIFEST_DIR")) - .file_name() - .and_then(|n| n.to_str()) - .expect("this crate has a directory name") - .to_string(); - let mut out: Vec = std::fs::read_dir(&crates_dir) - .expect("crates/ is readable") - .flatten() - .filter(|e| e.path().is_dir()) - .filter_map(|e| e.file_name().to_str().map(str::to_string)) - .filter(|name| name.starts_with("busbar-plane-") && *name != this) - .map(|name| name.replace('-', "_")) - .collect(); - out.sort(); - out -} - -/// The plane keeps no state of its own between calls. -#[test] -fn the_plane_holds_no_interior_state() { - let forbidden = [ - "Cell<", - "RefCell", - "UnsafeCell", - "Mutex<", - "RwLock", - "AtomicUsize", - "AtomicU64", - "AtomicU32", - "AtomicBool", - "OnceLock", - "OnceCell", - "LazyLock", - "lazy_static", - "thread_local", - "static mut", - ]; - let mut offenders = Vec::new(); - walk(&src_dir(), &mut |path, text| { - for (n, line) in text.lines().enumerate() { - if is_comment(line) { - continue; - } - for name in forbidden { - if line.contains(name) { - offenders.push(format!("{}:{}: {name}", path.display(), n + 1)); - } - } - } - }); - assert!( - offenders.is_empty(), - "the plane holds interior state: {offenders:?}" - ); -} - -/// The plane performs no input and no output. -#[test] -fn the_plane_performs_no_input_or_output() { - let forbidden = [ - "std::fs", - "std::net", - "std::process", - "std::thread", - "std::io", - "SystemTime", - "Instant::now", - "tokio", - "reqwest", - "async fn", - "await", - "env!", - "std::env", - "println!", - "eprintln!", - ]; - let mut offenders = Vec::new(); - walk(&src_dir(), &mut |path, text| { - for (n, line) in text.lines().enumerate() { - if is_comment(line) { - continue; - } - for name in forbidden { - if line.contains(name) { - offenders.push(format!("{}:{}: {name}", path.display(), n + 1)); - } - } - } - }); - assert!( - offenders.is_empty(), - "the plane reaches outside itself: {offenders:?}" - ); -} - -/// The plane names no GENUINELY kernel-side crate. -/// -/// The legacy plugin-facing crate IS on this list now (first entry). It was the deliberate -/// exception for as long as this crate named it for `UpstreamCreds`, and that exception was the -/// single edge by which a pure plane reached banned source (`sha2 -> cpufeatures -> libc`). The -/// type moved to `busbar_contract::config` and the entry below is what stops the edge coming back -/// by the same door. `busbar_kernel` has been on it throughout, and this test was RED on exactly -/// two source lines — -/// `src/registry.rs`'s `use` of `PlaneDecl` and `src/tests/registry.rs`'s `BuildCtx` literal — -/// until both files were deleted as unreachable code. It is GREEN now and is the source-side half -/// of this crate's #40 witness (the manifest-side half is `invariance.rs`'s -/// `the_manifest_names_only_what_this_plane_may_name`). -#[test] -fn the_plane_names_no_kernel_side_crate() { - let mut forbidden: Vec = vec![ - "busbar_api".to_string(), - "busbar_caps".to_string(), - "busbar_kernel".to_string(), - "busbar_unit".to_string(), - "busbar_core".to_string(), - "busbar_admin".to_string(), - ]; - forbidden.extend(sibling_plane_modules()); - let mut offenders = Vec::new(); - walk(&src_dir(), &mut |path, text| { - for (n, line) in text.lines().enumerate() { - if is_comment(line) { - continue; - } - for name in &forbidden { - if line.contains(name.as_str()) { - offenders.push(format!("{}:{}: {name}", path.display(), n + 1)); - } - } - } - }); - assert!( - offenders.is_empty(), - "the plane names kernel-side crates: {offenders:?}" - ); -} - -/// The decode step gives the same answer every time it is asked the same question. -#[test] -fn the_decode_step_is_deterministic() { - let plane = DecisionPlane::EMPTY; - let body = br#"{"state":{"a":1},"context":{}}"#; - let mut answers = Vec::new(); - for _ in 0..8 { - let scaffold = Scaffold::new("http") - .with_method("POST") - .on_path("/v1/systemone"); - let ctx = scaffold.ctx(); - let frames = vec![frame(body)]; - let mut cursor = FrameCursor::new(&frames); - let ingress = plane - .decode_ingress(&mut cursor, None, &ctx) - .expect("a known surface decodes"); - let summary = match ingress { - Ingress::OneShot(d) => format!("{:?}/{}", d.op, d.facts.len()), - other => format!("{other:?}"), - }; - answers.push(summary); - } - assert!( - answers.windows(2).all(|w| w[0] == w[1]), - "the decode step varied over one body: {answers:?}" - ); -} - -/// The encode step writes the same bytes every time it is asked the same question. -#[test] -fn the_encode_step_is_deterministic() { - let plane = DecisionPlane::EMPTY; - let answer = br#"{"request_id":"r1","usage":{"units":1}}"#; - let mut written = Vec::new(); - for _ in 0..8 { - let scaffold = Scaffold::new("http"); - let ctx = scaffold.ctx(); - let r = busbar_contract::plane::Response { - ir: busbar_contract::bounded::Ir::new(answer, &[]), - finish: busbar_contract::unit::FinishClass::Complete, - facts: busbar_contract::bounded::Facts::new(), - }; - let out = plane - .encode_response(&r, None, &ctx) - .expect("it re-encodes"); - written.push(out.as_slice().to_vec()); - } - assert!( - written.windows(2).all(|w| w[0] == w[1]), - "the encode step varied over one answer" - ); -} - -/// The plane does not read the clock, so a call at a different time gives the same answer. -#[test] -fn the_answer_does_not_move_with_the_clock() { - let plane = DecisionPlane::EMPTY; - let body = br#"{"state":{"a":1}}"#; - let mut answers = Vec::new(); - for unix_secs in [2_000_000_000_u64, 1_000_000_000] { - let scaffold = Scaffold::new("http") - .with_method("POST") - .on_path("/v1/systemone"); - let ctx = busbar_contract::unit::Ctx::new( - busbar_contract::unit::Clock { - unix_secs, - monotonic_nanos: 0, - }, - &scaffold.config, - Some(&scaffold.session), - &scaffold.transport, - &scaffold.labels, - &scaffold.arena, - ); - assert_eq!(ctx.clock().unix_secs, unix_secs); - let frames = vec![frame(body)]; - let mut cursor = FrameCursor::new(&frames); - let Ok(Ingress::OneShot(draft)) = plane.decode_ingress(&mut cursor, None, &ctx) else { - panic!("a systemone POST with a body decodes as one shot"); - }; - answers.push(format!("{:?}", draft.op)); - } - assert_eq!( - answers[0], answers[1], - "the decoded answer moved with the clock: {answers:?}" - ); -} - -/// The plane never hands back a decision, an amount or a credential. -#[test] -fn the_plane_names_no_money_and_no_decision() { - let forbidden = [ - "nano_units", - "nanounits", - "unit_price", - "price_micros", - "charge(", - "admit_decision", - "allow(", - "deny(", - "credential_bytes", - "secret_value", - "credential_token", - ]; - let mut offenders = Vec::new(); - walk(&src_dir(), &mut |path, text| { - for (n, line) in text.lines().enumerate() { - if is_comment(line) { - continue; - } - let t = line.trim_start(); - if t.starts_with("#[") || t.starts_with("#![") { - continue; - } - for name in forbidden { - if line.contains(name) { - offenders.push(format!("{}:{}: {name}", path.display(), n + 1)); - } - } - } - }); - assert!( - offenders.is_empty(), - "the plane names money or decisions: {offenders:?}" - ); -} - -/// The PlaneMeta constants are actually reachable off the crate root — a smoke check that the -/// crate wires its own trait impl where the other tests assume it does. -#[test] -fn plane_meta_key_is_decision() { - assert_eq!(::KEY, "decision"); -} diff --git a/crates/busbar-plane-mcp/Cargo.toml b/crates/busbar-plane-mcp/Cargo.toml deleted file mode 100644 index 37503f2e56..0000000000 --- a/crates/busbar-plane-mcp/Cargo.toml +++ /dev/null @@ -1,72 +0,0 @@ -# busbar-plane-mcp — THE MCP PLANE, as an ADAPTER and nothing else. -# -# WHAT THIS CRATE IS. One implementation of the plugin contract's plane kind for the Model Context -# Protocol. Every method is a thin adapter over a codec that already exists in `busbar-mcp`: the -# JSON-RPC envelope reader and writer, the method dispatch vocabulary, the notification codec and -# the error envelopes. No wire format is written twice. If a byte differs between what this crate -# produces and what the existing codec produces, the existing codec is the answer and this crate -# has the bug. -# -# WHAT THIS CRATE IS NOT. There is no governance here, no breaker, no hook seat, no approval -# decision and no metering arithmetic. Those are units, and a unit is on the other side of the -# kernel from a plane. The metering method returns LOCATORS — where the numbers are — and never a -# number the ledger would post. The routing method returns a plan, never a connection. The plane's -# durable state — the tool catalogue, the approvals, the settings — is reached only through record -# legs on that plan, never through a store this crate holds. -# -# THE DEPENDENCY EDGE, AND THE THING THAT CHANGED. There is no longer a codec crate to name. The -# MCP wire dialect — the mount path, the revision, the method names, the `_meta` keys, the error -# code table, the notification pair, the durable record vocabulary and the content sanitizer — -# folded IN here, because definition 16–20 puts everything one protocol needs and no other protocol -# may name in the protocol's own plane crate (#39: no `busbar-*-codec`). -# The engine's registry row and its two operation cells did NOT come with it: they name the kernel's -# handler matrix, which is kernel-side machinery, and they now read this crate's vocabulary rather -# than restating it. The adapter still names no runtime, opens nothing, holds no connection and -# reads no clock but the one the context hands it. -[package] -name = "busbar-plane-mcp" -version = "1.6.0" -publish = false # internal workspace crate; never goes to crates.io -edition = "2021" -# One pinned number, set to what CI verifies — the same rationale every other member states. -rust-version = "1.97" -description = "The MCP plane for busbar: the plugin contract's plane kind implemented as a thin adapter over the busbar-mcp JSON-RPC codec." -license = "Apache-2.0" - -[lib] -name = "busbar_plane_mcp" -path = "src/lib.rs" - -# NO FEATURES, ON PURPOSE. A plane compiled two ways is two planes, and the kernel would have no way -# to tell which one it registered. The contract crate holds itself to the same rule and asserts it. - -[dependencies] -# THE CONTRACT. The one busbar crate a plugin manifest may name without argument. -busbar-contract = { path = "../busbar-contract" } -# The `Mcp-Name` header's `=?base64?…?=` sentinel for a name a header value cannot carry -# (owner PLUGIN LIBRARIES ruling, 2026-09-28). -base64 = { workspace = true } -indexmap = { workspace = true } -# The notification codec writes its envelope as bytes, the same type the frame cursor hands in. -bytes = { workspace = true } -# The two wire parameter objects the notification codec reads and writes derive their shapes. -serde = { workspace = true } -# The object notation the codec speaks. The adapter hands values across the seam and never invents -# a second parser. -serde_json = { workspace = true } - -[dev-dependencies] -# The TEST-ONLY seal (#65). `busbar_contract::plugin::TestKernelSeal` is the one blessed -# fixture implementor of the SEALED `KernelSeal` trait; this crate's test harnesses used to -# forge their own. Dev-only and feature-gated, so it is absent from every release build. -busbar-contract = { path = "../busbar-contract", features = ["test-seal"] } -# The one loader both of the door's forms are admitted through (`tests/conformance.rs`, the -# `both_ways` rig): the compiled-in door by `load_linked`, the dropped-in `mcp_door` example by -# `load_dropped`, on one dispatcher. Dev-only, and named by that one file alone (the gate's -# conformance-witness grant). -busbar-plugin-loader = { path = "../plugin-loader" } - -# The door, dropped in: `export_door!` over the same `plane_door::door` the linked row holds. -[[example]] -name = "mcp_door" -crate-type = ["cdylib"] diff --git a/crates/busbar-plane-mcp/examples/mcp_door.rs b/crates/busbar-plane-mcp/examples/mcp_door.rs deleted file mode 100644 index 1c86560099..0000000000 --- a/crates/busbar-plane-mcp/examples/mcp_door.rs +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! The mcp plane's door, dropped in: the one exported symbol, forwarding to the same door a -//! compiled-in row holds. `tests/conformance.rs` (`both_ways`) loads it beside the linked door -//! and drives both the same way. - -busbar_contract::export_door!(busbar_plane_mcp::tool_door::door); diff --git a/crates/busbar-plane-mcp/src/adapt.rs b/crates/busbar-plane-mcp/src/adapt.rs deleted file mode 100644 index 48e184b6cc..0000000000 --- a/crates/busbar-plane-mcp/src/adapt.rs +++ /dev/null @@ -1,324 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE SESSION REVISIONS, READ INTO AND WRITTEN OUT OF THE ONE DISPATCH. -//! -//! The plane has one method dispatch, written against the stateless `2026-07-28` shape: every -//! request carries its revision and the client's capabilities in `params._meta`, and mirrors its -//! method (and target name) into request headers. A session revision states those facts once, in -//! `initialize`, and then relies on the session. So a session request is RAISED into the stateless -//! shape before dispatch ([`raise`]), and the answer is LOWERED back into the session revision after -//! it ([`lower_result`]). There is no second dispatch table. -//! -//! What a session client is allowed to reach is decided here too ([`session_method`]): the methods -//! the session revisions define. The stateless revision's own methods (`server/discover`, -//! `subscriptions/listen`, the tasks extension) are never offered to a session client, whether it -//! asks for them by name or reads the capabilities `initialize` answered with. The session's own -//! verbs (`resources/subscribe`, `resources/unsubscribe`, `logging/setLevel`) stay for the old -//! revisions (THE DESIGN section 2, the mcp bullet) and are answered from the session's state. -//! -//! The `2024-11-05` event-stream framing lives here as values ([`frame`], [`endpoint_event`]); -//! writing them to a connection is the caller's. - -use serde_json::{Map, Value}; - -use crate::codec::{META_CLIENT_CAPABILITIES, META_PROTOCOL_VERSION, PROTOCOL_VERSION}; -use crate::revision::Revision; - -/// The method that opens a session. -pub const METHOD_INITIALIZE: &str = "initialize"; -/// The notification that completes the opening. -pub const METHOD_INITIALIZED: &str = "notifications/initialized"; -/// The liveness request every session revision defines. -pub const METHOD_PING: &str = "ping"; -/// The response header that names a session, and the request header that carries it back. -pub const H_SESSION_ID: &str = "mcp-session-id"; -/// The request header a resuming GET names its cursor in. -pub const H_LAST_EVENT_ID: &str = "last-event-id"; -/// The query member the `2024-11-05` message address carries the session in. -pub const QUERY_SESSION_ID: &str = "sessionId"; - -/// How a POST to the endpoint is to be read. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum PostKind { - /// The stateless revision's request, carried exactly as it was before sessions existed. - Stateless, - /// `initialize`, opening a session. - Initialize, - /// A message inside a session named by the session header. - InSession, - /// A `2024-11-05` message POSTed to the address an event stream named. - EventStreamMessage, -} - -/// Classifies a POST. The stateless revision is recognised by its own marker (`_meta` carrying the -/// protocol version) and wins over everything else, and a request with neither a session nor an -/// `initialize` keeps the stateless reading too, so the stateless path's answers are unchanged. -#[must_use] -pub fn classify_post( - body: &Value, - session_header: Option<&str>, - message_session: Option<&str>, -) -> PostKind { - if message_session.is_some() { - return PostKind::EventStreamMessage; - } - let stateless_marker = body - .get("params") - .and_then(|p| p.get("_meta")) - .and_then(|m| m.get(META_PROTOCOL_VERSION)) - .is_some(); - if stateless_marker { - return PostKind::Stateless; - } - if session_header.is_some() { - return PostKind::InSession; - } - if body.get("method").and_then(Value::as_str) == Some(METHOD_INITIALIZE) { - return PostKind::Initialize; - } - PostKind::Stateless -} - -/// Reads the session a `2024-11-05` message address carries, from a raw query string. -#[must_use] -pub fn message_session_of(query: Option<&str>) -> Option<&str> { - query? - .split('&') - .filter_map(|pair| pair.split_once('=')) - .find(|(k, _)| *k == QUERY_SESSION_ID) - .map(|(_, v)| v) - .filter(|v| !v.is_empty()) -} - -/// What a session message is, for the dispatch. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SessionMethod { - /// A method the one dispatch answers, after [`raise`]. - Dispatch, - /// `ping`, answered here with an empty result. - Ping, - /// A verb of the session's own state: answered from it, never dispatched. - Session, - /// A notification or a response from the client: accepted (202) and not answered. - Accept, - /// Not a method of the session revisions: `-32601`. - NotFound, -} - -/// The methods a session client reaches through the dispatch. -const SESSION_DISPATCHED: &[&str] = &[ - "tools/list", - "tools/call", - "prompts/list", - "prompts/get", - "resources/list", - "resources/templates/list", - "resources/read", - "completion/complete", -]; - -/// The verbs a session's own state answers (subscribe stays for the old revisions). -const SESSION_STATE_VERBS: &[&str] = &[ - "resources/subscribe", - "resources/unsubscribe", - "logging/setLevel", -]; - -/// Classifies one session message. `method` is `None` for a client's response to a request. -#[must_use] -pub fn session_method(method: Option<&str>, has_id: bool) -> SessionMethod { - match method { - None => SessionMethod::Accept, - Some(_) if !has_id => SessionMethod::Accept, - Some(METHOD_PING) => SessionMethod::Ping, - Some(m) if SESSION_DISPATCHED.contains(&m) => SessionMethod::Dispatch, - Some(m) if SESSION_STATE_VERBS.contains(&m) => SessionMethod::Session, - Some(_) => SessionMethod::NotFound, - } -} - -/// The header values a raised request must carry, for the caller to mirror. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Mirror { - /// `MCP-Protocol-Version`. - pub version: &'static str, - /// `Mcp-Method`. - pub method: String, - /// `Mcp-Name`, raw (the caller applies the header sentinel where a name needs it). - pub name: Option, -} - -/// RAISES a session request into the stateless shape: `params._meta` gains the stateless revision -/// and an EMPTY client-capability declaration, and the header mirror is returned. -/// -/// The capabilities are empty on purpose. A session client declared its capabilities in the -/// session revision's own vocabulary, and the stateless dispatch would read `elicitation` or -/// `sampling` there as consent to answer with an input-required result, a shape no session client -/// can receive. Declaring nothing keeps every answer a complete one. -/// -/// `None` when the message has no method or its `params` is present and not an object. -#[must_use] -pub fn raise(message: &mut Value) -> Option { - let method = message.get("method")?.as_str()?.to_string(); - let obj = message.as_object_mut()?; - let params = obj - .entry("params") - .or_insert_with(|| Value::Object(Map::new())); - let params = params.as_object_mut()?; - let name = crate::codec::name_source_of(&method) - .and_then(|k| params.get(k)) - .and_then(Value::as_str) - .map(str::to_string); - let meta = params - .entry("_meta") - .or_insert_with(|| Value::Object(Map::new())); - let meta = meta.as_object_mut()?; - meta.insert(META_PROTOCOL_VERSION.into(), PROTOCOL_VERSION.into()); - meta.insert(META_CLIENT_CAPABILITIES.into(), Value::Object(Map::new())); - Some(Mirror { - version: PROTOCOL_VERSION, - method, - name, - }) -} - -/// THE CUSTOM PARAMETER MIRROR a raised `tools/call` must carry: for every property of the tool's -/// `inputSchema` that names an `x-mcp-header` suffix and whose argument is a string, the pair -/// (`mcp-param-`, the value in the name sentinel where it needs one). -/// -/// A session client never sends these (its revision does not define them), and the one dispatch -/// refuses a `tools/call` whose annotated argument has no mirror. The mirror is built from the -/// client's own arguments, so it says exactly what the body says. -#[must_use] -pub fn param_mirror( - input_schema: Option<&Value>, - arguments: Option<&Value>, -) -> Vec<(String, String)> { - let Some(props) = input_schema - .and_then(|s| s.get("properties")) - .and_then(Value::as_object) - else { - return Vec::new(); - }; - props - .iter() - .filter_map(|(property, definition)| { - let suffix = definition.get("x-mcp-header")?.as_str()?; - let value = arguments?.get(property)?.as_str()?; - Some(( - format!("mcp-param-{suffix}"), - crate::client::jsonrpc::encode_sentinel(value), - )) - }) - .collect() -} - -/// The stateless revision's result members no session revision defines. -const STATELESS_ONLY_MEMBERS: &[&str] = &["resultType", "cacheScope", "ttlMs"]; - -/// A result the session revisions cannot express (an input-required or task answer). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct NotExpressible; - -/// LOWERS a stateless result into a session revision's shape. -/// -/// # Errors -/// [`NotExpressible`] when the result is not a complete one. -pub fn lower_result(result: &mut Value) -> Result<(), NotExpressible> { - let Some(obj) = result.as_object_mut() else { - return Ok(()); - }; - match obj.get("resultType").and_then(Value::as_str) { - None | Some("complete") => {} - Some(_) => return Err(NotExpressible), - } - for k in STATELESS_ONLY_MEMBERS { - obj.remove(*k); - } - Ok(()) -} - -/// Builds the `initialize` result from the stateless discovery document, for `revision`. -/// -/// Only the capability groups the session revisions define are carried, and only when discovery -/// declared them for this caller: `tools`, `prompts`, `resources` and (from `2025-06-18`) -/// `completions`. `listChanged` is `list_changed` for all three lists, the caller's statement of -/// whether it delivers those notifications on the session's stream. Resource subscription and -/// `logging` are declared on every session revision: subscribe stays for the old revisions, and the -/// session's own state answers both (an upstream's `notifications/resources/updated` reaches the -/// session's stream, and its `notifications/message` past the session's floor). -#[must_use] -pub fn initialize_result(discovery: &Value, revision: Revision, list_changed: bool) -> Value { - let declared = discovery.get("capabilities"); - let has = |k: &str| declared.and_then(|c| c.get(k)).is_some(); - let mut caps = Map::new(); - for group in ["tools", "prompts", "resources"] { - if has(group) { - caps.insert( - group.into(), - serde_json::json!({ "listChanged": list_changed }), - ); - } - } - if let Some(resources) = caps.get_mut("resources").and_then(Value::as_object_mut) { - resources.insert("subscribe".into(), Value::Bool(true)); - } - if has("logging") { - caps.insert("logging".into(), Value::Object(Map::new())); - } - if has("completions") && revision != Revision::R2024_11_05 { - caps.insert("completions".into(), Value::Object(Map::new())); - } - let server_info = discovery - .get("serverInfo") - .cloned() - .unwrap_or_else(|| serde_json::json!({ "name": "busbar", "version": "" })); - serde_json::json!({ - "protocolVersion": revision.wire(), - "capabilities": caps, - "serverInfo": server_info, - }) -} - -/// One event-stream event. `data` is written one `data:` line per line it holds, so a multi-line -/// payload survives framing. -#[must_use] -pub fn frame(id: Option<&str>, event: Option<&str>, data: &str) -> String { - let mut out = String::with_capacity(data.len() + 48); - if let Some(e) = event { - out.push_str("event: "); - out.push_str(e); - out.push('\n'); - } - if let Some(i) = id { - out.push_str("id: "); - out.push_str(i); - out.push('\n'); - } - for line in data.split('\n') { - out.push_str("data: "); - out.push_str(line.strip_suffix('\r').unwrap_or(line)); - out.push('\n'); - } - out.push('\n'); - out -} - -/// The `2024-11-05` stream's first event: the address, relative to the endpoint's own origin, that -/// messages for this session are POSTed to. -#[must_use] -pub fn endpoint_event(mount_path: &str, session: &str) -> String { - frame( - None, - Some("endpoint"), - &format!("{mount_path}?{QUERY_SESSION_ID}={session}"), - ) -} - -/// The event name a `2024-11-05` stream carries JSON-RPC messages under. -pub const EVENT_MESSAGE: &str = "message"; - -#[cfg(test)] -#[path = "tests/adapt_tests.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/answer.rs b/crates/busbar-plane-mcp/src/answer.rs deleted file mode 100644 index 346bb434cb..0000000000 --- a/crates/busbar-plane-mcp/src/answer.rs +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE ANSWERS THE PLANE GIVES FROM WHAT IT HOLDS: every request whose answer is the generation's -//! catalogue or the section's own content, and that reaches no far end. Pure: the arrival's -//! [`Disposition`], the generation's [`Catalogue`] and the caller's `admit` predicate in; the status -//! and the answer's bytes out, or [`Answer::Far`] for a request that is not answered here. -//! -//! The answers are the served engine's, byte for byte, including its refusals: a local answer that -//! refuses answers its own refusal body with its own status. - -use serde_json::{json, Value}; - -use crate::catalogue::{complete, Catalogue, CACHE_SCOPE, CACHE_TTL_MS}; -use crate::checks::SUPPORTED_PROTOCOL_VERSIONS; -use crate::codec::{IMPLEMENTED_METHODS, PROTOCOL_VERSION}; -use crate::reads; -use crate::tool_arrival::{Disposition, Refusal}; -use crate::tool_ops::{ - OP_COMPLETION, OP_DISCOVER, OP_PROMPTS_LIST, OP_PROMPT_GET, OP_RESOURCES_LIST, - OP_RESOURCE_READ, OP_RESOURCE_TEMPLATES_LIST, OP_TASK_CANCEL, OP_TASK_GET, OP_TASK_UPDATE, - OP_TOOLS_LIST, -}; - -/// The status of a successful answer. -pub const STATUS_OK: u32 = 200; - -/// The status a notification is acknowledged with on a request-response carrier: no body. -pub const STATUS_ACCEPTED: u32 = 202; - -/// The tasks extension this server advertises unconditionally. -pub const TASKS_EXTENSION_ID: &str = "io.modelcontextprotocol/tasks"; - -/// What one arrival is answered with. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Answer { - /// Answered here: the status and the body (empty for none). - Here { - /// The status. - status: u32, - /// The body. - body: Vec, - }, - /// Not answered here: the request reaches a far end, or asks its caller first. - Far, -} - -impl Answer { - fn ok(body: Vec) -> Self { - Answer::Here { - status: STATUS_OK, - body, - } - } - - fn refused(refusal: &Refusal) -> Self { - Answer::Here { - status: refusal.status, - body: refusal.body(), - } - } - - fn of(result: Result, Refusal>) -> Self { - match result { - Ok(body) => Answer::ok(body), - Err(refusal) => Answer::refused(&refusal), - } - } -} - -/// `server/discover`: the capabilities, and the catalogue this caller reaches, counted. -#[must_use] -pub fn discover(catalogue: &Catalogue, id: &Value, admit: &impl Fn(&str, &str) -> bool) -> Vec { - let tools = catalogue.tools_for(admit); - let prompts = catalogue.prompts_for(admit); - let resources = catalogue.resources_for(admit); - let mut servers: Vec<&str> = tools - .iter() - .map(|t| t.server.as_str()) - .chain(prompts.iter().map(|p| p.server.as_str())) - .chain(resources.iter().map(|r| r.server.as_str())) - .collect(); - servers.sort_unstable(); - servers.dedup(); - complete( - id, - json!({ - "protocolVersion": PROTOCOL_VERSION, - "supportedVersions": SUPPORTED_PROTOCOL_VERSIONS, - "serverInfo": { "name": "busbar", "version": crate::tool_door::VERSION }, - "capabilities": { - "tools": { "listChanged": true }, - "prompts": { "listChanged": true }, - "resources": { "listChanged": true, "subscribe": true }, - "completions": {}, - "logging": {}, - "extensions": { TASKS_EXTENSION_ID: {} }, - }, - "methods": IMPLEMENTED_METHODS, - "servers": servers, - "counts": { - "tools": tools.len(), - "prompts": prompts.len(), - "resources": resources.len(), - }, - "registryEmpty": catalogue.is_empty(), - "cacheScope": CACHE_SCOPE, - "ttlMs": CACHE_TTL_MS, - }), - ) -} - -/// `completion/complete`: always the empty completion, never validated against the catalogue. -#[must_use] -pub fn completion(id: &Value) -> Vec { - complete( - id, - json!({ "completion": { "values": [], "hasMore": false, "total": 0 } }), - ) -} - -/// ANSWER ONE ARRIVAL from what the plane holds. `params` is the request body's `params`; -/// `hidden` is the kernel's trust verdict for a listed tool: `true` hides it. -pub fn answer( - disposition: &Disposition, - params: Option<&Value>, - catalogue: &Catalogue, - admit: &impl Fn(&str, &str) -> bool, - hidden: impl Fn(&crate::catalogue::ToolEntry) -> bool, -) -> Answer { - match disposition { - Disposition::Refused(refusal) => Answer::refused(refusal), - Disposition::Notice { .. } => Answer::Here { - status: STATUS_ACCEPTED, - body: Vec::new(), - }, - Disposition::Request { row, id, .. } => { - let op = row.op; - if op == OP_TOOLS_LIST { - Answer::ok(catalogue.tools_list(id, admit, hidden)) - } else if op == OP_PROMPTS_LIST { - Answer::ok(catalogue.prompts_list(id, admit)) - } else if op == OP_RESOURCES_LIST { - Answer::ok(catalogue.resources_list(id, admit)) - } else if op == OP_RESOURCE_TEMPLATES_LIST { - Answer::ok(catalogue.resource_templates_list(id, admit)) - } else if op == OP_RESOURCE_READ { - Answer::of(reads::resources_read(catalogue, id, params, admit)) - } else if op == OP_PROMPT_GET { - // A prompt that asks its caller first, or a retry presenting answers or state, - // takes busbar's ask decision before it renders: not answered here. - let retried = params.is_some_and(|p| { - p.get("inputResponses").is_some() || p.get("requestState").is_some() - }); - match reads::prompt_named(catalogue, id, params, admit) { - Err(refusal) => Answer::refused(&refusal), - Ok(prompt) if !prompt.ask_caller.is_empty() || retried => Answer::Far, - Ok(prompt) => Answer::ok(reads::prompts_get(prompt, id, params)), - } - } else if op == OP_TASK_GET || op == OP_TASK_UPDATE || op == OP_TASK_CANCEL { - Answer::refused(&crate::call::task_verb(id, params)) - } else if op == OP_COMPLETION { - Answer::ok(completion(id)) - } else if op == OP_DISCOVER { - Answer::ok(discover(catalogue, id, admit)) - } else { - Answer::Far - } - } - } -} - -#[cfg(test)] -#[path = "tests/answer.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/argguard.rs b/crates/busbar-plane-mcp/src/argguard.rs deleted file mode 100644 index 2b28ba21d4..0000000000 --- a/crates/busbar-plane-mcp/src/argguard.rs +++ /dev/null @@ -1,549 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE ARGUMENT HALF OF THE DISPATCH SSRF GUARD: a schema-aware walk of the nested per-request -//! `tools/call` arguments, finding every URL and every host they carry and asking the host's ONE -//! destination judge (`dest.judge`, BUSBAR-1.6.0.md Appendix C B.3 item 11) about each. The plane -//! reads the value; the deployment's egress rules decide it. -//! -//! ## The gap this closes, stated as the live consequence -//! -//! The transport guard closes the URL/host/address/redirect half: busbar resolves the UPSTREAM's own URL -//! once, judges every address the resolver returned, and connects to the address that was judged. -//! Routing is likewise immune to attacker text, because a route is decided on the bound identity -//! and `super::dispatch` provably reads no free text at all. -//! -//! Neither of those makes the PAYLOAD safe. A model that has just read a hostile tool description -//! can still put `http://169.254.169.254/latest/meta-data/…` into a tool ARGUMENT, and without this -//! module busbar hands that argument to the upstream untouched. An argument is attacker-influenced -//! DATA travelling to an operator-chosen destination, which is the classic SSRF shape and a -//! different shape from the one the routing rule answers. -//! -//! ## Why the PINNED schema is what makes this a check rather than a guess -//! -//! `super::catalogue` already fetches, canonicalises and hash-pins each tool's input schema, and a -//! schema that changed to hide or reveal a URL field is DRIFT, which quarantines the server before -//! any call goes out. So the schema this walk reads is not "whatever the upstream said this -//! morning" — it is the exact document whose digest an operator approved. [`super::dispatch`] -//! re-compares that digest immediately before handing the schema here, so the walk cannot be -//! steered by a schema swapped underneath it. -//! -//! ## Two detectors, and why there are two -//! -//! 1. **DECLARED.** A string whose schema declares a URL-ish `format` — `uri`, `uri-reference`, -//! `iri`, `url`, `hostname`, `idn-hostname`, `ipv4`, `ipv6` — is judged in full: scheme -//! allowlist for the URL kinds, host judgement for all of them. Nested objects, arrays, tuple -//! positions, `additionalProperties` and `allOf`/`anyOf`/`oneOf` branches are all followed, so -//! "several levels deep" is not a special case. -//! -//! 2. **UNDECLARED.** Most real MCP tools take a URL in a plain `{"type": "string"}` with no -//! `format` at all. A walk that judged only declared fields would therefore refuse almost -//! nothing while reporting green, so every string in the arguments is ALSO judged when the whole -//! value (after trimming) begins `http://` or `https://`. That prefix has exactly one reading: -//! it is a URL. Nothing narrower is sniffed — a bare `localhost` in an untyped `query` field -//! stays untouched, because refusing a hostname-shaped word in free text breaks real tools and -//! buys nothing an attacker cannot restate. -//! -//! The counts of both are RETURNED ([`ArgScan`]) rather than kept internal, because a walker that -//! visits nothing refuses nothing and passes every test it is given. A caller — and the tests — -//! can assert the walk actually reached fields. -//! -//! ## What this module deliberately does NOT do -//! -//! - **No host rules of its own.** The scheme allowlist and the host reader are the walk's; every -//! verdict on a host is `dest.judge`'s, under the deployment's egress rules (its allow-list, the -//! metadata hosts, the private-address setting), so an argument and the connector's dial are -//! judged by the same guard. -//! - **No DNS.** The transport guard resolves and PINS because busbar is the party that connects. -//! For an argument busbar is NOT the connecting party: the upstream resolves the name itself, -//! later, from its own resolver. A lookup here would therefore be advisory at best — trivially -//! defeated by rebinding, since nothing binds our answer to the upstream's connect — while -//! costing a per-argument lookup on the dispatch path and turning busbar into a name-resolution -//! oracle for whatever a model types. So the judgement is decided from the string alone, and the -//! honest consequence is stated rather than softened: a hostname whose A record points at -//! loopback is NOT caught here. -//! - **No `$ref` resolution.** A `$ref` is followed nowhere; a URL field reachable only through one -//! is not visited. -//! - **No mid-string URLs.** A URL embedded inside a longer free-text value in an UNDECLARED field -//! is not detected. Substring matching over prose is how a guard acquires false positives, and a -//! false positive here refuses a legitimate call. -//! - **No plaintext-to-public rule.** `super::ssrf` refuses `http` to a public host because -//! busbar's own upstream credential rides that request. No busbar credential rides a tool -//! argument, so the rule does not transfer and is not applied. - -use busbar_contract::abi::host::service::{ - DEST_ALLOWED, DEST_INTERNAL, DEST_METADATA, DEST_NO_HOST, DEST_OBFUSCATED, -}; -use busbar_contract::net::{extract_normalized_host, scheme_is}; -use serde_json::Value; - -/// THE HOST'S JUDGE, as the walk asks it: one host an argument names (an IPv6 literal bracketed, -/// as `dest.judge` reads a `host[:port]`), answered with the host's `DEST_*` verdict, or `None` -/// when the host gave none (the value is then refused: fail closed). -pub type Judge<'a> = dyn FnMut(&str) -> Option + 'a; - -/// How deep into the ARGUMENT value the walk goes before refusing. Arguments arrive as already -/// parsed JSON, so this is a floor under stack safety rather than the primary bound; exceeding it -/// is a refusal instead of a truncation, because a walk that silently stops is a walk that reports -/// "nothing found" on the one document built to make it stop. -const MAX_VALUE_DEPTH: usize = 64; - -/// How far the SCHEMA-side expansion follows `allOf`/`anyOf`/`oneOf` nesting. Bounded separately -/// from the value depth because a pinned schema can nest combinators independently of how deep the -/// arguments go. -const MAX_COMBINATOR_DEPTH: usize = 8; - -/// The `format` values that make a string field URL-ish. Matched case-insensitively: JSON Schema -/// spells these lowercase, and an upstream writing `URI` should not thereby remove a field from the -/// walk. -const URLISH_FORMATS: &[&str] = &[ - "uri", - "uri-reference", - "iri", - "url", - "hostname", - "idn-hostname", - "ipv4", - "ipv6", -]; - -/// What a URL-ish format says the value IS, which decides how it is read. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Urlish { - /// An absolute URL. A non-`http(s)` scheme is refused; `file:`, `gopher:` and friends are - /// refused by absence rather than by a list somebody has to maintain. - AbsoluteUrl, - /// A URI reference, which may legitimately be relative. A relative reference names no host and - /// is admissible; a scheme-relative `//host/path` names one and is judged. - Reference, - /// A bare host: `hostname`, `idn-hostname`, `ipv4`, `ipv6`. - Host, -} - -/// Why one argument value was refused. Each arm names the offending value, because a refusal an -/// operator cannot diagnose is a refusal an operator disables. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ArgWhy { - /// An absolute-URL field carrying something that is not `http(s)`. - Scheme(String), - /// The value has no host component to judge. - NoHost(String), - /// `dest.judge` answered `DEST_METADATA`: a cloud-metadata endpoint, by name, address or the - /// deployment's own list. Refused unconditionally, `allow_private` or not. - CloudMetadata(String), - /// `dest.judge` answered `DEST_OBFUSCATED`: an alternate IPv4 encoding (`2130706433`, - /// `0x7f000001`, `127.1`) that a resolver expands but a canonical IP-literal check misses. - ObfuscatedHost(String), - /// `dest.judge` answered `DEST_INTERNAL`: an internal host the egress rules do not admit here. - InternalHost(String), - /// `dest.judge` refused the host with another `DEST_*` verdict. - Refused(String, u64), - /// The host gave no verdict (it serves no `dest.judge`, or the judgement failed). - Unjudged(String), - /// The argument nested deeper than the walk will follow. - DepthExceeded, -} - -impl std::fmt::Display for ArgWhy { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ArgWhy::Scheme(v) => write!( - f, - "`{v}` is not an http(s) URL, and no other scheme is passed through in a tool \ - argument" - ), - ArgWhy::NoHost(v) => write!(f, "`{v}` has no host to check"), - ArgWhy::CloudMetadata(h) => write!( - f, - "`{h}` is a cloud-metadata endpoint; that is refused unconditionally, because an \ - operator opting a server into private addressing has said nothing about the \ - service whose whole value to an attacker is that it hands out credentials" - ), - ArgWhy::ObfuscatedHost(h) => write!( - f, - "`{h}` is an alternate IPv4 encoding a resolver expands; write the address in \ - dotted-quad form so it can be checked" - ), - ArgWhy::InternalHost(h) => write!( - f, - "`{h}` is an internal address; set this server's `allow_private` if reaching \ - internal hosts through its tools is deliberate (the deployment's destination \ - rules still apply)" - ), - ArgWhy::Refused(h, verdict) => write!( - f, - "`{h}` is refused by the deployment's destination rules (verdict {verdict})" - ), - ArgWhy::Unjudged(h) => write!( - f, - "`{h}` could not be checked against the deployment's destination rules, so it is \ - refused" - ), - ArgWhy::DepthExceeded => write!( - f, - "the tool arguments nest deeper than {MAX_VALUE_DEPTH} levels, which is refused \ - rather than walked partially" - ), - } - } -} - -/// ONE REFUSED ARGUMENT, located precisely. The pointer is an RFC 6901 JSON Pointer into the -/// arguments, so an operator reading the refusal is told WHICH field of a nested document was the -/// problem rather than being handed the document back. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ArgRefusal { - /// RFC 6901 pointer, e.g. `/delivery/callbacks/1`. - pub pointer: String, - /// The schema-declared format that put this field in scope, or `None` when the field was - /// caught by the undeclared `http(s)://` detector. - pub declared_format: Option, - /// Why the value was refused. - pub why: ArgWhy, -} - -impl std::fmt::Display for ArgRefusal { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let at = if self.pointer.is_empty() { - "the argument" - } else { - self.pointer.as_str() - }; - match &self.declared_format { - Some(fmt) => write!( - f, - "tool argument {at} (schema format `{fmt}`) was refused: {}", - self.why - ), - None => write!( - f, - "tool argument {at} carries a URL the schema does not declare, and it was refused: \ - {}", - self.why - ), - } - } -} - -/// WHAT THE WALK ACTUALLY VISITED. Returned on success so a caller can assert a non-zero floor: a -/// walker that finds nothing refuses nothing and passes every test, which is the false-green shape -/// this check exists to avoid being. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct ArgScan { - /// String values judged because the pinned schema declared a URL-ish format for them. - pub declared_judged: usize, - /// String values judged because the value itself began `http://` or `https://` while the - /// schema declared no URL-ish format there. - pub undeclared_judged: usize, - /// Every string leaf the walk reached, judged or not. The denominator that makes the two - /// numerators above readable. - pub strings_seen: usize, -} - -impl ArgScan { - /// Total values that were actually put through the guard. - /// Not read on the dispatch path: `mcp::upstream` acts on the REFUSAL, and the counts exist - /// for the floor assertions that stop a walker which visits nothing from passing. - #[allow(dead_code)] - pub fn judged(&self) -> usize { - self.declared_judged + self.undeclared_judged - } -} - -/// THE ENTRY POINT: walk `arguments` against the PINNED `schema` and refuse on the first -/// inadmissible value. -/// -/// Fails on the first refusal rather than collecting all of them, because the call is refused -/// either way and the operator's question is "why was this refused", which one named field answers. -/// Every host found is asked of `judge` ([`Judge`]). -pub fn guard( - schema: &Value, - arguments: &Value, - mut judge: impl FnMut(&str) -> Option, -) -> Result { - let mut scan = ArgScan::default(); - let mut pointer = String::new(); - walk(&[schema], arguments, &mut pointer, 0, &mut judge, &mut scan)?; - Ok(scan) -} - -/// The value-driven walk. The VALUE drives and the schema rides alongside, which is what lets the -/// undeclared detector see fields the schema never mentioned — a schema-driven walk cannot, by -/// construction, visit a field the schema omits. -fn walk( - schemas: &[&Value], - value: &Value, - pointer: &mut String, - depth: usize, - judge: &mut Judge<'_>, - scan: &mut ArgScan, -) -> Result<(), ArgRefusal> { - if depth > MAX_VALUE_DEPTH { - return Err(ArgRefusal { - pointer: pointer.clone(), - declared_format: None, - why: ArgWhy::DepthExceeded, - }); - } - match value { - Value::Object(map) => { - for (k, v) in map { - let mark = pointer.len(); - pointer.push('/'); - pointer.push_str(&escape_token(k)); - let child = child_for_key(schemas, k); - let child_refs: Vec<&Value> = child; - walk(&child_refs, v, pointer, depth + 1, judge, scan)?; - pointer.truncate(mark); - } - Ok(()) - } - Value::Array(items) => { - for (i, v) in items.iter().enumerate() { - let mark = pointer.len(); - pointer.push('/'); - pointer.push_str(&i.to_string()); - let child = child_for_index(schemas, i); - let child_refs: Vec<&Value> = child; - walk(&child_refs, v, pointer, depth + 1, judge, scan)?; - pointer.truncate(mark); - } - Ok(()) - } - Value::String(s) => judge_string(schemas, s, pointer, judge, scan), - _ => Ok(()), - } -} - -/// One string leaf: declared first, then the undeclared `http(s)://` detector. -fn judge_string( - schemas: &[&Value], - s: &str, - pointer: &str, - judge: &mut Judge<'_>, - scan: &mut ArgScan, -) -> Result<(), ArgRefusal> { - scan.strings_seen += 1; - let trimmed = s.trim(); - if let Some((format, kind)) = first_urlish(schemas) { - scan.declared_judged += 1; - return judge_argument(kind, trimmed, judge).map_err(|why| ArgRefusal { - pointer: pointer.to_string(), - declared_format: Some(format), - why, - }); - } - if starts_with_http(trimmed) { - scan.undeclared_judged += 1; - return judge_argument(Urlish::AbsoluteUrl, trimmed, judge).map_err(|why| ArgRefusal { - pointer: pointer.to_string(), - declared_format: None, - why, - }); - } - Ok(()) -} - -/// Whether the WHOLE value is an `http(s)` URL. Deliberately a prefix test on the whole trimmed -/// string rather than a search: see the module header on why substring matching over free text is -/// not wanted here. -fn starts_with_http(v: &str) -> bool { - let lower = v.to_ascii_lowercase(); - lower.starts_with("http://") || lower.starts_with("https://") -} - -fn judge_argument(kind: Urlish, value: &str, judge: &mut Judge<'_>) -> Result<(), ArgWhy> { - match kind { - Urlish::AbsoluteUrl => judge_absolute(value, judge), - Urlish::Reference => { - // A scheme-relative reference (`//169.254.169.254/x`) inherits the base scheme and - // names a host, so it is judged as one. A path-relative reference names no host and - // there is nothing to judge. - if let Some(rest) = value.strip_prefix("//") { - let authority = rest.split(['/', '?', '#']).next().unwrap_or(rest); - let host = authority.rsplit('@').next().unwrap_or(authority); - return judge_host(strip_port(host), judge); - } - if value.contains("://") { - return judge_absolute(value, judge); - } - Ok(()) - } - Urlish::Host => { - // A `hostname`/`ipv4`/`ipv6`-declared field is supposed to carry a bare host, but - // nothing stops a caller writing a full absolute URL into it instead. Left to - // `judge_host` as-is, a value like `https://169.254.169.254/x` is not a syntactically - // valid host, so the judge (reading it as an opaque host string) would miss the - // address entirely — the scheme and path are noise to it, not a signal to strip. Any embedded `://` means this is actually a URL wearing - // a `hostname` declaration, so it is judged as one (scheme allowlist + host judgement - // on the REAL host), the same authority the `Reference` arm above already gives a - // scheme-relative value. - if value.contains("://") { - return judge_absolute(value, judge); - } - judge_host(value, judge) - } - } -} - -/// An absolute URL: the scheme allowlist, then the host. -fn judge_absolute(url: &str, judge: &mut Judge<'_>) -> Result<(), ArgWhy> { - if !scheme_is(url, "http") && !scheme_is(url, "https") { - return Err(ArgWhy::Scheme(url.to_string())); - } - // `extract_normalized_host` is the tree's strictest host reader: it mirrors the WHATWG - // tab/newline strip, the backslash fold, the userinfo drop, the trailing-root-dot strip and the - // percent-decode that a connecting stack applies. Re-deriving any of that here would be a - // second copy of a guard that already exists. - let host = extract_normalized_host(url).ok_or_else(|| ArgWhy::NoHost(url.to_string()))?; - judge_host(&host, judge) -} - -/// THE HOST, ASKED OF THE HOST'S ONE JUDGE (`dest.judge`, BUSBAR-1.6.0.md Appendix C B.3 item 11): -/// the plane reads the host and holds no rule about it. `judge`'s verdict is rendered as the -/// refusal; a host that gave none refuses the value. -fn judge_host(raw: &str, judge: &mut Judge<'_>) -> Result<(), ArgWhy> { - let host = normalize_host(raw).ok_or_else(|| ArgWhy::NoHost(raw.to_string()))?; - let dest = if host.contains(':') { - format!("[{host}]") - } else { - host.clone() - }; - match judge(&dest) { - Some(DEST_ALLOWED) => Ok(()), - Some(DEST_METADATA) => Err(ArgWhy::CloudMetadata(host)), - Some(DEST_OBFUSCATED) => Err(ArgWhy::ObfuscatedHost(host)), - Some(DEST_INTERNAL) => Err(ArgWhy::InternalHost(host)), - Some(DEST_NO_HOST) => Err(ArgWhy::NoHost(host)), - Some(verdict) => Err(ArgWhy::Refused(host, verdict)), - None => Err(ArgWhy::Unjudged(host)), - } -} - -/// Normalize a bare host the same way a URL's host component is normalized, by routing it through -/// the same reader: an IPv6 literal is bracketed so the reader sees an authority rather than a -/// host:port, and everything else is passed as written. -fn normalize_host(raw: &str) -> Option { - extract_normalized_host(&probe_url(raw)) -} - -fn probe_url(host: &str) -> String { - if busbar_contract::net::host_ip(host).is_some_and(|ip| ip.is_ipv6()) { - format!("https://[{host}]/") - } else { - format!("https://{host}/") - } -} - -/// Drop a `:port` suffix from an authority, leaving a bracketed IPv6 literal intact for -/// [`normalize_host`] to unwrap. -fn strip_port(authority: &str) -> &str { - if authority.starts_with('[') { - return authority; - } - match authority.rsplit_once(':') { - Some((left, _)) if !left.contains(':') => left, - _ => authority, - } -} - -/// RFC 6901 token escaping: `~` becomes `~0` and `/` becomes `~1`, in that order, so a key -/// containing either cannot forge a different pointer than the one it is at. -fn escape_token(key: &str) -> String { - key.replace('~', "~0").replace('/', "~1") -} - -/// The first URL-ish `format` declared for this position, in declaration order across the expanded -/// schema nodes. -fn first_urlish(schemas: &[&Value]) -> Option<(String, Urlish)> { - for node in expanded(schemas) { - let Some(format) = node.get("format").and_then(Value::as_str) else { - continue; - }; - let lower = format.to_ascii_lowercase(); - if !URLISH_FORMATS.contains(&lower.as_str()) { - continue; - } - let kind = match lower.as_str() { - "uri-reference" => Urlish::Reference, - "hostname" | "idn-hostname" | "ipv4" | "ipv6" => Urlish::Host, - _ => Urlish::AbsoluteUrl, - }; - return Some((format.to_string(), kind)); - } - None -} - -/// A schema node plus every `allOf`/`anyOf`/`oneOf` branch reachable from it, so a URL-ish format -/// hidden inside a combinator is still found. -fn expanded<'a>(schemas: &[&'a Value]) -> Vec<&'a Value> { - let mut out = Vec::new(); - expand_into(schemas, &mut out, MAX_COMBINATOR_DEPTH); - out -} - -fn expand_into<'a>(schemas: &[&'a Value], out: &mut Vec<&'a Value>, budget: usize) { - if budget == 0 { - return; - } - for node in schemas { - if !node.is_object() { - continue; - } - out.push(node); - for key in ["allOf", "anyOf", "oneOf"] { - if let Some(branches) = node.get(key).and_then(Value::as_array) { - let refs: Vec<&Value> = branches.iter().collect(); - expand_into(&refs, out, budget - 1); - } - } - } -} - -/// The schema(s) governing object member `key`: `properties` where it is named, otherwise the -/// object's `additionalProperties` schema where there is one. -fn child_for_key<'a>(schemas: &[&'a Value], key: &str) -> Vec<&'a Value> { - let mut out = Vec::new(); - for node in expanded(schemas) { - if let Some(named) = node - .get("properties") - .and_then(Value::as_object) - .and_then(|m| m.get(key)) - { - out.push(named); - } else if let Some(extra) = node.get("additionalProperties") { - if extra.is_object() { - out.push(extra); - } - } - } - out -} - -/// The schema(s) governing array element `index`: the tuple position where the schema gives one -/// (`prefixItems`, or the legacy array form of `items`), otherwise the single `items` schema. -fn child_for_index<'a>(schemas: &[&'a Value], index: usize) -> Vec<&'a Value> { - let mut out = Vec::new(); - for node in expanded(schemas) { - if let Some(tuple) = node.get("prefixItems").and_then(Value::as_array) { - if let Some(at) = tuple.get(index) { - out.push(at); - continue; - } - } - match node.get("items") { - Some(Value::Array(tuple)) => { - if let Some(at) = tuple.get(index) { - out.push(at); - } - } - Some(single) if single.is_object() => out.push(single), - _ => {} - } - } - out -} - -#[cfg(test)] -#[path = "tests/argguard_tests.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/ask.rs b/crates/busbar-plane-mcp/src/ask.rs deleted file mode 100644 index abd0e059df..0000000000 --- a/crates/busbar-plane-mcp/src/ask.rs +++ /dev/null @@ -1,795 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! BUSBAR'S OWN ASK (elicitation): an `InputRequiredResult` composed entirely from operator -//! configuration, filtered by the caller's declared capabilities and sealed in a `requestState` -//! busbar mints. Pure: the decision is the served engine's, in its order and words; the seal, the -//! nonce and the one-time redemption are a [`Seal`] the door hands in. -//! -//! An [`CallerAsk`] is authored by the operator: its one constructor takes an -//! [`AskEntryCfg`](crate::tools_config::AskEntryCfg), and its fields exist only inside the private module -//! that declares it, so nothing an upstream said can become one. -//! -//! If filtering by the caller's capabilities removes every ask in a round, the answer is a refusal -//! and never "proceed": proceeding would let a caller strip the operator's confirmation gate by -//! declaring nothing. - -use serde_json::{json, Map, Value}; - -use crate::tools_config::{AskRoundCfg, ASK_ELICITATION, ASK_ROOTS, ASK_SAMPLING}; - -pub use authored::CallerAsk; - -/// THE PRIVACY BOUNDARY: outside this module a [`CallerAsk`]'s fields do not exist, so the one way -/// to make one is [`CallerAsk::from_config`]. -mod authored { - use crate::tools_config::AskEntryCfg; - - /// One ask busbar makes of its caller: an entry of the `inputRequests` map. - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct CallerAsk { - key: String, - method: String, - params: serde_json::Value, - } - - impl CallerAsk { - /// THE ONLY CONSTRUCTOR: an operator-written map entry. - #[must_use] - pub fn from_config(key: &str, cfg: &AskEntryCfg) -> Self { - CallerAsk { - key: key.to_string(), - method: cfg.method.clone(), - params: cfg.params.clone().unwrap_or_else(|| serde_json::json!({})), - } - } - - /// The entry's key in `inputRequests`. - #[must_use] - pub fn key(&self) -> &str { - &self.key - } - - /// The client method asked for. - #[must_use] - pub fn method(&self) -> &str { - &self.method - } - - /// The request `params`, verbatim. - #[must_use] - pub fn params(&self) -> &serde_json::Value { - &self.params - } - } -} - -impl CallerAsk { - /// The client capability that gates this ask. - fn capability_key(&self) -> Option<&'static str> { - match self.method() { - ASK_ELICITATION => Some("elicitation"), - ASK_SAMPLING => Some("sampling"), - ASK_ROOTS => Some("roots"), - _ => None, - } - } -} - -/// How long a minted `requestState` stands, in seconds: a short replay window. -pub const DEFAULT_TTL_SECS: u64 = 300; - -/// The notification a caller announces its roots moved with: every state it was sealed under that -/// carries a roots answer stops verifying. -pub const NOTIFY_ROOTS_LIST_CHANGED: &str = "notifications/roots/list_changed"; - -/// The principal an ungoverned deployment's state is bound to: it has exactly one. -pub const UNGOVERNED: &str = ""; - -/// THE SEALED PAYLOAD: who, which request, which round, and when. -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct AskState { - /// The authenticated principal. - #[serde(rename = "p")] - pub principal: String, - /// `tools/call` or `prompts/get`. - #[serde(rename = "m")] - pub method: String, - /// The published capability name. - #[serde(rename = "c")] - pub capability: String, - /// The digest of the request's arguments. - #[serde(rename = "d")] - pub args_digest: String, - /// The catalogue generation it was minted under. - #[serde(rename = "g")] - pub generation: u64, - /// The round this state ends. - #[serde(rename = "r")] - pub round: u32, - /// Fresh randomness. - #[serde(rename = "n")] - pub nonce: String, - /// Unix seconds at mint. - #[serde(rename = "i")] - pub issued_at: u64, - /// Seconds of validity. - #[serde(rename = "t")] - pub ttl_secs: u64, - /// The principal's roots epoch at mint, when the exchange includes a roots ask. - #[serde(rename = "e", default, skip_serializing_if = "Option::is_none")] - pub roots_epoch: Option, - /// THE UPSTREAM'S ASK RELAYED TO THE CALLER (Law 11: busbar answers nothing on the caller's - /// behalf): the member that asked, and its own `requestState`, nested in this one sealed state. - /// Present only on a state minted for a relayed ask; busbar's own rounds were already answered. - #[serde(rename = "u", default, skip_serializing_if = "Option::is_none")] - pub upstream: Option, -} - -/// AN UPSTREAM'S ASK, RELAYED: the pool member that asked (the retry goes back to it), the state it -/// sealed for itself (handed back to it verbatim on the retry; `None` when it sealed none), and how -/// many of its rounds this call has relayed. -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct UpstreamLeg { - /// The registration that asked. - #[serde(rename = "m")] - pub member: String, - /// The upstream's own `requestState`, verbatim. - #[serde(rename = "s", default, skip_serializing_if = "Option::is_none")] - pub state: Option, - /// The upstream rounds relayed so far: the next request is round `round`. - #[serde(rename = "r")] - pub round: u32, - /// A STDIO CHILD'S OWN REQUESTS, relayed: present only when the ask was the child's JSON-RPC - /// request read while its call was in flight (not an `input_required` result). The retry - /// answers each on the child's input under the child's own id, then reads on for the call. - #[serde(rename = "c", default, skip_serializing_if = "Option::is_none")] - pub child: Option, -} - -/// THE CHILD'S SIDE OF A RELAYED ASK (spec Part 3 B.3 item 10: across separate HTTP arrivals the -/// ask is correlated with `work.*`): the child generation that asked (an answer is never written to -/// the next), the id of the call the child still owes an answer to, each `inputRequests` key the -/// caller was handed with the child's own request id, and the reference of the work handle the -/// exchange is correlated under. -#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct ChildLeg { - /// The generation of the child that asked. - #[serde(rename = "g")] - pub generation: u64, - /// The id of the call in flight on the child. - #[serde(rename = "w")] - pub wait: u64, - /// `(inputRequests key, the child's request id)`, in the order the child asked. - #[serde(rename = "i")] - pub asks: Vec<(String, Value)>, - /// The work handle's reference (`work.open`), once one is opened. - #[serde(rename = "h", default, skip_serializing_if = "Option::is_none")] - pub work: Option, -} - -/// Why a presented `requestState` was refused. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Rejected { - /// Not the form this plane writes. - Malformed, - /// The seal did not verify. - BadSignature, - /// Past its window. - Expired, - /// Sealed for another principal. - WrongPrincipal, - /// Sealed for another method, capability or argument set. - WrongRequest, - /// Sealed under a generation that is no longer live. - WrongGeneration, - /// Already redeemed. - AlreadySpent, - /// Sealed under a roots epoch the caller has since moved. - StaleRoots, -} - -impl Rejected { - /// The stable audit reason word. - #[must_use] - pub fn audit_reason(self) -> &'static str { - match self { - Rejected::Malformed => "state_malformed", - Rejected::BadSignature => "state_bad_signature", - Rejected::Expired => "state_expired", - Rejected::WrongPrincipal => "state_wrong_principal", - Rejected::WrongRequest => "state_wrong_request", - Rejected::WrongGeneration => "state_wrong_generation", - Rejected::AlreadySpent => "state_already_spent", - Rejected::StaleRoots => "state_stale_roots", - } - } -} - -impl std::fmt::Display for Rejected { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Rejected::StaleRoots => f.write_str( - "`requestState` was minted before you sent `notifications/roots/list_changed`, and \ - the exchange it continues includes a `roots/list` answer — which your notification \ - declared stale. Retry the request without `requestState` to restart the exchange \ - and answer with your current roots.", - ), - _ => f.write_str( - "`requestState` failed integrity verification and was refused. It is an opaque \ - value minted by this server for one caller, one request and a short window; it \ - cannot be modified, reused by another caller, replayed after it lapses, or \ - presented on a different request.", - ), - } - } -} - -impl AskState { - /// Whether this state was minted for this principal, request and generation, and is still - /// inside its window at `now`. - /// - /// # Errors - /// - /// The first check it fails. - pub fn matches( - &self, - principal: &str, - method: &str, - capability: &str, - args_digest: &str, - generation: u64, - now: u64, - ) -> Result<(), Rejected> { - if now > self.issued_at.saturating_add(self.ttl_secs) { - return Err(Rejected::Expired); - } - if self.principal != principal { - return Err(Rejected::WrongPrincipal); - } - if self.method != method || self.capability != capability || self.args_digest != args_digest - { - return Err(Rejected::WrongRequest); - } - if self.generation != generation { - return Err(Rejected::WrongGeneration); - } - Ok(()) - } -} - -/// THE SEAL THE DOOR HANDS IN: minting and opening the integrity-protected state, a fresh nonce, -/// and the one-time redemption of a completed exchange. The host's signing and one-time claim stand -/// behind it. -pub trait Seal { - /// The sealed form of `state`; `None` when it cannot be sealed. - fn mint(&mut self, state: &AskState) -> Option; - /// The state `blob` seals. - /// - /// # Errors - /// - /// [`Rejected::Malformed`] or [`Rejected::BadSignature`]. - fn open(&mut self, blob: &str) -> Result; - /// A fresh nonce; `None` when no randomness is reachable. - fn nonce(&mut self) -> Option; - /// Spend `nonce`, standing until `expires_at`, exactly once: `true` for the first spend. - fn redeem(&mut self, nonce: &str, expires_at: u64, now: u64) -> bool; -} - -/// What the ask decision answers. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum AskDecision { - /// No ask stands between the caller and the call. - Proceed, - /// Ask the caller. - Ask { - /// The round's asks the caller can answer. - asks: Vec, - /// The sealed state the retry presents. - request_state: String, - /// The round. - round: u32, - }, - /// Refused. - Refuse(AskRefusal), - /// THE RETRY OF A RELAYED UPSTREAM ASK: the caller's answers go to the member that asked, - /// with the upstream's own state, verbatim. Busbar's own rounds were answered before the call - /// went out, and this state is spent once. - Relayed(UpstreamLeg), -} - -/// Why the caller's side of the exchange was refused. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum AskRefusal { - /// The caller declares none of the capabilities the round needs. - NoDeclaredCapability { - /// The capability. - capability: String, - /// The round. - round: u32, - /// The capabilities it needs. - required: Vec<&'static str>, - }, - /// The round cap was reached. - RoundCapExceeded { - /// The capability. - capability: String, - /// The cap. - cap: u32, - }, - /// The presented state was refused. - StateRejected(Rejected), - /// State or answers presented where nothing was asked. - Unsolicited { - /// The capability. - capability: String, - }, - /// A retry answered nothing. - Unanswered { - /// The capability. - capability: String, - /// What was asked. - missing: String, - }, - /// No seal: the state cannot be protected. - NoSealer { - /// The capability. - capability: String, - }, - /// The host's clock could not be read: the state's window can be neither stamped nor judged. - ClockUnavailable { - /// The capability. - capability: String, - }, -} - -impl AskRefusal { - /// The stable audit reason word. - #[must_use] - pub fn audit_reason(&self) -> &'static str { - match self { - AskRefusal::NoDeclaredCapability { .. } => "ask_no_declared_capability", - AskRefusal::RoundCapExceeded { .. } => "ask_round_cap", - AskRefusal::StateRejected(r) => r.audit_reason(), - AskRefusal::Unsolicited { .. } => "ask_unsolicited_state", - AskRefusal::Unanswered { .. } => "ask_unanswered", - AskRefusal::NoSealer { .. } => "ask_no_sealer", - AskRefusal::ClockUnavailable { .. } => "ask_clock_unavailable", - } - } - - /// The refusal as the caller is answered: `400` `-32602` for refused state, `400` `-32021` - /// naming the capabilities for an undeclared one, `403` `-32000` otherwise. - #[must_use] - pub fn refusal(&self, id: &Value) -> crate::tool_arrival::Refusal { - let reason = json!({ "reason": self.audit_reason() }); - let (status, code, data) = match self { - AskRefusal::StateRejected(_) => (400, crate::codec::CODE_INVALID_PARAMS, reason), - AskRefusal::NoDeclaredCapability { required, .. } => { - let caps: Map = required - .iter() - .map(|k| ((*k).to_string(), json!({}))) - .collect(); - ( - 400, - crate::codec::CODE_MISSING_CLIENT_CAPABILITY, - json!({ "reason": self.audit_reason(), "requiredCapabilities": caps }), - ) - } - _ => (403, crate::codec::CODE_REFUSED, reason), - }; - crate::tool_arrival::Refusal { - status, - id: Some(id.clone()), - code, - message: self.to_string(), - data: Some(data), - } - } -} - -impl std::fmt::Display for AskRefusal { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AskRefusal::NoDeclaredCapability { - capability, round, .. - } => write!( - f, - "`{capability}` requires input from you before it runs, and your \ - `_meta.io.modelcontextprotocol/clientCapabilities` declares none of the \ - capabilities round {round} of that exchange needs. Declaring no capabilities does \ - not skip the operator's gate; it means the call cannot proceed." - ), - AskRefusal::RoundCapExceeded { capability, cap } => write!( - f, - "this exchange with `{capability}` has already used its {cap} input rounds. The cap \ - is hard and it is carried inside the request state, so it cannot be reset by \ - replaying an earlier one." - ), - AskRefusal::StateRejected(r) => write!(f, "{r}"), - AskRefusal::Unsolicited { capability } => write!( - f, - "`{capability}` did not ask you for any input, so `inputResponses` and \ - `requestState` have nothing to answer. This server does not accept request state it \ - did not mint." - ), - AskRefusal::Unanswered { - capability, - missing, - } => write!( - f, - "the retry of `{capability}` answered none of the requested inputs (expected \ - `{missing}`)." - ), - AskRefusal::ClockUnavailable { capability } => write!( - f, - "`{capability}` is configured to request input, but the host's clock could not be \ - read, so the window of the `requestState` the protocol requires can be neither \ - stamped nor checked. The call is refused rather than served with a state judged \ - at no time." - ), - AskRefusal::NoSealer { capability } => write!( - f, - "`{capability}` is configured to request input, but this deployment has no \ - `auth.signing_key` and therefore cannot mint the integrity-protected `requestState` \ - the protocol requires. The call is refused rather than served with unprotected \ - state." - ), - } - } -} - -/// What the request presented on a retry. -#[derive(Clone, Copy, Debug, Default)] -pub struct Retry<'a> { - /// `params.inputResponses`. - pub responses: Option<&'a Value>, - /// `params.requestState`. - pub state: Option<&'a str>, -} - -/// What the state is bound to. -#[derive(Clone, Copy, Debug)] -pub struct Bind<'a> { - /// The authenticated principal. - pub principal: &'a str, - /// `tools/call` or `prompts/get`. - pub method: &'a str, - /// The published capability. - pub capability: &'a str, - /// The live catalogue generation. - pub generation: u64, - /// Unix seconds now. - pub now: u64, - /// The principal's roots epoch now. - pub roots_epoch: u64, -} - -/// The digest of a request's arguments: SHA-256 over their serialisation (key-ordered), hex. -#[must_use] -pub fn digest_arguments(arguments: &Value) -> String { - busbar_contract::redacted::sha256_hex(&serde_json::to_vec(arguments).unwrap_or_default()) -} - -/// Whether the caller's capabilities declare `key`. -/// THE ASKS OF ONE ROUND busbar makes of its caller from inside a task, filtered to the capabilities -/// the caller declared (the served engine's `asks_for_round`): an ask whose capability the caller -/// did not declare is not made. -#[must_use] -pub fn asks_for_round(round: &AskRoundCfg, capabilities: &Value) -> Vec { - round - .iter() - .map(|(key, cfg)| CallerAsk::from_config(key, cfg)) - .filter(|ask| { - ask.capability_key() - .is_some_and(|k| declared(capabilities, k)) - }) - .collect() -} - -fn declared(capabilities: &Value, key: &str) -> bool { - capabilities.get(key).is_some_and(|v| !v.is_null()) -} - -/// THE DECISION, in the engine's order: no rounds; the presented state opened and matched; the -/// cap; a completed exchange redeemed once; an unanswered retry; the round's asks filtered by the -/// caller's capabilities; a fresh state sealed. `seal` is `None` when the deployment cannot seal. -pub fn decide( - rounds: &[AskRoundCfg], - cap: u32, - caller_capabilities: &Value, - retry: Retry<'_>, - bind: Bind<'_>, - args_digest: &str, - seal: Option<&mut dyn Seal>, -) -> AskDecision { - let capability = || bind.capability.to_string(); - let mut seal = seal; - // A RELAYED ASK'S RETRY: the state opened and matched as busbar's own is, then spent once. - if let (Some(blob), Some(sealer)) = (retry.state, seal.as_deref_mut()) { - if let Ok(opened) = sealer.open(blob) { - if let Some(leg) = opened.upstream.clone() { - if let Err(e) = opened.matches( - bind.principal, - bind.method, - bind.capability, - args_digest, - bind.generation, - bind.now, - ) { - return AskDecision::Refuse(AskRefusal::StateRejected(e)); - } - // A CHILD'S REQUESTS are each answered on its input, and busbar writes none of - // them itself: a retry that leaves one unanswered is refused before the state is - // spent, so the caller can still answer it. - if let Some(missing) = leg.child.as_ref().and_then(|c| unanswered(c, retry)) { - return AskDecision::Refuse(AskRefusal::Unanswered { - capability: capability(), - missing, - }); - } - let expires_at = opened.issued_at.saturating_add(opened.ttl_secs); - if !sealer.redeem(&opened.nonce, expires_at, bind.now) { - return AskDecision::Refuse(AskRefusal::StateRejected(Rejected::AlreadySpent)); - } - return AskDecision::Relayed(leg); - } - } - } - if rounds.is_empty() { - if retry.state.is_some() || retry.responses.is_some() { - return AskDecision::Refuse(AskRefusal::Unsolicited { - capability: capability(), - }); - } - return AskDecision::Proceed; - } - let no_sealer = || { - AskDecision::Refuse(AskRefusal::NoSealer { - capability: capability(), - }) - }; - let mut presented: Option<(String, u64)> = None; - let next_round = match retry.state { - None => 0u32, - Some(blob) => { - let Some(seal) = seal.as_deref_mut() else { - return no_sealer(); - }; - let opened = match seal.open(blob) { - Ok(s) => s, - Err(e) => return AskDecision::Refuse(AskRefusal::StateRejected(e)), - }; - if let Err(e) = opened.matches( - bind.principal, - bind.method, - bind.capability, - args_digest, - bind.generation, - bind.now, - ) { - return AskDecision::Refuse(AskRefusal::StateRejected(e)); - } - if opened - .roots_epoch - .is_some_and(|sealed| sealed != bind.roots_epoch) - { - return AskDecision::Refuse(AskRefusal::StateRejected(Rejected::StaleRoots)); - } - presented = Some(( - opened.nonce.clone(), - opened.issued_at.saturating_add(opened.ttl_secs), - )); - opened.round.saturating_add(1) - } - }; - if next_round >= cap && (next_round as usize) < rounds.len() { - return AskDecision::Refuse(AskRefusal::RoundCapExceeded { - capability: capability(), - cap, - }); - } - let this_round = rounds.get(next_round as usize); - if next_round > 0 { - let answered = retry - .responses - .and_then(Value::as_object) - .is_some_and(|m| !m.is_empty()); - if !answered { - let previous = rounds - .get(next_round as usize - 1) - .map(|r| r.keys().cloned().collect::>().join(", ")) - .unwrap_or_default(); - return AskDecision::Refuse(AskRefusal::Unanswered { - capability: capability(), - missing: previous, - }); - } - } - let Some(this_round) = this_round else { - let (Some((nonce, expires_at)), Some(seal)) = (presented, seal.as_deref_mut()) else { - return AskDecision::Refuse(AskRefusal::StateRejected(Rejected::AlreadySpent)); - }; - if !seal.redeem(&nonce, expires_at, bind.now) { - return AskDecision::Refuse(AskRefusal::StateRejected(Rejected::AlreadySpent)); - } - return AskDecision::Proceed; - }; - let asks: Vec = this_round - .iter() - .map(|(key, cfg)| CallerAsk::from_config(key, cfg)) - .filter(|a| { - a.capability_key() - .is_some_and(|k| declared(caller_capabilities, k)) - }) - .collect(); - if asks.is_empty() { - let mut required: Vec<&'static str> = Vec::new(); - for (key, cfg) in this_round { - if let Some(k) = CallerAsk::from_config(key, cfg).capability_key() { - if !required.contains(&k) { - required.push(k); - } - } - } - return AskDecision::Refuse(AskRefusal::NoDeclaredCapability { - capability: capability(), - round: next_round, - required, - }); - } - let Some(seal) = seal else { - return no_sealer(); - }; - let Some(nonce) = seal.nonce() else { - return no_sealer(); - }; - let exchange_asks_roots = rounds - .iter() - .flat_map(|round| round.values()) - .any(|cfg| cfg.method == ASK_ROOTS); - let Some(request_state) = seal.mint(&AskState { - principal: bind.principal.to_string(), - method: bind.method.to_string(), - capability: bind.capability.to_string(), - args_digest: args_digest.to_string(), - generation: bind.generation, - round: next_round, - nonce, - issued_at: bind.now, - ttl_secs: DEFAULT_TTL_SECS, - roots_epoch: exchange_asks_roots.then_some(bind.roots_epoch), - upstream: None, - }) else { - return no_sealer(); - }; - AskDecision::Ask { - asks, - request_state, - round: next_round, - } -} - -/// The `input_required` result busbar composes from its own asks: `200`, the asks keyed as the -/// operator keyed them, and the sealed state. -#[must_use] -pub fn input_required_result(id: &Value, asks: &[CallerAsk], request_state: &str) -> Vec { - let mut requests = Map::new(); - for ask in asks { - requests.insert( - ask.key().to_string(), - json!({ "method": ask.method(), "params": ask.params() }), - ); - } - let mut value = Map::new(); - value.insert( - "resultType".into(), - crate::jsonrpc::RESULT_TYPE_INPUT_REQUIRED.into(), - ); - value.insert("inputRequests".into(), Value::Object(requests)); - value.insert("requestState".into(), request_state.into()); - let mut envelope = Map::new(); - envelope.insert("jsonrpc".into(), "2.0".into()); - envelope.insert("id".into(), id.clone()); - envelope.insert("result".into(), Value::Object(value)); - serde_json::to_vec(&Value::Object(envelope)).unwrap_or_default() -} - -/// The keys of a child's requests the retry did not answer, joined; `None` when it answered all. -fn unanswered(child: &ChildLeg, retry: Retry<'_>) -> Option { - let answered = retry.responses.and_then(Value::as_object); - let missing: Vec<&str> = child - .asks - .iter() - .map(|(key, _)| key.as_str()) - .filter(|key| !answered.is_some_and(|m| m.contains_key(*key))) - .collect(); - (!missing.is_empty()).then(|| missing.join(", ")) -} - -/// THE STATE A RELAYED UPSTREAM ASK IS ANSWERED UNDER: busbar's one sealed `requestState`, bound to -/// the principal, the call (method, tool, arguments as the caller sent them) and the catalogue -/// generation, nesting the member that asked and its own state. `None` when it cannot be sealed. -#[must_use] -pub fn relay_state( - bind: Bind<'_>, - args_digest: &str, - leg: UpstreamLeg, - seal: &mut dyn Seal, -) -> Option { - relay_state_for(bind, args_digest, leg, DEFAULT_TTL_SECS, seal) -} - -/// [`relay_state`], standing `ttl_secs`: a task parked on its caller's answer waits as long as the -/// task may stay active ([`crate::tool_tasks::ACTIVE_TASK_ABANDON_MS`]), not a request's replay -/// window. -#[must_use] -pub fn relay_state_for( - bind: Bind<'_>, - args_digest: &str, - leg: UpstreamLeg, - ttl_secs: u64, - seal: &mut dyn Seal, -) -> Option { - let nonce = seal.nonce()?; - seal.mint(&AskState { - principal: bind.principal.to_string(), - method: bind.method.to_string(), - capability: bind.capability.to_string(), - args_digest: args_digest.to_string(), - generation: bind.generation, - round: 0, - nonce, - issued_at: bind.now, - ttl_secs, - roots_epoch: None, - upstream: Some(leg), - }) -} - -/// A RELAYED ASK'S STATE, presented on the task path's retry (the continuation `tasks/update` nests): -/// opened, matched to the principal, the call and the generation it was sealed under, and spent -/// once. The leg it nests, or why it was refused: a forged state does not open, a spent one does -/// not redeem, and a state of busbar's own asks is not a relayed one. -/// -/// # Errors -/// -/// The first check it fails. -pub fn open_relayed( - blob: &str, - bind: Bind<'_>, - args_digest: &str, - seal: &mut dyn Seal, -) -> Result { - let opened = seal.open(blob)?; - let Some(leg) = opened.upstream.clone() else { - return Err(Rejected::WrongRequest); - }; - opened.matches( - bind.principal, - bind.method, - bind.capability, - args_digest, - bind.generation, - bind.now, - )?; - let expires_at = opened.issued_at.saturating_add(opened.ttl_secs); - if !seal.redeem(&opened.nonce, expires_at, bind.now) { - return Err(Rejected::AlreadySpent); - } - Ok(leg) -} - -/// THE UPSTREAM'S `InputRequiredResult`, RELAYED: its result as it came (`inputRequests` verbatim), -/// its own `requestState` replaced by busbar's sealed `state`. -#[must_use] -pub fn relayed_result(id: &Value, upstream: &Value, state: &str) -> Vec { - let mut result = upstream.as_object().cloned().unwrap_or_default(); - result.insert("requestState".into(), state.into()); - serde_json::to_vec(&json!({ "jsonrpc": "2.0", "id": id, "result": Value::Object(result) })) - .unwrap_or_default() -} - -#[cfg(test)] -#[path = "tests/ask.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/call.rs b/crates/busbar-plane-mcp/src/call.rs deleted file mode 100644 index 080e7e5fda..0000000000 --- a/crates/busbar-plane-mcp/src/call.rs +++ /dev/null @@ -1,1441 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE RELAYED CALL, pure: one `tools/call` from the caller's request to the far end's answer, in the -//! served engine's order and words. Three steps, each a function the door calls on one piece: -//! -//! 1. [`admit`]: the caller's request judged against the generation's catalogue and the caller's -//! grants (the `admit` predicate the door binds to the kernel's entitlement service), in the -//! engine's order — the name, its length, the tool, the grant, the approval, the mirrored -//! `Mcp-Param-*` headers, the tasks gate, and the answers to busbar's own asks. A refusal is the -//! engine's status, envelope and call-log line. -//! 2. [`outbound`]: the request bound for the member the kernel's walk picked, built by the client -//! dialect's own builder ([`crate::client::jsonrpc::tools_call`]). The credential is not here: -//! the kernel's auth binding adds it when it sends. -//! 3. [`settle`]: the far end's answer read as the engine reads it — the last event of a streamed -//! answer, the JSON-RPC correlation, an upstream's ask judged against the operator's grants and -//! relayed to the caller (busbar answers none itself, Law 11), and a result relayed as the -//! upstream sent it — into the caller's answer and its call-log line. -//! -//! What is not here is the kernel's: the trust lifecycle (pin, sightings, demotion), the hook gate -//! and rewrite, the outbound credential, the breaker and the pool walk, the budget and the meter. - -use serde_json::{json, Map, Value}; - -use crate::catalogue::{granted, Catalogue, ToolEntry}; -use crate::checks::decode_sentinel; -use crate::client::jsonrpc::{self as wire, AdvertisedCaps, RpcOutcome, ServerAsk}; -use crate::codec::{ - CODE_HEADER_MISMATCH, CODE_INVALID_PARAMS, CODE_MISSING_CLIENT_CAPABILITY, CODE_REFUSED, - META_CLIENT_CAPABILITIES, -}; -use crate::identity::{ServerId, ToolKey}; -use crate::jsonrpc::RESULT_TYPE_COMPLETE; -use crate::tool_arrival::Refusal; -use crate::tools_config::{McpServerDefCfg, TaskSupport, DEFAULT_MAX_INPUT_REQUIRED_ROUNDS}; -use busbar_contract::abi::host::service::{ - DISTRUST_CHANGED, DISTRUST_NONE, DISTRUST_NOT_APPROVED, DISTRUST_QUARANTINED, - DISTRUST_UNKNOWN_ITEM, DISTRUST_UNSIGHTED, -}; -use busbar_contract::vocab; - -/// The audit reason of a call whose arguments the argument guard refused. -pub const REASON_TOOL_ARGUMENT_REFUSED: &str = "tool_argument_refused"; - -/// THE CEILING ON `params.name`, IN BYTES, checked before the per-call log opens: the name is -/// written verbatim into a durable, hash-chained record the instant any terminal fires, so an -/// oversized one is refused with no log line at all. Refused outright, never truncated: a truncated -/// name is a different tool. -pub const MAX_TOOL_NAME_BYTES: usize = 256; - -/// The status of a successful answer. -const STATUS_OK: u32 = 200; -/// A malformed request. -const STATUS_BAD_REQUEST: u32 = 400; -/// A refusal by policy. -const STATUS_FORBIDDEN: u32 = 403; -/// Nothing there for this caller. -const STATUS_NOT_FOUND: u32 = 404; - -/// The call-log reason of a call refused for a mirrored-header disagreement. -pub const REASON_CUSTOM_PARAM_MISMATCH: &str = "custom_param_mismatch"; -/// The call-log reason of a `task_support: required` call from a caller that declared no tasks. -pub const REASON_TASKS_UNDECLARED: &str = "tasks_capability_undeclared"; -/// The call-log reason of an answer naming an input busbar did not ask for. -pub const REASON_ANSWER_UNDECLARED: &str = "caller_ask_answer_undeclared"; -/// The call-log reason of a complete result still carrying an ask's field, refused at the terminal -/// check (an ask is relayed only as an input-required result). -pub const REASON_ASK_NOT_PROXIED: &str = "ask_not_proxied"; -/// The call-log reason of a call whose upstream asked its caller something, relayed to the caller. -pub const REASON_ASK_RELAYED: &str = "ask_relayed"; - -/// The tasks extension's identifier. -pub const TASKS_EXTENSION_ID: &str = crate::answer::TASKS_EXTENSION_ID; - -/// ONE LINE OF THE PER-CALL LOG, as the call ended: what it named, what it resolved to, and how it -/// ended. `server` and `tool_digest` stay empty on a refusal that never reached a registration. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CallLine { - /// The server the call resolved to. - pub server: String, - /// The published tool name, or the name the caller asked for before resolution. - pub tool: String, - /// The approved digest the call rode. - pub tool_digest: String, - /// `dispatched` or `refused`. - pub outcome: &'static str, - /// The reason token; empty on a plain dispatch. - pub reason: String, - /// The admin audit row the call writes as it ends; `None` for a refusal the served engine - /// audited nothing for (a malformed request, a header that disagrees with its body). - pub audit: Option, -} - -/// The audit action of a `tools/call` decision. -pub const ACTION_TOOL_CALL: &str = "mcp_tool.call"; -/// The audit action of busbar's own ask of its caller (asked, or the answer refused). -pub const ACTION_CALLER_ASK: &str = "mcp.caller_ask"; -/// The audit action of a task a caller cancelled. -pub const ACTION_TASK_CANCEL: &str = "mcp_task.cancel"; - -/// ONE ADMIN AUDIT ROW, in the served engine's words: the action, the resource it names, and whether -/// the action was applied or rejected. The kernel writes it on its own audit chain under the -/// principal it verified (a `RECORD_AUDIT` write on the unit's answer). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AuditRow { - /// The action word. - pub action: &'static str, - /// The resource it names. - pub resource: String, - /// `applied` (true) or `rejected` (false). - pub applied: bool, -} - -impl AuditRow { - /// A `tools/call` decision on the tool `name`. - #[must_use] - pub fn tool(name: &str, applied: bool) -> Self { - AuditRow { - action: ACTION_TOOL_CALL, - resource: format!("mcp_tool:{name}"), - applied, - } - } - - /// Busbar's ask of its caller for the prompt `namespaced`. - #[must_use] - pub fn prompt_ask(namespaced: &str, applied: bool) -> Self { - AuditRow { - action: ACTION_CALLER_ASK, - resource: format!("mcp_prompt:{namespaced}"), - applied, - } - } - - /// A task the caller cancelled. - #[must_use] - pub fn task_cancel(task_id: &str) -> Self { - AuditRow { - action: ACTION_TASK_CANCEL, - resource: format!("mcp_task:{task_id}"), - applied: true, - } - } -} - -impl CallLine { - /// A refusal before the name resolved: audited as a rejected call of the name asked for. - fn asked(name: &str, reason: &str) -> Self { - CallLine { - server: String::new(), - tool: name.to_string(), - tool_digest: String::new(), - outcome: vocab::OUTCOME_REFUSED, - reason: reason.to_string(), - audit: Some(AuditRow::tool(name, false)), - } - } - - /// A call that resolved to `entry`: audited applied only for a plain dispatch (a dispatch with a - /// reason came back badly; a refusal never went). - fn resolved(entry: &ToolEntry, outcome: &'static str, reason: &str) -> Self { - CallLine { - server: entry.server.clone(), - tool: entry.namespaced.clone(), - tool_digest: entry.schema_hash.clone().unwrap_or_default(), - outcome, - reason: reason.to_string(), - audit: Some(AuditRow::tool( - &entry.namespaced, - outcome == vocab::OUTCOME_DISPATCHED && reason.is_empty(), - )), - } - } - - /// No audit row: a refusal the served engine audited nothing for. - fn unaudited(mut self) -> Self { - self.audit = None; - self - } - - /// The row is busbar's ask of its caller (`applied`: asked; rejected: its answer refused), on - /// the same tool. - fn asking(mut self, applied: bool) -> Self { - if let Some(row) = &mut self.audit { - row.action = ACTION_CALLER_ASK; - row.applied = applied; - } - self - } -} - -/// A call admitted: the entry it resolved to and the arguments that go upstream. -#[derive(Debug, Clone, PartialEq)] -pub struct AdmittedCall { - /// The catalogue entry. - pub entry: ToolEntry, - /// The arguments, with any declared answers merged in. - pub arguments: Value, - /// The caller's request id. - pub id: Value, - /// The caller's own `progressToken`, when it asked for progress. - pub progress_token: Option, - /// The client capabilities the caller declared (`_meta`): what an upstream's ask may be relayed - /// to it for. - pub capabilities: Value, - /// The digest of the arguments AS THE CALLER SENT THEM (before any answer of busbar's own asks - /// was merged): what a relayed ask's state is bound to. - pub sent_digest: String, - /// THE RETRY OF A RELAYED UPSTREAM ASK: the member that asked (the call goes back to it and no - /// other) and the continuation it is sent (the caller's `inputResponses` and the upstream's own - /// `requestState`, verbatim). `None` for every other call. - pub relay: Option>, -} - -/// A relayed ask's retry: where it goes and what it carries. -#[derive(Debug, Clone, PartialEq)] -pub struct RelayedRetry { - /// The registration that asked. - pub member: String, - /// The upstream round it answers. - pub round: u32, - /// `{inputResponses, requestState}`, the caller's answers and the upstream's state verbatim. - pub continuation: Value, - /// A stdio child's own requests the caller answered: each answer goes back to the child under - /// its request id, and the call the child still owes is read on ([`crate::ask::ChildLeg`]). - pub child: Option, -} - -/// What [`admit`] decided. -#[derive(Debug, Clone, PartialEq)] -pub enum Admission { - /// AdmittedCall: the call goes to the far end. - Go(AdmittedCall), - /// Refused: the answer, and the call-log line (none for a name too long to record). - Refused(Refusal, Option), - /// Busbar asks its own caller first: the `input_required` answer and the call-log line. - Asked(Vec, CallLine), - /// Answered here and never sent: the upstream-failure tool error of a call whose server the - /// door's live re-fetch could not reach (ARCHITECT Q3 (c)), and the call-log line. - Unreached(Vec, CallLine), -} - -/// A JSON-RPC error answer. -fn error(status: u32, id: &Value, code: i64, message: String, data: Option) -> Refusal { - Refusal { - status, - id: Some(id.clone()), - code, - message, - data, - } -} - -/// A catalogue refusal: `-32000` with the audit reason in `data`. -fn catalogue_refusal(status: u32, id: &Value, message: String, reason: &str) -> Refusal { - error( - status, - id, - CODE_REFUSED, - message, - Some(json!({ "reason": reason })), - ) -} - -/// The words of a tool the caller cannot reach: one sentence whether it does not exist or the -/// caller may not see it, so the catalogue does not leak what it hides. -fn not_exposed(name: &str) -> String { - format!("`{name}` is not a tool this server exposes") -} - -/// THE ANSWER TO A CALL THE CALLER IS NOT GRANTED, in the served engine's words: `404`, the same -/// sentence an unknown tool gets (the catalogue does not leak what it hides), its audit reason -/// `not_granted` in `data`. The kernel's grant check refuses such a call before the plane decides it -/// (`ScopeDenied`), and the plane renders that refusal as the served engine answered it. -#[must_use] -pub fn not_granted(id: &Value, name: &str) -> Refusal { - catalogue_refusal( - STATUS_NOT_FOUND, - id, - not_exposed(name), - vocab::REASON_NOT_GRANTED, - ) -} - -/// Whether the caller's `_meta` client capabilities declare the tasks extension. -#[must_use] -pub fn client_declares_tasks(capabilities: &Value) -> bool { - capabilities - .get("extensions") - .and_then(|e| e.get(TASKS_EXTENSION_ID)) - .is_some_and(|v| !v.is_null()) -} - -/// The `-32021` refusal of a request that needs the tasks extension the caller did not declare: -/// `400`, which `MissingRequiredClientCapabilityError` fixes, naming what to add. -#[must_use] -pub fn missing_tasks_capability(id: &Value) -> Refusal { - error( - STATUS_BAD_REQUEST, - id, - CODE_MISSING_CLIENT_CAPABILITY, - format!( - "This request needs the `{TASKS_EXTENSION_ID}` extension, and it was not declared in \ - `params._meta.io.modelcontextprotocol/clientCapabilities.extensions`. Declare it — \ - per session or on this one request — and retry." - ), - Some(json!({ - "reason": "tasks_extension_not_declared", - "requiredCapabilities": { "extensions": { TASKS_EXTENSION_ID: {} } }, - })), - ) -} - -/// A TASKS VERB (`tasks/get`, `tasks/update`, `tasks/cancel`) on this plane: behind the `-32021` -/// gate, then `params.taskId`, then the task itself. The plane answers every call as a result and -/// holds no task (QUESTIONS FOLD-MCP-2-4), so every id names no task of this caller's: `-32602`, the one -/// answer for an id that does not exist and one that is another caller's. -#[must_use] -pub fn task_verb(id: &Value, params: Option<&Value>) -> Refusal { - let capabilities = params - .and_then(|p| p.get("_meta")) - .and_then(|m| m.get(META_CLIENT_CAPABILITIES)) - .unwrap_or(&Value::Null); - if !client_declares_tasks(capabilities) { - return missing_tasks_capability(id); - } - let message = match params.and_then(|p| p.get("taskId")).and_then(Value::as_str) { - None => "`params.taskId` is required and must be a string.", - Some(_) => "No task with that `taskId` exists for this caller.", - }; - error( - STATUS_BAD_REQUEST, - id, - CODE_INVALID_PARAMS, - message.to_string(), - None, - ) -} - -/// SEP-2243: the first disagreement between a tool's `x-mcp-header` properties and the caller's -/// `Mcp-Param-*` headers, in the engine's words. `header` reads a request head field by its -/// lower-case name. -#[must_use] -pub fn custom_param_mismatch( - entry: &ToolEntry, - arguments: &Value, - header: &impl Fn(&str) -> Option, -) -> Option { - let properties = entry - .input_schema - .as_ref()? - .get("properties")? - .as_object()?; - for (property, definition) in properties { - let Some(suffix) = definition.get("x-mcp-header").and_then(|v| v.as_str()) else { - continue; - }; - // The reader takes the lower-case name; the engine's head lookup was case-insensitive. - let header_name = format!("mcp-param-{}", suffix.to_ascii_lowercase()); - let value = header(&header_name); - let header_value = value.as_deref().map(str::trim); - let body = arguments.get(property).and_then(|v| v.as_str()); - match (header_value, body) { - (None, None) => continue, - (None, Some(_)) => { - return Some(format!( - "`{property}` carries an `x-mcp-header` annotation, so a request whose body \ - sets it must also carry the `Mcp-Param-{suffix}` header. Without it an \ - intermediary routes on a parameter it cannot see." - )) - } - (Some(_), None) => { - return Some(format!( - "The `Mcp-Param-{suffix}` header is set but the body's `arguments.{property}` \ - is absent or is not a string, so there is nothing for it to mirror." - )) - } - (Some(header_value), Some(body)) => { - let Some(decoded) = decode_sentinel(header_value) else { - return Some(format!( - "The `Mcp-Param-{suffix}` header carries a `=?base64?…?=` sentinel whose \ - contents are not valid Base64. It is refused rather than decoded \ - leniently: two intermediaries that disagree about the same bytes are two \ - different requests." - )); - }; - if decoded != body { - return Some(format!( - "The `Mcp-Param-{suffix}` header does not match the body's \ - `arguments.{property}`." - )); - } - } - } - } - None -} - -/// ADMIT ONE CALL, in the engine's order. `header` reads a request head field by its lower-case -/// name; `admit` is the caller's grant predicate (scope kind, name); `ask` is busbar's own ask -/// decision for the resolved tool over the arguments as the caller sent them -/// ([`crate::ask::decide`]), made before any answer becomes an argument. -pub fn admit_call( - catalogue: &Catalogue, - id: &Value, - params: Option<&Value>, - header: &impl Fn(&str) -> Option, - admit: &impl Fn(&str, &str) -> bool, - ask: &mut dyn FnMut(&ToolEntry, &Value) -> crate::ask::AskDecision, -) -> Admission { - admit_trusted( - catalogue, - id, - params, - header, - admit, - &mut |_| Trust::Verdict(DISTRUST_NONE), - ask, - ) -} - -/// THE KERNEL'S TRUST VERDICT ON ONE TOOL, as the door read it on the route leg after its live -/// re-fetch (ARCHITECT Q3: route, re-fetch and sight, the kernel's Approve, serve). The door states -/// facts and judges none: the verdict is `trust.serves`'s, and the only answer the door renders on -/// its own is the fact that its re-fetch failed, which leaves nothing to sight. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Trust { - /// `trust.serves`'s answer for the tool's registration and trust key: [`DISTRUST_NONE`] (it - /// serves) or the `DISTRUST_*` that refuses it. - Verdict(u64), - /// The live re-fetch could not reach the server, with the reason: reported to the kernel as an - /// unreachable sighting (its last verdict stands), and the call fails as an upstream failure. - Unreached(String), -} - -/// The refusal reason of a tool whose server the kernel quarantined. -pub const REASON_QUARANTINED: &str = "quarantined"; -/// The refusal reason of a tool the operator has not approved. -pub const REASON_NOT_APPROVED: &str = "not_approved"; -/// The refusal reason of a tool whose server was never verified. -pub const REASON_UNVERIFIED: &str = "pending"; -/// The refusal reason of a tool the server does not offer (the catalogue's own word). -pub const REASON_UNKNOWN_TOOL: &str = "unknown_tool"; - -/// THE KERNEL'S VERDICT RENDERED in this dialect's words: `None` when it serves. An item the kernel -/// never sighted is not found (`404`, as a name the catalogue does not carry); one it knows and the -/// operator has not granted, and every other distrust, is refused (`403`). A server the re-fetch -/// could not reach is no verdict at all: the call fails as an upstream failure, a tool error. -fn trust_answer(id: &Value, name: &str, entry: &ToolEntry, trust: Trust) -> Option { - trust_refusal(id, name, entry, trust) - .map(|either| either.unwrap_or_else(|(r, line)| Admission::Refused(r, Some(line)))) -} - -/// [`trust_answer`]'s two shapes: a refusal (`Err`) or the unreached call's answer (`Ok`). -fn trust_refusal( - id: &Value, - name: &str, - entry: &ToolEntry, - trust: Trust, -) -> Option> { - let server = &entry.server; - let refused = |message: String, reason: &str| { - ( - catalogue_refusal(STATUS_FORBIDDEN, id, message, reason), - CallLine::resolved(entry, vocab::OUTCOME_REFUSED, reason), - ) - }; - let code = match trust { - Trust::Verdict(code) => code, - Trust::Unreached(reason) => { - return Some(Ok(Admission::Unreached( - result( - id, - upstream_failure_result( - server, - &format!("busbar could not reach it to verify its tool list: {reason}"), - ), - ), - CallLine::resolved(entry, vocab::OUTCOME_REFUSED, vocab::REASON_UPSTREAM_FAILED), - ))) - } - }; - Some(Err(match code { - DISTRUST_NONE => return None, - DISTRUST_UNKNOWN_ITEM => ( - catalogue_refusal(STATUS_NOT_FOUND, id, not_exposed(name), REASON_UNKNOWN_TOOL), - CallLine::asked(name, REASON_UNKNOWN_TOOL), - ), - DISTRUST_NOT_APPROVED => ( - catalogue_refusal( - STATUS_FORBIDDEN, - id, - format!("`{name}` is registered but has not been approved, so it does not serve"), - REASON_NOT_APPROVED, - ), - CallLine::asked(name, REASON_NOT_APPROVED), - ), - // A tool offered at another digest than the one approved is the rug-pull: its server is - // quarantined (ARCHITECT Q3: changed digest -> quarantined), the served engine's words. - DISTRUST_CHANGED | DISTRUST_QUARANTINED => refused( - format!( - "MCP server `{server}` is quarantined, so it serves nothing; review its changes \ - and approve it again" - ), - REASON_QUARANTINED, - ), - DISTRUST_UNSIGHTED => refused( - format!("MCP server `{server}` has not been verified yet, so it serves nothing"), - REASON_UNVERIFIED, - ), - _ => refused( - format!("MCP server `{server}` is not one this door may trust, so it serves nothing"), - vocab::REASON_NOT_SERVING, - ), - })) -} - -/// [`admit`], with THE KERNEL'S TRUST VERDICT: `trust` answers, for the named tool, what the -/// kernel's Approve said of it ([`Trust`], `trust.serves` over the tool's registration and trust -/// key after the door's re-fetch sighted it), rendered by [`trust_refusal`]. Asked after the name -/// and the grants, before the header mirror and the ask: a refused dispatch never reaches the wire. -/// The arguments it admits are judged next, by [`judge_arguments`]. -pub fn admit_trusted( - catalogue: &Catalogue, - id: &Value, - params: Option<&Value>, - header: &impl Fn(&str) -> Option, - admit: &impl Fn(&str, &str) -> bool, - trust: &mut dyn FnMut(&ToolEntry) -> Trust, - ask: &mut dyn FnMut(&ToolEntry, &Value) -> crate::ask::AskDecision, -) -> Admission { - let Some(name) = params.and_then(|p| p.get("name")).and_then(Value::as_str) else { - return Admission::Refused( - error( - STATUS_BAD_REQUEST, - id, - CODE_INVALID_PARAMS, - "`params.name` is required and must be a string.".to_string(), - None, - ), - Some(CallLine::asked("", vocab::REASON_MALFORMED).unaudited()), - ); - }; - if name.len() > MAX_TOOL_NAME_BYTES { - return Admission::Refused( - error( - STATUS_BAD_REQUEST, - id, - CODE_INVALID_PARAMS, - format!( - "`params.name` is {} bytes, longer than the {MAX_TOOL_NAME_BYTES} this \ - deployment retains for a tool name. Refused before it reached the per-call \ - log: a name this long would be written verbatim into a durable, hash-chained \ - record.", - name.len() - ), - None, - ), - None, - ); - } - let mut arguments = params - .and_then(|p| p.get("arguments")) - .cloned() - .unwrap_or_else(|| json!({})); - let sent_digest = crate::ask::digest_arguments(&arguments); - let Some(entry) = catalogue.tool(name) else { - return Admission::Refused( - catalogue_refusal(STATUS_NOT_FOUND, id, not_exposed(name), REASON_UNKNOWN_TOOL), - Some(CallLine::asked(name, REASON_UNKNOWN_TOOL)), - ); - }; - if !granted(admit, &entry.server, &entry.namespaced) { - return Admission::Refused( - catalogue_refusal( - STATUS_NOT_FOUND, - id, - not_exposed(name), - vocab::REASON_NOT_GRANTED, - ), - Some(CallLine::asked(name, vocab::REASON_NOT_GRANTED)), - ); - } - // THE KERNEL'S APPROVE, as the route leg asks it: the door states the tool's registration and - // trust key, and renders the verdict (ARCHITECT Q3). - if let Some(answer) = trust_answer(id, name, entry, trust(entry)) { - return answer; - } - - let line = |reason: &str| Some(CallLine::resolved(entry, vocab::OUTCOME_REFUSED, reason)); - let header_mismatch = - |message: String| error(STATUS_BAD_REQUEST, id, CODE_HEADER_MISMATCH, message, None); - if let Some(message) = custom_param_mismatch(entry, &arguments, header) { - return Admission::Refused( - header_mismatch(message), - line(REASON_CUSTOM_PARAM_MISMATCH).map(CallLine::unaudited), - ); - } - let meta = params.and_then(|p| p.get("_meta")); - let capabilities = meta.and_then(|m| m.get(META_CLIENT_CAPABILITIES)); - if entry.task_support == TaskSupport::Required - && !client_declares_tasks(capabilities.unwrap_or(&Value::Null)) - { - return Admission::Refused(missing_tasks_capability(id), line(REASON_TASKS_UNDECLARED)); - } - // BUSBAR'S OWN ASK, decided over the arguments as the caller sent them: the state is sealed over - // their digest, so the answers are merged only after it. - let mut relay = None; - match ask(entry, &arguments) { - crate::ask::AskDecision::Proceed => {} - // A RELAYED ASK'S RETRY: the caller's answers are the upstream's, sent back to the member - // that asked with its own state, verbatim; none of them becomes an argument. - crate::ask::AskDecision::Relayed(leg) => { - let mut continuation = Map::new(); - if let Some(responses) = params.and_then(|p| p.get("inputResponses")) { - continuation.insert("inputResponses".to_string(), responses.clone()); - } - if let Some(state) = leg.state { - continuation.insert("requestState".to_string(), state); - } - relay = Some(Box::new(RelayedRetry { - member: leg.member, - round: leg.round, - continuation: Value::Object(continuation), - child: leg.child, - })); - } - crate::ask::AskDecision::Refuse(refusal) => { - return Admission::Refused( - refusal.refusal(id), - line(refusal.audit_reason()).map(|l| l.asking(false)), - ); - } - crate::ask::AskDecision::Ask { - asks, - request_state, - .. - } => { - return Admission::Asked( - crate::ask::input_required_result(id, &asks, &request_state), - CallLine::resolved( - entry, - vocab::OUTCOME_REFUSED, - vocab::REASON_CALLER_ASK_PENDING, - ) - .asking(true), - ); - } - } - // THE ANSWERS BECOME ARGUMENTS, bounded by what the operator asked for: an answer may bind only - // a key this tool's own `ask_caller:` rounds declared, and never an argument the caller sent. - if let Some(responses) = params - .and_then(|p| p.get("inputResponses")) - .and_then(Value::as_object) - .filter(|_| relay.is_none()) - { - let declared: std::collections::BTreeSet<&str> = entry - .ask_caller - .iter() - .flat_map(|round| round.keys().map(String::as_str)) - .collect(); - let sealed: std::collections::BTreeSet = arguments - .as_object() - .map(|o| o.keys().cloned().collect()) - .unwrap_or_default(); - if let Some(offending) = responses - .keys() - .find(|k| !declared.contains(k.as_str()) || sealed.contains(*k)) - { - return Admission::Refused( - catalogue_refusal( - STATUS_NOT_FOUND, - id, - format!( - "the answer named `{offending}`, which is not one of the inputs `{}` \ - requested — an answer may only supply what was asked for, and may never \ - rewrite an argument the confirmation was shown for.", - entry.namespaced - ), - vocab::REASON_NOT_GRANTED, - ), - line(REASON_ANSWER_UNDECLARED), - ); - } - if let Some(merged) = arguments.as_object_mut() { - for (key, value) in responses { - merged.insert(key.clone(), value.clone()); - } - } - if let Some(message) = custom_param_mismatch(entry, &arguments, header) { - return Admission::Refused( - header_mismatch(message), - line(REASON_CUSTOM_PARAM_MISMATCH).map(CallLine::unaudited), - ); - } - } - Admission::Go(AdmittedCall { - entry: entry.clone(), - arguments, - id: id.clone(), - progress_token: meta - .and_then(|m| m.get("progressToken")) - .filter(|v| !v.is_null()) - .cloned(), - capabilities: capabilities.cloned().unwrap_or(Value::Null), - sent_digest, - relay, - }) -} - -/// THE ARGUMENT GUARD ([`crate::argguard`]) on an admitted call: the arguments that go out (the -/// caller's, with the operator-bounded answers merged) are walked against the tool's approved input -/// schema, and every URL or host they carry is asked of the host's ONE destination judge: `judge` -/// answers `dest.judge`'s verdict for the tool and the host (BUSBAR-1.6.0.md Appendix C B.3 item -/// 11), `None` when it gave none. A refused one refuses the call before it is sent. A tool that -/// declared no schema is walked against `{"type": "object"}`: the walk is value-driven, so declaring -/// no schema narrows what it calls "declared" and nothing else. Any other admission is unchanged. -pub fn judge_arguments( - admission: Admission, - judge: &mut dyn FnMut(&ToolEntry, &str) -> Option, -) -> Admission { - let Admission::Go(admitted) = admission else { - return admission; - }; - let entry = &admitted.entry; - let schema = entry - .input_schema - .clone() - .unwrap_or_else(|| json!({ "type": "object" })); - match crate::argguard::guard(&schema, &admitted.arguments, |dest| judge(entry, dest)) { - Ok(_) => Admission::Go(admitted), - Err(refused) => Admission::Refused( - error( - STATUS_FORBIDDEN, - &admitted.id, - crate::codec::CODE_REFUSED, - refused.to_string(), - Some(json!({ "reason": REASON_TOOL_ARGUMENT_REFUSED })), - ), - Some(CallLine::resolved( - entry, - vocab::OUTCOME_REFUSED, - REASON_TOOL_ARGUMENT_REFUSED, - )), - ), - } -} - -/// What busbar declares to `def`'s server for `admitted`'s call: each ask kind the operator lets the -/// server put to callers AND the caller declared it can answer (the ask is relayed, Law 11). -fn advertised(admitted: &AdmittedCall, def: &McpServerDefCfg) -> AdvertisedCaps { - let declares = |k: &str| admitted.capabilities.get(k).is_some_and(|v| !v.is_null()); - AdvertisedCaps { - roots: def.grants.roots && declares("roots"), - sampling: def.grants.sampling && declares("sampling"), - elicitation: def.grants.elicitation && declares("elicitation"), - progress: admitted.progress_token.is_some(), - } -} - -/// THE `{tool, arguments}` PROJECTION of a `tools/call` body, as the hook kind reads an invocation: -/// the published name and the arguments as the caller sent them. `None` for any other body. -#[must_use] -pub fn invocation(body: &[u8]) -> Option { - let value: Value = serde_json::from_slice(body).ok()?; - if value.get("method")?.as_str()? != crate::codec::METHOD_TOOLS_CALL { - return None; - } - let params = value.get("params")?; - Some(busbar_contract::ir::invoke::InvokeReq { - tool: params.get("name")?.as_str()?.to_string(), - arguments: params - .get("arguments") - .cloned() - .unwrap_or_else(|| json!({})), - extra: busbar_contract::ir::SourceScopedExtra::default(), - }) -} - -/// A REQUEST-STAGE HOOK'S REWRITE, APPLIED TO A `tools/call` (`project`'s `rewrite`; BUSBAR-1.6.0.md -/// Part 3 section 12, "Hooks": the plane applies it in its own dialect and re-projects). The hook -/// stage hands the plane the hook's reply as `{"messages": [...], "tools": ...}`; an invocation has -/// one untrusted content member, its `arguments` object, and the rewrite REPLACES it under the -/// served engine's invoke rewrite contract: a `messages` entry's `content` (an object verbatim, or a -/// JSON string that parses to an object) or, for an entry with no `role`, the entry itself; the LAST -/// entry that yields an object wins. -/// -/// `None` = not applied (no usable object, a body that is not a `tools/call`, a reply that is not -/// JSON): the unit proceeds with the body it had, as a rewrite that cannot be applied left the -/// served engine's call untouched. `Some` is the rewritten request, whole, with every member the -/// caller sent but `params.arguments` as it was. -#[must_use] -pub fn rewritten(body: &[u8], rewrite: &[u8]) -> Option> { - let reply: Value = serde_json::from_slice(rewrite).ok()?; - let mut arguments: Option = None; - for message in reply.get("messages")?.as_array()? { - let candidate = match message.get("content") { - Some(content) => content, - None if message.get("role").is_none() => message, - None => continue, - }; - let resolved = match candidate { - Value::Object(_) => Some(candidate.clone()), - Value::String(text) => serde_json::from_str::(text) - .ok() - .filter(Value::is_object), - _ => None, - }; - if resolved.is_some() { - arguments = resolved; - } - } - let arguments = arguments?; - let mut request: Value = serde_json::from_slice(body).ok()?; - if request.get("method").and_then(Value::as_str) != Some(crate::codec::METHOD_TOOLS_CALL) { - return None; - } - let params = request.get_mut("params")?.as_object_mut()?; - params.insert("arguments".to_string(), arguments); - serde_json::to_vec(&request).ok() -} - -/// One request bound for the far end: the verb, the target path the kernel joins onto the -/// member's base URL, the dialect's head fields and the body. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct OutboundCall { - /// The verb. - pub verb: &'static str, - /// The target: the path (and query) of the member's registered URL. - pub target: String, - /// The head fields, lower-case names, in the order they are written. - pub fields: Vec<(String, String)>, - /// The body. - pub body: Vec, -} - -/// The path and query of `url`: everything from the first `/` after the authority, or `/`. -#[must_use] -pub fn path_of(url: &str) -> String { - let rest = url.split_once("://").map_or(url, |(_, r)| r); - match rest.find(['/', '?']) { - Some(at) if rest[at..].starts_with('/') => rest[at..].to_string(), - Some(at) => format!("/{}", &rest[at..]), - None => "/".to_string(), - } -} - -/// THE REQUEST FOR ONE ROUND to `member`'s registration `def`, with the continuation that answers -/// the previous round's ask. `None` when the member has no URL this plane can reach it at. -#[must_use] -pub fn outbound( - admitted: &AdmittedCall, - member: &str, - def: &McpServerDefCfg, - round: u32, - continuation: Option<&Value>, -) -> Option { - if def.url.is_empty() { - return None; - } - let key = ToolKey::new(ServerId::new(member).ok()?, &admitted.entry.tool).ok()?; - let request = wire::tools_call( - &def.url, - &key, - &admitted.arguments, - wire::dispatch_request_id(round), - None, - advertised(admitted, def), - continuation, - ); - Some(OutboundCall { - verb: "POST", - target: path_of(&request.url), - fields: request.headers, - body: request.body, - }) -} - -/// THE REQUEST FOR ONE ROUND to a `transport: stdio` member (ARCHITECT round 5 -/// Q-L3B-STDIO-UPSTREAM (A)): the same `tools/call` body, carrying `id` (the unit's own on the -/// child, `crate::tool_program::call_id`), sent as one message on the member's program; no head -/// field rides a pipe. `None` for a member that is not a program. -#[must_use] -pub fn outbound_program( - admitted: &AdmittedCall, - member: &str, - def: &McpServerDefCfg, - continuation: Option<&Value>, - id: u64, -) -> Option { - if !def - .transport - .is_some_and(crate::tools_config::ServerTransport::spawns_child) - { - return None; - } - let key = ToolKey::new(ServerId::new(member).ok()?, &admitted.entry.tool).ok()?; - let request = wire::tools_call( - "", - &key, - &admitted.arguments, - id, - None, - advertised(admitted, def), - continuation, - ); - Some(OutboundCall { - verb: "POST", - target: "/".to_string(), - fields: Vec::new(), - body: request.body, - }) -} - -/// A relayed call whose upstream leg failed before an answer (`reason`, in the previous release's -/// words): the caller is answered the upstream-failure result, the call log names it dispatched and -/// failed. -#[must_use] -pub fn upstream_failed(admitted: &AdmittedCall, reason: &str) -> Settled { - Settled::Answer { - status: STATUS_OK, - body: result( - &admitted.id, - upstream_failure_result(&admitted.entry.server, reason), - ), - line: CallLine::resolved( - &admitted.entry, - vocab::OUTCOME_DISPATCHED, - vocab::REASON_UPSTREAM_FAILED, - ), - } -} - -/// The payload of the LAST `data:` field in an event-stream body: a POST answered as a stream may -/// carry progress ahead of the response, and the response is what the stream was opened to -/// deliver. Multi-line `data:` fields are joined with `\n`; a body with no `data:` is empty, which -/// the JSON-RPC reader then reports as malformed. -#[must_use] -pub fn last_sse_data(raw: &[u8]) -> Vec { - let text = String::from_utf8_lossy(raw); - let mut current: Option> = None; - let mut last: Option> = None; - for line in text.lines() { - if let Some(rest) = line.strip_prefix("data:") { - current - .get_or_insert_with(Vec::new) - .push(rest.strip_prefix(' ').unwrap_or(rest).to_string()); - } else if line.is_empty() { - if let Some(done) = current.take() { - last = Some(done); - } - } - } - if let Some(done) = current.take() { - last = Some(done); - } - last.map(|parts| parts.join("\n").into_bytes()) - .unwrap_or_default() -} - -/// Every `notifications/progress` frame in an event-stream body, in arrival order: the one -/// server-originated frame busbar relays to its own caller. -#[must_use] -pub fn progress_frames(raw: &[u8]) -> Vec { - use crate::client::peer::{NotificationEffect, ServerMessage}; - String::from_utf8_lossy(raw) - .lines() - .filter_map(|l| l.strip_prefix("data:")) - .filter_map(|d| serde_json::from_str::(d.trim()).ok()) - .filter(|frame| { - matches!( - crate::client::peer::classify(frame), - Some(ServerMessage::Notification(n)) if n.effect() == NotificationEffect::RelayProgress - ) - }) - .collect() -} - -/// Which MRTR ask field, if any, a supposedly complete result still carries: the discriminator, or -/// either member that carries an ask's content. -#[must_use] -pub fn upstream_ask_field(value: &Value) -> Option<&'static str> { - let obj = value.as_object()?; - if obj.get("resultType").and_then(|v| v.as_str()) == Some("input_required") { - return Some("resultType"); - } - ["inputRequests", "requestState"] - .into_iter() - .find(|field| obj.contains_key(*field)) -} - -/// The tool-execution-error RESULT for an upstream leg that failed: `isError: true` with the -/// failure, busbar-attributed and naming the server, in one text block. An upstream's own words in -/// `reason` (its JSON-RPC error message) are carried unchanged (Law 11). -#[must_use] -pub fn upstream_failure_result(server: &str, reason: &str) -> Value { - json!({ - "resultType": "complete", - "isError": true, - "content": [{ - "type": "text", - "text": format!("The MCP server `{server}` did not complete this tool call: {reason}"), - }], - }) -} - -/// A success envelope with `resultType: complete` stamped on the result. -#[must_use] -pub fn result(id: &Value, mut value: Value) -> Vec { - if let Some(obj) = value.as_object_mut() { - obj.insert("resultType".into(), RESULT_TYPE_COMPLETE.into()); - } - let mut envelope = Map::new(); - envelope.insert("jsonrpc".into(), "2.0".into()); - envelope.insert("id".into(), id.clone()); - envelope.insert("result".into(), value); - serde_json::to_vec(&Value::Object(envelope)).unwrap_or_default() -} - -/// Why busbar declined to carry an upstream's ask to its caller, in the engine's words. Every arm is -/// busbar enforcing the operator's policy or the limits of the deployment; none answers the ask on -/// the caller's behalf (Law 11). -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum AskRefusal { - /// The operator granted this server nothing for this kind of ask. - Ungranted { - /// The server. - server: String, - /// The ask's kind. - kind: String, - }, - /// The hard cap on input-required rounds was reached. - RoundCapExceeded { - /// The server. - server: String, - /// The cap. - cap: u32, - }, - /// The deployment cannot seal the state the relayed ask is answered under (no signing key). - NoSealer { - /// The server. - server: String, - }, - /// The ask could not be carried to the caller: the work handle a stdio child's ask is - /// correlated under could not be opened. - Unavailable { - /// The server. - server: String, - /// Why. - reason: String, - }, -} - -impl std::fmt::Display for AskRefusal { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - AskRefusal::Ungranted { server, kind } => write!( - f, - "MCP server `{server}` asked its caller for a `{kind}` request, and that grant is \ - not held, so the ask was not relayed to you. Server-initiated asks are \ - deny-by-default: set `tools.{server}.grants.{kind}: true` if the operator intends \ - this server to put that ask to its callers." - ), - AskRefusal::RoundCapExceeded { server, cap } => write!( - f, - "MCP server `{server}` returned more than {cap} input-required rounds for one \ - dispatch, and no further round is relayed. The cap is hard: an upstream that can \ - ask indefinitely can amplify cost indefinitely. Raise `tools.{server}.max_input_required_rounds` only if this \ - exchange genuinely needs more rounds." - ), - AskRefusal::NoSealer { server } => write!( - f, - "MCP server `{server}` asked for input that would be relayed to you, but this \ - deployment has no `auth.signing_key` and cannot seal the `requestState` a relayed \ - ask is answered under. The ask is refused rather than relayed with unprotected \ - state." - ), - AskRefusal::Unavailable { server, reason } => write!( - f, - "the ask MCP server `{server}` made could not be relayed to busbar's caller: \ - {reason}" - ), - } - } -} - -impl AskRefusal { - /// The stable audit reason word. - #[must_use] - pub fn audit_reason(&self) -> &'static str { - match self { - AskRefusal::Ungranted { .. } => "ask_ungranted", - AskRefusal::RoundCapExceeded { .. } => "ask_round_cap", - AskRefusal::NoSealer { .. } => "ask_no_sealer", - AskRefusal::Unavailable { .. } => "ask_unavailable", - } - } -} - -/// What one far-end answer came to. -#[derive(Debug, Clone, PartialEq)] -pub enum Settled { - /// The caller's answer: the status, the body and the call-log line. - Answer { - /// The status. - status: u32, - /// The body. - body: Vec, - /// The call-log line. - line: CallLine, - }, - /// THE UPSTREAM ASKED ITS CALLER something the operator lets this server put to callers - /// (Law 11: busbar answers nothing on the caller's behalf): its result, relayed to the caller - /// with `inputRequests` verbatim and its state nested in busbar's sealed one. - Relay { - /// The upstream's `InputRequiredResult`, as it came. - result: Value, - /// The upstream round it asked on. - round: u32, - /// A stdio child's own requests, when that is what asked: `result` is then their - /// `inputRequests`, each verbatim ([`crate::tool_program::relayed_asks`]). - child: Option, - }, -} - -/// The kind word of an ask. -fn kind_word(kind: ServerAsk) -> &'static str { - match kind { - ServerAsk::Sampling => "sampling", - ServerAsk::Elicitation => "elicitation", - ServerAsk::Roots => "roots", - } -} - -/// SETTLE ONE ROUND's far-end answer. `status` is the far end's, `sse` = its answer is an event -/// stream, `round` the round it answered and `def` the registration of the member that answered. -#[must_use] -pub fn settle_call( - admitted: &AdmittedCall, - def: Option<&McpServerDefCfg>, - status: u32, - raw: &[u8], - sse: bool, - round: u32, -) -> Settled { - settle_call_as( - admitted, - def, - status, - raw, - sse, - round, - wire::dispatch_request_id(round), - ) -} - -/// [`settle_call`], the answer correlated to `sent_id`: the id the round's request carried (a -/// `transport: stdio` member's carries the unit's own, `crate::tool_program::call_id`). -#[must_use] -pub fn settle_call_as( - admitted: &AdmittedCall, - def: Option<&McpServerDefCfg>, - status: u32, - raw: &[u8], - sse: bool, - round: u32, - sent_id: u64, -) -> Settled { - let entry = &admitted.entry; - let id = &admitted.id; - let body = if sse { - last_sse_data(raw) - } else { - raw.to_vec() - }; - let failed = |reason: String| Settled::Answer { - status: STATUS_OK, - body: result(id, upstream_failure_result(&entry.server, &reason)), - line: CallLine::resolved( - entry, - vocab::OUTCOME_DISPATCHED, - vocab::REASON_UPSTREAM_FAILED, - ), - }; - match wire::parse_response(&body, sent_id) { - RpcOutcome::Result(value) => completed(admitted, &value, &body), - failure @ (RpcOutcome::Error { .. } - | RpcOutcome::Malformed(_) - | RpcOutcome::Uncorrelated(_)) => { - failed(failure_reason(status, &failure).unwrap_or_default()) - } - RpcOutcome::InputRequired { kind } => { - let kind = kind_word(kind); - let payload = serde_json::from_slice::(&body) - .ok() - .and_then(|v| v.get("result").cloned()) - .unwrap_or_else(|| json!({})); - match judge_ask(admitted, def, round, kind, &payload) { - Ok(settled) => settled, - Err(refusal) => ask_refused(admitted, &refusal), - } - } - } -} - -/// Why one round's answer is an UPSTREAM FAILURE (the call went out and the far end did not answer -/// it), in the engine's words; `None` for a result or an ask. -fn failure_reason(status: u32, outcome: &RpcOutcome) -> Option { - match outcome { - RpcOutcome::Error { code, message } => Some(format!( - "MCP upstream answered JSON-RPC error {code}: {message}" - )), - RpcOutcome::Malformed(reason) => Some(format!( - "MCP upstream returned HTTP {status} and a body that is not a JSON-RPC response: \ - {reason}" - )), - RpcOutcome::Uncorrelated(reason) => Some(format!( - "MCP upstream returned HTTP {status} and a JSON-RPC response busbar cannot correlate \ - to this call: {reason}" - )), - RpcOutcome::Result(_) | RpcOutcome::InputRequired { .. } => None, - } -} - -/// What one round's far-end answer is to a TASK (SEP-2663's statuses, the served engine's task -/// runner): the tool's own result, which the task inlines as it came; an upstream failure, which -/// fails the task; or an ask, which the rounds already judged. -#[derive(Debug, Clone, PartialEq)] -pub enum Leg { - /// The far end answered a result: `completed`, `isError` and all. - Done(Value), - /// The call went out and the far end did not answer it: `failed`, in the engine's words. - Failed(String), - /// The far end asked for something: the rounds settled it, and their answer says how. - Asked, -} - -/// The far end's answer to round `round` (`status`, the raw answer, `sse` = an event stream), as a -/// task reads it ([`Leg`]). -#[must_use] -pub fn leg_of(status: u32, raw: &[u8], sse: bool, round: u32) -> Leg { - let body = if sse { - last_sse_data(raw) - } else { - raw.to_vec() - }; - let outcome = wire::parse_response(&body, wire::dispatch_request_id(round)); - if let Some(reason) = failure_reason(status, &outcome) { - return Leg::Failed(reason); - } - match outcome { - RpcOutcome::Result(value) => Leg::Done(value), - _ => Leg::Asked, - } -} - -/// The call-log line of a call answered on the task path under `reason`: `refused`, because at the -/// moment the caller is answered nothing has gone out (the served engine recorded a task it created -/// as `refused`/`task_created`). Its audit row is applied for a task created (the action — starting -/// the work — was), rejected otherwise. -#[must_use] -pub fn task_line(entry: &ToolEntry, reason: &str) -> CallLine { - let mut line = CallLine::resolved(entry, vocab::OUTCOME_REFUSED, reason); - if let Some(row) = &mut line.audit { - row.applied = reason == vocab::REASON_TASK_CREATED; - } - line -} - -/// The ask judged: the bound, then the grant of EVERY kind it asks for (an unknown method is judged -/// as the most privileged, as the recogniser reads it). A granted ask is relayed to the caller; -/// busbar answers none itself. -fn judge_ask( - admitted: &AdmittedCall, - def: Option<&McpServerDefCfg>, - round: u32, - kind: &'static str, - payload: &Value, -) -> Result { - let server = admitted.entry.server.clone(); - let cap = def - .and_then(|d| d.max_input_required_rounds) - .unwrap_or(DEFAULT_MAX_INPUT_REQUIRED_ROUNDS); - if round >= cap { - return Err(AskRefusal::RoundCapExceeded { server, cap }); - } - let grants = def.map(|d| d.grants).unwrap_or_default(); - let mut kinds: Vec<&str> = payload - .get("inputRequests") - .and_then(Value::as_object) - .map(|requests| { - requests - .values() - .map(|r| match r.get("method").and_then(Value::as_str) { - Some("elicitation/create") => "elicitation", - Some("roots/list") => "roots", - _ => "sampling", - }) - .collect() - }) - .unwrap_or_default(); - kinds.push(kind); - if let Some(denied) = kinds.into_iter().find(|k| !grants.allows(k)) { - return Err(AskRefusal::Ungranted { - server, - kind: denied.to_string(), - }); - } - Ok(Settled::Relay { - result: payload.clone(), - round, - child: None, - }) -} - -/// The call-log line of a call whose upstream's ask was relayed to its caller: it went out and was -/// answered (`dispatched`, `ask_relayed`), audited as busbar's ask of its caller, applied. -#[must_use] -pub fn relayed_line(entry: &ToolEntry) -> CallLine { - CallLine::resolved(entry, vocab::OUTCOME_DISPATCHED, REASON_ASK_RELAYED).asking(true) -} - -/// The call-log line of a relayed ask's retry refused under `reason` (its state, or the work handle -/// it is correlated under): audited as busbar's ask of its caller, rejected. -#[must_use] -pub fn retry_refused_line(entry: &ToolEntry, reason: &str) -> CallLine { - CallLine::resolved(entry, vocab::OUTCOME_REFUSED, reason).asking(false) -} - -/// An upstream ask busbar declined: `403`, `-32000`, the refusal's words and reason. -#[must_use] -pub fn ask_refused(admitted: &AdmittedCall, refusal: &AskRefusal) -> Settled { - let reason = refusal.audit_reason(); - Settled::Answer { - status: STATUS_FORBIDDEN, - body: catalogue_refusal(STATUS_FORBIDDEN, &admitted.id, refusal.to_string(), reason).body(), - line: CallLine::resolved(&admitted.entry, vocab::OUTCOME_REFUSED, reason), - } -} - -/// A finished result: the terminal ask check, then the upstream's result relayed as it came -/// (Law 11). `value` is the result parsed, `body` the answer it was parsed from. -fn completed(admitted: &AdmittedCall, value: &Value, body: &[u8]) -> Settled { - let entry = &admitted.entry; - let id = &admitted.id; - if let Some(field) = upstream_ask_field(value) { - return Settled::Answer { - status: STATUS_FORBIDDEN, - body: catalogue_refusal( - STATUS_FORBIDDEN, - id, - format!( - "MCP server `{}` answered with a complete result that still carries an ask's \ - `{field}`. An upstream's ask is relayed to you only as an input-required \ - result, under the operator's grant; this answer is neither a complete result \ - nor such an ask, so it is not served.", - entry.server - ), - REASON_ASK_NOT_PROXIED, - ) - .body(), - line: CallLine::resolved(entry, vocab::OUTCOME_REFUSED, REASON_ASK_NOT_PROXIED), - }; - } - Settled::Answer { - status: STATUS_OK, - body: relayed(id, value, body), - line: CallLine::resolved(entry, vocab::OUTCOME_DISPATCHED, ""), - } -} - -/// The caller's answer carrying the upstream's `result` as the upstream wrote it: its own bytes, -/// under the caller's id. The one addition is the dialect's `resultType: complete` on a result -/// object that carries no `resultType` at all; a `resultType` the upstream sent is its own. -fn relayed(id: &Value, value: &Value, body: &[u8]) -> Vec { - let Some(sent) = result_bytes(body) else { - return result(id, value.clone()); - }; - let mut out = format!("{{\"id\":{id},\"jsonrpc\":\"2.0\",\"result\":"); - match sent.trim_start().strip_prefix('{') { - Some(members) if value.get("resultType").is_none() => { - out.push_str("{\"resultType\":\""); - out.push_str(RESULT_TYPE_COMPLETE); - out.push('"'); - if !members.trim_start().starts_with('}') { - out.push(','); - } - out.push_str(members); - } - _ => out.push_str(sent), - } - out.push('}'); - out.into_bytes() -} - -/// The upstream's `result` member as it sits in `body`: its value's own bytes, found by reading -/// the answer's top-level members in turn (each value read and its extent taken, never re-written, -/// so serde_json's `raw_value` feature is not asked for: the binary would unify it into every -/// linked plugin). `None` for an answer that is not one JSON object with exactly one `result`. -fn result_bytes(body: &[u8]) -> Option<&str> { - let text = std::str::from_utf8(body).ok()?; - let ws = |at: usize| text.len() - text[at..].trim_start_matches([' ', '\t', '\n', '\r']).len(); - let mut at = ws(0); - if text.as_bytes().get(at) != Some(&b'{') { - return None; - } - at = ws(at + 1); - let mut found = None; - if text.as_bytes().get(at) != Some(&b'}') { - loop { - let mut keys = serde_json::Deserializer::from_str(&text[at..]).into_iter::(); - let key = keys.next()?.ok()?; - at = ws(at + keys.byte_offset()); - if text.as_bytes().get(at) != Some(&b':') { - return None; - } - let start = ws(at + 1); - let mut values = serde_json::Deserializer::from_str(&text[start..]) - .into_iter::(); - values.next()?.ok()?; - let end = start + values.byte_offset(); - if key == "result" && found.replace(&text[start..end]).is_some() { - return None; - } - at = ws(end); - match text.as_bytes().get(at)? { - b',' => at = ws(at + 1), - b'}' => break, - _ => return None, - } - } - } - if ws(at + 1) != text.len() { - return None; - } - found -} - -#[cfg(test)] -#[path = "tests/call.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/catalogue.rs b/crates/busbar-plane-mcp/src/catalogue.rs deleted file mode 100644 index e012ba1876..0000000000 --- a/crates/busbar-plane-mcp/src/catalogue.rs +++ /dev/null @@ -1,569 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE CATALOGUE THE DOOR ANSWERS FROM: every capability the operator's `tools:` section -//! registers, built once per generation from the section that generation's snapshot was published -//! over. An approval is a change to the section, so it is visible from the generation that carries -//! it and from no earlier one. -//! -//! Pure. What a caller may see is not judged in this module: each listing asks an `admit` -//! predicate, once per grant, with the two grants every capability needs (the server first, then -//! the capability's published name). The door binds the predicate to the kernel's entitlement -//! service; a test binds it to a table. The rendering is the served engine's, member for member, -//! and every cacheable result carries the same two caching hints. -//! -//! The catalogue LISTS what it will not dispatch: a tool with no approved hash is listed, so an -//! operator can see what is waiting. Only a tool whose live sighting is quarantined is hidden, and -//! that verdict is a second predicate, asked after the grants and never in place of them. - -use std::collections::BTreeMap; - -use serde_json::{json, Map, Value}; - -use crate::door::{SCOPE, SCOPE_TOOL}; -use crate::jsonrpc::RESULT_TYPE_COMPLETE; -use crate::sanitize::normalise_opt; -use crate::tools_config::{namespaced, AskRoundCfg, PromptMessageCfg, TaskSupport, ToolsCfg}; - -/// The caching scope every cacheable result names: a result is scoped to its caller's grant. -pub const CACHE_SCOPE: &str = "private"; - -/// The caching lifetime every cacheable result names: none, as an approval can move at any time. -pub const CACHE_TTL_MS: i64 = 0; - -/// One listed tool. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ToolEntry { - /// The registered server id. - pub server: String, - /// The tool's name as the upstream spells it. - pub tool: String, - /// The published name: `publish_as`, else `{server}_{tool}`. - pub namespaced: String, - /// The approved schema hash; a blank one is none. - pub schema_hash: Option, - description: Option, - pub(crate) input_schema: Option, - pub(crate) output_schema: Option, - /// The rounds of input asked of the caller before this tool is called; empty = none. - pub ask_caller: Vec, - /// Whether a call may, or must, be answered with a task. - pub task_support: TaskSupport, - /// The rounds of input asked of the caller from INSIDE a task, before its upstream call. - pub task_ask_caller: Vec, -} - -/// One listed prompt. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct PromptEntry { - /// The registered server id. - pub server: String, - /// The prompt's name as the operator wrote it. - pub name: String, - /// `{server}_{name}`. - pub namespaced: String, - pub(crate) description: Option, - pub(crate) template: Option, - pub(crate) messages: Vec, - /// The rounds of input asked of the caller before this prompt renders; empty = none. - pub ask_caller: Vec, -} - -/// One listed resource. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ResourceEntry { - /// The registered server id. - pub server: String, - /// The resource's own uri, which is what a caller reads it by. - pub uri: String, - /// `{server}_{uri}`: the grant value. - pub namespaced: String, - name: Option, - description: Option, - pub(crate) mime_type: Option, - pub(crate) text: Option, - pub(crate) blob: Option, -} - -impl ResourceEntry { - /// Its rendered fields, in order: uri, name, description, mime type, text, blob (a blank one - /// for each it has not). - #[must_use] - pub fn rendered(&self) -> [&str; 6] { - [ - self.uri.as_str(), - self.name.as_deref().unwrap_or(""), - self.description.as_deref().unwrap_or(""), - self.mime_type.as_deref().unwrap_or(""), - self.text.as_deref().unwrap_or(""), - self.blob.as_deref().unwrap_or(""), - ] - } -} - -/// One listed resource template. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ResourceTemplateEntry { - /// The registered server id. - pub server: String, - /// The template as the operator wrote it. - pub uri_template: String, - /// `{server}_{uri_template}`: the grant value. - pub namespaced: String, - name: Option, - description: Option, - pub(crate) mime_type: Option, - pub(crate) text: Option, -} - -/// ONE GENERATION'S CATALOGUE. Built whole from one section and never changed. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Catalogue { - generation: u64, - servers: Vec, - tools: BTreeMap, - prompts: BTreeMap, - resources: BTreeMap, - resource_templates: BTreeMap, -} - -/// Which approval an address names, after the caller's grants narrowed the field. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Lookup { - /// Exactly one. - One(T), - /// None, or none the caller may see: one answer for both. - NotFound, - /// More than one the caller may see, named and sorted. - Ambiguous(Vec), -} - -/// MATCH one level-1 uri template against a concrete uri. A binding is non-empty and holds no `/`, -/// so one template approves one shape and never a subtree. -#[must_use] -pub fn match_uri_template(template: &str, uri: &str) -> Option> { - let mut bindings = BTreeMap::new(); - let mut t = template; - let mut u = uri; - loop { - let Some(open) = t.find('{') else { - return (t == u).then_some(bindings); - }; - let literal = &t[..open]; - u = u.strip_prefix(literal)?; - let after = &t[open + 1..]; - let close = after.find('}')?; - let name = &after[..close]; - t = &after[close + 1..]; - let stop = &t[..t.find('{').unwrap_or(t.len())]; - let value = if stop.is_empty() { - std::mem::take(&mut u) - } else { - let at = u.find(stop)?; - let (v, rest) = u.split_at(at); - u = rest; - v - }; - if value.is_empty() || value.contains('/') { - return None; - } - bindings.insert(name.to_string(), value.to_string()); - } -} - -/// A blank hash is no hash. -fn approved_hash(raw: Option<&str>) -> Option { - raw.map(str::trim) - .filter(|h| !h.is_empty()) - .map(str::to_string) -} - -/// Both grants a capability needs: the server first, then the capability. -pub(crate) fn granted(admit: &impl Fn(&str, &str) -> bool, server: &str, namespaced: &str) -> bool { - admit(SCOPE, server) && admit(SCOPE_TOOL, namespaced) -} - -/// A cacheable result with its two hints. -fn cache_hints(mut value: Value) -> Value { - if let Some(obj) = value.as_object_mut() { - obj.insert("cacheScope".into(), CACHE_SCOPE.into()); - obj.insert("ttlMs".into(), CACHE_TTL_MS.into()); - } - value -} - -/// A successful answer: the result stamped `complete`, the id echoed. -#[must_use] -pub fn complete(id: &Value, mut value: Value) -> Vec { - if let Some(obj) = value.as_object_mut() { - obj.insert("resultType".into(), RESULT_TYPE_COMPLETE.into()); - } - let mut envelope = Map::new(); - envelope.insert("jsonrpc".into(), "2.0".into()); - envelope.insert("id".into(), id.clone()); - envelope.insert("result".into(), value); - serde_json::to_vec(&Value::Object(envelope)).unwrap_or_default() -} - -impl Catalogue { - /// BUILD one generation's catalogue from the section it was published over. - #[must_use] - pub fn build(generation: u64, cfg: &ToolsCfg) -> Self { - let mut catalogue = Catalogue { - generation, - servers: cfg.servers.keys().cloned().collect(), - tools: BTreeMap::new(), - prompts: BTreeMap::new(), - resources: BTreeMap::new(), - resource_templates: BTreeMap::new(), - }; - for (id, def) in &cfg.servers { - for (tool, allow) in &def.tools_allow { - let published = allow - .publish_as - .clone() - .unwrap_or_else(|| namespaced(id, tool)); - catalogue.tools.insert( - published.clone(), - ToolEntry { - server: id.clone(), - tool: tool.clone(), - namespaced: published, - schema_hash: approved_hash(allow.schema_hash.as_deref()), - description: allow.description.clone(), - input_schema: allow.input_schema.clone(), - output_schema: allow.output_schema.clone(), - ask_caller: allow.ask_caller.clone(), - task_support: allow.task_support, - task_ask_caller: allow.task_ask_caller.clone(), - }, - ); - } - for (name, allow) in &def.prompts_allow { - let key = namespaced(id, name); - catalogue.prompts.insert( - key.clone(), - PromptEntry { - server: id.clone(), - name: name.clone(), - namespaced: key, - description: allow.description.clone(), - template: allow.template.clone(), - messages: allow.messages.clone(), - ask_caller: allow.ask_caller.clone(), - }, - ); - } - for (uri, allow) in &def.resources_allow { - let key = namespaced(id, uri); - catalogue.resources.insert( - key.clone(), - ResourceEntry { - server: id.clone(), - uri: uri.clone(), - namespaced: key, - name: allow.name.clone(), - description: allow.description.clone(), - mime_type: allow.mime_type.clone(), - text: allow.text.clone(), - blob: allow.blob.clone(), - }, - ); - } - for (template, allow) in &def.resource_templates_allow { - let key = namespaced(id, template); - catalogue.resource_templates.insert( - key.clone(), - ResourceTemplateEntry { - server: id.clone(), - uri_template: template.clone(), - namespaced: key, - name: allow.name.clone(), - description: allow.description.clone(), - mime_type: allow.mime_type.clone(), - text: allow.text.clone(), - }, - ); - } - } - catalogue - } - - /// The generation this catalogue was built for. - #[must_use] - pub fn generation(&self) -> u64 { - self.generation - } - - /// Whether the section registers no server at all. - #[must_use] - pub fn is_empty(&self) -> bool { - self.servers.is_empty() - } - - /// EVERY GRANT A REQUEST ON THIS CATALOGUE CAN ASK, `(scope kind, name)`: each server, then each - /// capability's published name. The door asks the kernel each one once, so the `admit` - /// predicate it hands the listings is a table rather than a call. - #[must_use] - pub fn grants(&self) -> Vec<(&'static str, &str)> { - let servers = self.servers.iter().map(|s| (SCOPE, s.as_str())); - let tools = self.tools.keys().map(|n| (SCOPE_TOOL, n.as_str())); - let prompts = self.prompts.keys().map(|n| (SCOPE_TOOL, n.as_str())); - let resources = self.resources.keys().map(|n| (SCOPE_TOOL, n.as_str())); - let templates = self - .resource_templates - .keys() - .map(|n| (SCOPE_TOOL, n.as_str())); - servers - .chain(tools) - .chain(prompts) - .chain(resources) - .chain(templates) - .collect() - } - - /// The entry a published tool name names, whoever asks. - #[must_use] - pub fn tool(&self, published: &str) -> Option<&ToolEntry> { - self.tools.get(published) - } - - /// The entry of the tool the upstream spells `bare` on `server` (a pool member's own twin of a - /// tool called on another member), whatever it is published as. - #[must_use] - pub fn tool_on(&self, server: &str, bare: &str) -> Option<&ToolEntry> { - self.tools - .values() - .find(|t| t.server == server && t.tool == bare) - } - - /// Every tool registration `server` publishes, whoever asks, in published-name order: the items - /// a live re-fetch of that server sights on the kernel's trust book. - pub fn tools_of<'a>(&'a self, server: &'a str) -> impl Iterator + 'a { - self.tools.values().filter(move |t| t.server == server) - } - - /// The tools this caller may see, in published-name order. - pub fn tools_for(&self, admit: &impl Fn(&str, &str) -> bool) -> Vec<&ToolEntry> { - self.tools - .values() - .filter(|t| granted(admit, &t.server, &t.namespaced)) - .collect() - } - - /// The prompts this caller may see. - pub fn prompts_for(&self, admit: &impl Fn(&str, &str) -> bool) -> Vec<&PromptEntry> { - self.prompts - .values() - .filter(|p| granted(admit, &p.server, &p.namespaced)) - .collect() - } - - /// The resources this caller may see. - pub fn resources_for(&self, admit: &impl Fn(&str, &str) -> bool) -> Vec<&ResourceEntry> { - self.resources - .values() - .filter(|r| granted(admit, &r.server, &r.namespaced)) - .collect() - } - - /// The resource templates this caller may see. - pub fn resource_templates_for( - &self, - admit: &impl Fn(&str, &str) -> bool, - ) -> Vec<&ResourceTemplateEntry> { - self.resource_templates - .values() - .filter(|t| granted(admit, &t.server, &t.namespaced)) - .collect() - } - - /// The prompt a published name names, under the caller's grants. `None` covers no such prompt - /// and not the caller's alike, so the answer leaks nothing about what the grant hides. - pub fn prompt_for( - &self, - admit: &impl Fn(&str, &str) -> bool, - published: &str, - ) -> Option<&PromptEntry> { - self.prompts - .get(published) - .filter(|p| granted(admit, &p.server, &p.namespaced)) - } - - /// The resource a caller's uri names, among those its grants reach. Two reachable approvals of - /// one uri are named, sorted by server, never picked between. - pub fn resource_by_uri( - &self, - admit: &impl Fn(&str, &str) -> bool, - uri: &str, - ) -> Lookup<&ResourceEntry> { - let mut found: Vec<&ResourceEntry> = self - .resources_for(admit) - .into_iter() - .filter(|r| r.uri == uri) - .collect(); - match found.len() { - 0 => Lookup::NotFound, - 1 => Lookup::One(found.remove(0)), - _ => { - let mut servers: Vec = found.iter().map(|r| r.server.clone()).collect(); - servers.sort(); - Lookup::Ambiguous(servers) - } - } - } - - /// The template a caller's expanded uri matches, among those its grants reach, with its - /// bindings. Two matches are named by their namespaced approvals, sorted, never picked between. - pub fn resource_template_for( - &self, - admit: &impl Fn(&str, &str) -> bool, - uri: &str, - ) -> Lookup<(&ResourceTemplateEntry, BTreeMap)> { - let mut found: Vec<(&ResourceTemplateEntry, BTreeMap)> = self - .resource_templates_for(admit) - .into_iter() - .filter_map(|t| match_uri_template(&t.uri_template, uri).map(|b| (t, b))) - .collect(); - match found.len() { - 0 => Lookup::NotFound, - 1 => Lookup::One(found.remove(0)), - _ => { - let mut names: Vec = - found.iter().map(|(t, _)| t.namespaced.clone()).collect(); - names.sort(); - Lookup::Ambiguous(names) - } - } - } - - /// `tools/list`: the caller's tools, minus any the kernel's trust verdict hides (`hidden`). - pub fn tools_list( - &self, - id: &Value, - admit: &impl Fn(&str, &str) -> bool, - hidden: impl Fn(&ToolEntry) -> bool, - ) -> Vec { - let tools: Vec = self - .tools_for(admit) - .into_iter() - .filter(|t| !hidden(t)) - .map(ToolEntry::render) - .collect(); - complete(id, cache_hints(json!({ "tools": tools }))) - } - - /// `prompts/list`. - pub fn prompts_list(&self, id: &Value, admit: &impl Fn(&str, &str) -> bool) -> Vec { - let prompts: Vec = self - .prompts_for(admit) - .into_iter() - .map(PromptEntry::render) - .collect(); - complete(id, cache_hints(json!({ "prompts": prompts }))) - } - - /// `resources/list`. - pub fn resources_list(&self, id: &Value, admit: &impl Fn(&str, &str) -> bool) -> Vec { - let resources: Vec = self - .resources_for(admit) - .into_iter() - .map(ResourceEntry::render) - .collect(); - complete(id, cache_hints(json!({ "resources": resources }))) - } - - /// `resources/templates/list`: the empty list for a caller who reaches none. - pub fn resource_templates_list( - &self, - id: &Value, - admit: &impl Fn(&str, &str) -> bool, - ) -> Vec { - let templates: Vec = self - .resource_templates_for(admit) - .into_iter() - .map(ResourceTemplateEntry::render) - .collect(); - complete(id, cache_hints(json!({ "resourceTemplates": templates }))) - } -} - -impl ToolEntry { - /// The entry as `tools/list` writes it. - #[must_use] - pub fn render(&self) -> Value { - let mut obj = Map::new(); - obj.insert("name".into(), self.namespaced.clone().into()); - if let Some(d) = normalise_opt(self.description.as_deref()) { - obj.insert("description".into(), d.into()); - } - obj.insert( - "inputSchema".into(), - self.input_schema - .clone() - .unwrap_or_else(|| json!({ "type": "object" })), - ); - if let Some(s) = &self.output_schema { - obj.insert("outputSchema".into(), s.clone()); - } - if let Some(h) = &self.schema_hash { - obj.insert("_meta".into(), json!({ "io.busbar/schemaHash": h })); - } - Value::Object(obj) - } -} - -impl PromptEntry { - /// The entry as `prompts/list` writes it. - #[must_use] - pub fn render(&self) -> Value { - let mut obj = Map::new(); - obj.insert("name".into(), self.namespaced.clone().into()); - if let Some(d) = normalise_opt(self.description.as_deref()) { - obj.insert("description".into(), d.into()); - } - Value::Object(obj) - } -} - -impl ResourceEntry { - /// The entry as `resources/list` writes it: the resource's own uri. - #[must_use] - pub fn render(&self) -> Value { - let mut obj = Map::new(); - obj.insert("uri".into(), self.uri.clone().into()); - if let Some(n) = normalise_opt(self.name.as_deref()) { - obj.insert("name".into(), n.into()); - } - if let Some(d) = normalise_opt(self.description.as_deref()) { - obj.insert("description".into(), d.into()); - } - if let Some(m) = &self.mime_type { - obj.insert("mimeType".into(), m.clone().into()); - } - Value::Object(obj) - } -} - -impl ResourceTemplateEntry { - /// The entry as `resources/templates/list` writes it: the operator's own template. - #[must_use] - pub fn render(&self) -> Value { - let mut obj = Map::new(); - obj.insert("uriTemplate".into(), self.uri_template.clone().into()); - if let Some(n) = normalise_opt(self.name.as_deref()) { - obj.insert("name".into(), n.into()); - } - if let Some(d) = normalise_opt(self.description.as_deref()) { - obj.insert("description".into(), d.into()); - } - if let Some(m) = &self.mime_type { - obj.insert("mimeType".into(), m.clone().into()); - } - Value::Object(obj) - } -} - -#[cfg(test)] -#[path = "tests/catalogue.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/checks.rs b/crates/busbar-plane-mcp/src/checks.rs deleted file mode 100644 index 9e3a934ce3..0000000000 --- a/crates/busbar-plane-mcp/src/checks.rs +++ /dev/null @@ -1,187 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE STATELESS REVISION'S ENVELOPE CHECKS: `params._meta` and its required members, the mirrored -//! request fields, and the revision the request names. Pure: the request body and a reader over the -//! request's head fields in, the first refusal out. -//! -//! The order is the served engine's, and so are the words: a body defect is answered in the body's -//! own vocabulary (`-32602`), a disagreement between an intermediary's mirror and the body is one -//! class of its own (`-32020`), and an unsupported revision is judged last, only once the request is -//! otherwise well formed, because its `data.supported` is an invitation to retry (`-32022`). Every -//! refusal here is `400`. - -use serde_json::Value; - -use crate::codec::{ - name_source_of, CODE_HEADER_MISMATCH, CODE_INVALID_PARAMS, CODE_UNSUPPORTED_PROTOCOL_VERSION, - H_MCP_METHOD, H_MCP_NAME, H_PROTOCOL_VERSION, META_CLIENT_CAPABILITIES, META_PROTOCOL_VERSION, - PROTOCOL_VERSION, -}; - -/// The revisions the STATELESS dispatch serves, as `data.supported` names them: the revision a -/// request's own `_meta` may name. The session revisions are never named there, because none of -/// them is carried in `_meta`: a client of theirs opens a session with `initialize`, whose answer -/// negotiates the revision ([`crate::revision::negotiate`]). -pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &[PROTOCOL_VERSION]; - -/// The status every refusal here is answered with. -pub const STATUS: u32 = 400; - -/// One refusal: its code, its sentence and its `data`. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Refused { - /// The JSON-RPC error code. - pub code: i64, - /// The sentence. - pub message: &'static str, - /// The `data` member, where the refusal carries one. - pub data: Option, -} - -impl Refused { - fn invalid_params(message: &'static str) -> Self { - Refused { - code: CODE_INVALID_PARAMS, - message, - data: None, - } - } - - fn header_mismatch(message: &'static str) -> Self { - Refused { - code: CODE_HEADER_MISMATCH, - message, - data: None, - } - } - - /// The refusal as the wire carries it: the one JSON-RPC error envelope, `id` or `null`. - #[must_use] - pub fn body(&self, id: Option<&Value>) -> Vec { - let envelope = busbar_contract::jsonrpc::error_body( - id.cloned().unwrap_or(Value::Null), - self.code, - self.message, - self.data.clone(), - ); - serde_json::to_vec(&envelope).unwrap_or_default() - } -} - -/// A `Mcp-Name` (or `Mcp-Param-*`) value with this revision's `=?base64?…?=` sentinel decoded, or the -/// value as it stands when it carries none. `None` when the sentinel is not valid Base64 or not -/// UTF-8. -#[must_use] -pub fn decode_sentinel(value: &str) -> Option { - use base64::Engine as _; - let Some(inner) = value - .strip_prefix("=?base64?") - .and_then(|v| v.strip_suffix("?=")) - else { - return Some(value.to_string()); - }; - let bytes = base64::engine::general_purpose::STANDARD - .decode(inner) - .ok()?; - String::from_utf8(bytes).ok() -} - -/// THE CHECKS, in the served order. `field` reads one request head field by its lower-case name. -/// -/// # Errors -/// -/// The first refusal the request earns. -pub fn check_request<'a>( - value: &Value, - method: &str, - field: impl Fn(&str) -> Option<&'a str>, -) -> Result<(), Refused> { - // `params._meta` AND ITS REQUIRED MEMBERS: a body defect, `-32602`. - let Some(meta) = value.get("params").and_then(|p| p.get("_meta")) else { - return Err(Refused::invalid_params( - "`params._meta` is required on every request. This revision has no handshake, so each \ - request states its own protocol version and client capabilities there.", - )); - }; - let Some(body_version) = meta - .get(META_PROTOCOL_VERSION) - .and_then(Value::as_str) - .filter(|v| !v.is_empty()) - else { - return Err(Refused::invalid_params( - "`params._meta` must carry `io.modelcontextprotocol/protocolVersion`; this revision \ - negotiates per request, so the version cannot be inferred.", - )); - }; - if meta.get(META_CLIENT_CAPABILITIES).is_none() { - return Err(Refused::invalid_params( - "`params._meta` must carry `io.modelcontextprotocol/clientCapabilities`. With no \ - handshake there is no earlier message it could have been declared in, and this server \ - will not decide on a client's behalf what that client can do; send `{}` to declare \ - none.", - )); - } - - // THE MIRRORED FIELDS: one class, an intermediary and the executor disagreeing, `-32020`. - // Values are compared case-sensitively. - let Some(header_version) = field(H_PROTOCOL_VERSION) else { - return Err(Refused::header_mismatch( - "Every POST to the MCP endpoint must carry an `MCP-Protocol-Version` header.", - )); - }; - if header_version != body_version { - return Err(Refused::header_mismatch( - "The `MCP-Protocol-Version` header does not match the body's \ - `_meta.io.modelcontextprotocol/protocolVersion`.", - )); - } - let Some(header_method) = field(H_MCP_METHOD) else { - return Err(Refused::header_mismatch( - "The `Mcp-Method` header is required on every request.", - )); - }; - if decode_sentinel(header_method).as_deref() != Some(method) { - return Err(Refused::header_mismatch( - "The `Mcp-Method` header does not match the body's `method`.", - )); - } - if let Some(source) = name_source_of(method) { - let body_name = value - .get("params") - .and_then(|p| p.get(source)) - .and_then(Value::as_str); - let Some(header_name) = field(H_MCP_NAME) else { - return Err(Refused::header_mismatch( - "The `Mcp-Name` header is required on tools/call, resources/read and prompts/get.", - )); - }; - let Some(decoded) = decode_sentinel(header_name) else { - return Err(Refused::header_mismatch( - "The `Mcp-Name` header carries a `=?base64?…?=` sentinel that is not valid Base64.", - )); - }; - if body_name != Some(decoded.as_str()) { - return Err(Refused::header_mismatch( - "The `Mcp-Name` header does not match the body's target name.", - )); - } - } - - // VERSION SUPPORT, only now, with a well-formed request in hand: `-32022`. - if !SUPPORTED_PROTOCOL_VERSIONS.contains(&body_version) { - return Err(Refused { - code: CODE_UNSUPPORTED_PROTOCOL_VERSION, - message: "This server does not implement the requested MCP protocol version.", - data: Some(serde_json::json!({ - "requested": body_version, - "supported": SUPPORTED_PROTOCOL_VERSIONS, - })), - }); - } - Ok(()) -} - -#[cfg(test)] -#[path = "tests/checks.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/client/compat.rs b/crates/busbar-plane-mcp/src/client/compat.rs deleted file mode 100644 index 019b6fb030..0000000000 --- a/crates/busbar-plane-mcp/src/client/compat.rs +++ /dev/null @@ -1,401 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE CLIENT HALF OF THE SESSION REVISIONS: how busbar, calling an upstream, speaks -//! `2025-11-25`, `2025-06-18` or `2024-11-05` when the upstream does not speak the stateless -//! `2026-07-28` revision busbar sends by default (OWNER 2026-09-29 compat scope; MCP-COMPAT C5). -//! -//! Every request busbar issues is built once, in the stateless shape, by [`super::jsonrpc`] and -//! [`super::verb`]. This module does not build a second set: it LOWERS a built request into a -//! session revision ([`lower_request`]) and reads the upstream's answers back. The ladder itself -//! (stateless, then `initialize`, then the event stream, moving only on a refusal) is -//! [`crate::revision::next_step`]; [`probe_outcome`] is what feeds it. -//! -//! Pure: no I/O, no clock, no randomness. The caller owns the connection and the memory of what -//! each upstream negotiated ([`crate::tool_sessions::UpstreamTable`]). - -use serde_json::Value; - -use super::jsonrpc::OutboundRequest; -use crate::codec::{ - CODE_INVALID_PARAMS, CODE_INVALID_REQUEST, CODE_METHOD_NOT_FOUND, - CODE_UNSUPPORTED_PROTOCOL_VERSION, H_MCP_METHOD, H_MCP_NAME, H_PROTOCOL_VERSION, - META_CLIENT_CAPABILITIES, META_PROTOCOL_VERSION, -}; -use crate::revision::{Revision, StepOutcome}; - -/// The response header an upstream names its session in, and the request header busbar carries it -/// back in. -pub const H_SESSION: &str = crate::adapt::H_SESSION_ID; - -/// Reads what a stateless request's answer says about the upstream's revision. -/// -/// A status in `400..500`, or a JSON-RPC error that says the method, the request shape or the -/// revision is not understood, is a refusal of the revision and moves the ladder. A `5xx`, or no -/// answer, says nothing about the revision. Anything else answered. -#[must_use] -pub fn probe_outcome(status: u16, body: &[u8]) -> StepOutcome { - if (500..600).contains(&status) { - return StepOutcome::Unreachable; - } - if (400..500).contains(&status) { - return StepOutcome::Refused4xx; - } - let code = serde_json::from_slice::(body) - .ok() - .and_then(|v| v.pointer("/error/code").and_then(Value::as_i64)); - match code { - Some(c) - if c == CODE_METHOD_NOT_FOUND - || c == CODE_INVALID_REQUEST - || c == CODE_INVALID_PARAMS - || c == CODE_UNSUPPORTED_PROTOCOL_VERSION => - { - StepOutcome::Refused4xx - } - _ => StepOutcome::Answered, - } -} - -/// The `initialize` request, sent to `url` with the credential `template` carries. `client_version` -/// is the caller's own version string (the plane reads no build metadata). -#[must_use] -pub fn initialize_request( - template: &OutboundRequest, - url: &str, - asked: Revision, - request_id: u64, - client_version: &str, -) -> OutboundRequest { - let body = serde_json::json!({ - "jsonrpc": "2.0", - "id": request_id, - "method": crate::adapt::METHOD_INITIALIZE, - "params": { - "protocolVersion": asked.wire(), - "capabilities": {}, - "clientInfo": { "name": "busbar", "version": client_version }, - }, - }); - session_envelope(template, url, &body, None, None) -} - -/// `notifications/initialized`, completing the opening of `revision`'s session. -#[must_use] -pub fn initialized_notification( - template: &OutboundRequest, - url: &str, - revision: Revision, - session: Option<&str>, -) -> OutboundRequest { - let body = serde_json::json!({ - "jsonrpc": "2.0", - "method": crate::adapt::METHOD_INITIALIZED, - }); - session_envelope(template, url, &body, Some(revision), session) -} - -/// The revision an `initialize` answer names, when this plane can carry it. `None` is an upstream -/// busbar must not continue with: the spec's rule for a client offered a revision it cannot speak. -#[must_use] -pub fn offered_revision(body: &[u8]) -> Option { - let v: Value = serde_json::from_slice(body).ok()?; - crate::revision::accept_offered(v.pointer("/result/protocolVersion")?.as_str()?) -} - -/// LOWERS a stateless request into `revision`: the stateless `_meta` members go (a session -/// revision states them once, in `initialize`), the stateless mirror headers go, the version -/// header names `revision` (where the revision has one) and the session header names `session`. -/// The URL is `url` (the endpoint, or a `2024-11-05` message address). The credential and every -/// other header `req` carries are kept. -#[must_use] -pub fn lower_request( - req: &OutboundRequest, - url: &str, - revision: Revision, - session: Option<&str>, -) -> OutboundRequest { - let mut body: Value = serde_json::from_slice(&req.body).unwrap_or(Value::Null); - if let Some(params) = body.get_mut("params").and_then(Value::as_object_mut) { - let empty = params - .get_mut("_meta") - .and_then(Value::as_object_mut) - .map(|meta| { - meta.remove(META_PROTOCOL_VERSION); - meta.remove(META_CLIENT_CAPABILITIES); - meta.is_empty() - }) - .unwrap_or(false); - if empty { - params.remove("_meta"); - } - } - session_envelope(req, url, &body, Some(revision), session) -} - -/// The shared session-revision envelope: `template`'s headers without the stateless mirror, plus -/// the version and session headers `revision` and `session` call for. -fn session_envelope( - template: &OutboundRequest, - url: &str, - body: &Value, - revision: Option, - session: Option<&str>, -) -> OutboundRequest { - let mut headers: Vec<(String, String)> = template - .headers - .iter() - .filter(|(k, _)| { - k != H_PROTOCOL_VERSION - && k != H_MCP_METHOD - && k != H_MCP_NAME - && k != H_SESSION - && !k.starts_with("mcp-param-") - }) - .cloned() - .collect(); - if let Some(r) = revision.filter(|r| r.requires_version_header()) { - headers.push((H_PROTOCOL_VERSION.to_string(), r.wire().to_string())); - } - if let Some(s) = session { - headers.push((H_SESSION.to_string(), s.to_string())); - } - OutboundRequest { - url: url.to_string(), - headers, - body: serde_json::to_vec(body).unwrap_or_default(), - } -} - -/// Why a `2024-11-05` stream's message address was refused. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum AddressRefused { - /// The address names another origin than the registered endpoint's. Following it would let - /// the upstream point busbar's POSTs, credential and all, at a host the operator never - /// registered. - CrossOrigin, - /// The address is not a path or an absolute address busbar can read. - Malformed, -} - -/// The `(scheme, authority)` of an absolute address, or `None`. -fn origin_of(url: &str) -> Option<(&str, &str)> { - let (scheme, rest) = url.split_once("://")?; - let authority = rest.split(['/', '?', '#']).next()?; - if scheme.is_empty() || authority.is_empty() { - return None; - } - Some((scheme, authority)) -} - -/// THE MESSAGE ADDRESS a `2024-11-05` stream's `endpoint` event names, resolved against the -/// registered endpoint `base`. A path is joined to `base`'s origin; an absolute address must have -/// exactly `base`'s origin. Anything else is refused. -/// -/// # Errors -/// [`AddressRefused`] for another origin or an unreadable address. -pub fn message_address(base: &str, event_data: &str) -> Result { - let data = event_data.trim(); - if data.is_empty() || data.chars().any(char::is_whitespace) || data.starts_with("//") { - return Err(AddressRefused::Malformed); - } - let (scheme, authority) = origin_of(base).ok_or(AddressRefused::Malformed)?; - if data.starts_with('/') { - return Ok(format!("{scheme}://{authority}{data}")); - } - match origin_of(data) { - Some(o) if o.0.eq_ignore_ascii_case(scheme) && o.1.eq_ignore_ascii_case(authority) => { - Ok(data.to_string()) - } - Some(_) => Err(AddressRefused::CrossOrigin), - None => Err(AddressRefused::Malformed), - } -} - -/// One event read off an event stream. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct StreamEvent { - /// The `event:` name, when the event named one. - pub event: Option, - /// The `id:`, when the event carried one. - pub id: Option, - /// The `data:` lines, joined with `\n`. - pub data: String, -} - -/// AN INCREMENTAL EVENT-STREAM READER: bytes in, whole events out, the unfinished tail kept. -/// Bounded: a tail that grows past `max_pending` bytes without completing an event is dropped and -/// [`Self::overflowed`] says so, so a peer that never ends an event cannot grow busbar's memory. -#[derive(Debug)] -pub struct EventReader { - pending: Vec, - max_pending: usize, - overflowed: bool, -} - -impl EventReader { - /// A reader holding at most `max_pending` bytes of an unfinished event. - #[must_use] - pub fn new(max_pending: usize) -> Self { - Self { - pending: Vec::new(), - max_pending, - overflowed: false, - } - } - - /// Whether an unfinished event was ever dropped for exceeding the bound. - #[must_use] - pub const fn overflowed(&self) -> bool { - self.overflowed - } - - /// Feeds `chunk` and returns every event it completed, in order. Comment lines and events with - /// neither a name nor data are skipped. - pub fn feed(&mut self, chunk: &[u8]) -> Vec { - // A carriage return only ever ends a line here, so it is dropped as it arrives and every - // line end reads as `\n`. - self.pending - .extend(chunk.iter().copied().filter(|b| *b != b'\r')); - let mut out = Vec::new(); - while let Some(end) = self.pending.windows(2).position(|w| w == b"\n\n") { - let raw: Vec = self.pending.drain(..end + 2).collect(); - let raw = String::from_utf8_lossy(&raw[..end]); - let mut event = StreamEvent { - event: None, - id: None, - data: String::new(), - }; - let mut data: Vec<&str> = Vec::new(); - for line in raw.lines() { - if line.starts_with(':') { - continue; - } - let (field, value) = line.split_once(':').unwrap_or((line, "")); - let value = value.strip_prefix(' ').unwrap_or(value); - match field { - "event" => event.event = Some(value.to_string()), - "id" => event.id = Some(value.to_string()), - "data" => data.push(value), - _ => {} - } - } - event.data = data.join("\n"); - if event.event.is_some() || !event.data.is_empty() { - out.push(event); - } - } - if self.pending.len() > self.max_pending { - self.pending.clear(); - self.overflowed = true; - } - out - } -} - -/// What one event on a `2024-11-05` stream means to the client holding it. -#[derive(Clone, Debug, PartialEq)] -pub enum LinkEffect { - /// The `endpoint` event named the message address: POSTs may start. - Ready(String), - /// The `endpoint` event named an address busbar will not POST to. The link is unusable. - Refused(AddressRefused), - /// The answer to a request this link is waiting on, by its id's JSON text. - Answer { - /// The request id, as JSON text. - id: String, - /// The whole JSON-RPC response, as sent. - body: Vec, - }, - /// A request or notification from the upstream, for the peer classifier. - FromPeer(Value), - /// Nothing busbar acts on: a repeat `endpoint`, a response nobody waits for, a body that is - /// not JSON. - Ignored, -} - -/// THE CLIENT STATE OF ONE `2024-11-05` STREAM: the message address once named, and the ids of the -/// requests waiting for an answer on it. Bounded: at most `max_waiting` requests wait at once. -#[derive(Debug)] -pub struct EventStreamLink { - base: String, - address: Option, - waiting: std::collections::BTreeSet, - max_waiting: usize, -} - -impl EventStreamLink { - /// A link to the stream opened at `base`, holding at most `max_waiting` open requests. - #[must_use] - pub fn new(base: &str, max_waiting: usize) -> Self { - Self { - base: base.to_string(), - address: None, - waiting: std::collections::BTreeSet::new(), - max_waiting: max_waiting.max(1), - } - } - - /// The message address, once the stream named an acceptable one. - #[must_use] - pub fn address(&self) -> Option<&str> { - self.address.as_deref() - } - - /// Registers a request by its `id` before it is POSTed. `false` when the link already holds - /// `max_waiting` requests or the id is already waiting. - pub fn expect(&mut self, id: &Value) -> bool { - if self.waiting.len() >= self.max_waiting { - return false; - } - self.waiting.insert(id.to_string()) - } - - /// Stops waiting for `id` (its caller gave up). - pub fn abandon(&mut self, id: &Value) { - self.waiting.remove(&id.to_string()); - } - - /// Reads one event. - pub fn on_event(&mut self, event: &StreamEvent) -> LinkEffect { - if event.event.as_deref() == Some("endpoint") { - if self.address.is_some() { - return LinkEffect::Ignored; - } - return match message_address(&self.base, &event.data) { - Ok(a) => { - self.address = Some(a.clone()); - LinkEffect::Ready(a) - } - Err(r) => LinkEffect::Refused(r), - }; - } - if !matches!( - event.event.as_deref(), - None | Some(crate::adapt::EVENT_MESSAGE) - ) { - return LinkEffect::Ignored; - } - let Ok(v) = serde_json::from_str::(&event.data) else { - return LinkEffect::Ignored; - }; - if v.get("method").is_some() { - return LinkEffect::FromPeer(v); - } - let Some(id) = v.get("id").map(Value::to_string) else { - return LinkEffect::Ignored; - }; - if self.waiting.remove(&id) { - LinkEffect::Answer { - id, - body: event.data.clone().into_bytes(), - } - } else { - LinkEffect::Ignored - } - } -} - -#[cfg(test)] -#[path = "tests/compat_tests.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/client/jsonrpc.rs b/crates/busbar-plane-mcp/src/client/jsonrpc.rs deleted file mode 100644 index 87e738c31e..0000000000 --- a/crates/busbar-plane-mcp/src/client/jsonrpc.rs +++ /dev/null @@ -1,574 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE OUTBOUND WIRE for MCP revision `2026-07-28`: the JSON-RPC envelope busbar sends to an -//! upstream, and the rules that govern what busbar does with the answer. -//! -//! ## Symmetry with the ingress, deliberately -//! -//! The engine's `mcp::envelope` enforces this revision's transport MUSTs on requests busbar RECEIVES: -//! `_meta` at `params._meta`, `Mcp-Method` mirroring the body's method, `Mcp-Name` mirroring the -//! target, `MCP-Protocol-Version` mirroring `_meta`. This module produces requests that SATISFY the -//! same MUSTs, and every one of the five shared wire words — the two `_meta` keys and the three -//! header names — is `use`d from the ingress rather than restated. A second copy of -//! `"io.modelcontextprotocol/protocolVersion"` is a second thing to get wrong, and the failure mode -//! is silent: busbar would accept requests it could not itself send. That sentence was true as a -//! claim and false as code until 2026-08-12: this module carried its own copy of all five, and a -//! source-scanning test in `tests/wire_tests.rs` compared the two copies. There is now one copy, so -//! there is nothing left to compare. -//! -//! There is no `initialize` and there is no session. Every request is self-describing, so there is -//! nothing to establish and nothing to invalidate — which is why nothing in this module caches a -//! negotiated fact across two requests. -//! -//! ## The namespaced name is BUSBAR'S, not the upstream's -//! -//! `{server}_{tool}` is how busbar names the tool internally and to its own callers. The upstream -//! has never heard of it and would answer `-32602` to it. So the outbound `params.name` is the -//! UN-namespaced tool, and the namespacing is stripped at exactly one point — here — rather than at -//! each call site. This is also why the routing key is a pair rather than a string (see -//! `super::identity`): stripping a prefix off a rendering would be parsing, and parsing is where the -//! ambiguity lives. -//! -//! ## An upstream's ask is relayed to the caller -//! -//! Under this revision a server cannot send a request. Sampling, elicitation and roots come back -//! INLINE as an `InputRequiredResult` in the result of a call busbar made. busbar answers none of -//! them: a granted ask is relayed to busbar's caller with its `inputRequests` as the upstream sent -//! them, and the caller's answer rides the retry (BUSBAR-1.6.0 Law 11: busbar answers nothing on -//! the caller's behalf and routes no call to another plane on the content's say-so). What this -//! module decides is only whether the operator lets the ask through: -//! -//! 1. **Deny by default, per-server grant.** [`ServerRequestGrants`] is all-false at construction; -//! an ungranted ask is refused, not relayed. -//! 2. **Re-checked on EVERY retry.** There is no handshake to check it once at, so the grant is -//! read from the live registration each round rather than captured. -//! 3. **The relayed rounds are BOUNDED.** Removing the handshake turned one call into a sequence, -//! and a hostile upstream can return `InputRequiredResult` forever; the per-server round cap -//! (`max_input_required_rounds`) ends it. - -use crate::codec::{H_MCP_METHOD, H_MCP_NAME, H_PROTOCOL_VERSION, PROTOCOL_VERSION}; -pub use crate::codec::{META_CLIENT_CAPABILITIES, META_PROTOCOL_VERSION}; -use crate::identity::ToolKey; - -/// The `_meta` key a progress token travels under, both directions. Serialised as `null` when -/// absent, which `serde_json` omits from the object rather than sending — so a caller that asked for -/// no progress produces an outbound `_meta` byte-identical to the one busbar sent before this. -const META_PROGRESS_TOKEN: &str = "progressToken"; - -// The `_meta` keys and the header names are NOT declared here. They are `use`d from -// `crate::codec`, which owns the single spelling of each — see the wire-words block there. -// A re-export keeps this module's existing callers (`super::*` users, the test suite) working -// against one definition rather than two, and the re-export is deliberately not a second `const`: -// there is nothing here for a divergence to live in. - -/// A request busbar is about to send: everything, serialized, in one value. -/// -/// One struct rather than a builder chain because it is what the adversarial no-passthrough test -/// scans. A test that has to reconstruct the request from a builder's intermediate state is a test -/// that can miss the field the builder added last. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct OutboundRequest { - /// The upstream endpoint the request is sent to. - pub url: String, - /// Header name/value pairs, lower-cased names, in the order they will be written. - pub headers: Vec<(String, String)>, - /// The serialized JSON-RPC body. - pub body: Vec, -} - -impl OutboundRequest { - /// EVERY byte this request will put on the wire, concatenated: URL, then each header name and - /// value, then the body. - /// - /// This exists for the adversarial scan and for nothing else. Its value is that it is derived - /// from the same fields the transport writes, so a field added to `OutboundRequest` without - /// being added here fails `wire_tests::every_field_is_scanned` — a scan that silently stops - /// covering a new field is the exact false green this rule exists to prevent. - /// Used by the tests that scan the whole wire for a leaked caller credential; the dispatch - /// path hands the body to the transport directly. - #[allow(dead_code)] - pub fn wire_bytes(&self) -> Vec { - let mut out = Vec::new(); - out.extend_from_slice(self.url.as_bytes()); - for (name, value) in &self.headers { - out.extend_from_slice(name.as_bytes()); - out.extend_from_slice(value.as_bytes()); - } - out.extend_from_slice(&self.body); - out - } -} - -/// WHICH client capabilities the outbound `_meta` declares — one flag per capability busbar can -/// genuinely answer for THIS server. A struct rather than adjacent booleans, because two `bool`s -/// in one signature are two arguments a call site can swap without the compiler noticing, and the -/// swap would advertise an ask busbar then refuses. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct AdvertisedCaps { - /// Declare `roots`: the operator lets this server put the ask to callers AND this caller - /// declared it can answer one (the ask is relayed to it, never answered by busbar). - pub roots: bool, - /// Declare `sampling`: as `roots`. - pub sampling: bool, - /// Declare `elicitation`: as `roots`. - pub elicitation: bool, - /// The CALLER asked for progress, so busbar's own token rides `_meta`. Read by the caller from - /// its per-request progress slot and handed in, so this builder reads no task state. - pub progress: bool, -} - -/// Build a `tools/call` for `key` against `url`. -/// -/// `authorization` is the ALREADY-PLANNED credential header value (see `super::egress`). It arrives -/// as a plain `String` rather than as a caller context, which is rule 1 in the type system: this -/// function has no access to the caller's busbar key and therefore no way to send it. -/// `continuation` is the CALLER's answer to an upstream's `InputRequiredResult` that busbar relayed -/// to it: an object carrying the caller's `inputResponses` and (when the upstream sent one) the -/// upstream's own `requestState`, echoed here onto the RETRY of the same logical call — which -/// is MRTR's own continuation shape, the one busbar's ingress reads at -/// the engine's `mcp::method` `inputResponses` sites. `None` is every first round and every call whose -/// upstream asked nothing, and produces a byte-identical request to what this builder always sent. -/// `advertise` declares the client capabilities in `_meta`. Each flag is TRUE exactly when the -/// registration holds the matching grant (the operator lets this server put that ask to callers) AND -/// the caller declared the capability — the two facts that make the ask answerable, by the caller -/// it is relayed to (Law 11: busbar answers none itself). MRTR forbids a server sending an ask the -/// client has not declared, and declaring one that would then be refused invites an upstream to -/// build a call sequence around a refusal. -pub fn tools_call( - url: &str, - key: &ToolKey, - arguments: &serde_json::Value, - request_id: u64, - authorization: Option<&str>, - advertise: AdvertisedCaps, - continuation: Option<&serde_json::Value>, -) -> OutboundRequest { - // The UN-namespaced name: see the module header. - let name = key.tool(); - // BUSBAR'S OWN TOKEN, not the caller's. Sent only when the caller asked for progress at all — - // a server MUST NOT emit progress without a token, so sending none is how busbar says "this - // caller did not ask". Derived from `request_id`, which is already unique per outbound call, so - // two concurrent calls cannot be told apart by the upstream as one conversation. - let progress_token: Option = advertise.progress.then(|| format!("busbar-{request_id}")); - // busbar declares NO client capabilities BY DEFAULT. Every ask kind is deny-by-default per - // server, and declaring a capability we then refuse to honour invites an upstream to build a - // call sequence around it. Declaring the empty set is the honest statement of a client that - // will not be answering — and each capability joins it exactly when the operator made the - // statement true (see `advertise` above). - let mut capabilities = serde_json::Map::new(); - if advertise.roots { - capabilities.insert("roots".to_string(), serde_json::json!({})); - } - if advertise.sampling { - capabilities.insert("sampling".to_string(), serde_json::json!({})); - } - if advertise.elicitation { - capabilities.insert("elicitation".to_string(), serde_json::json!({})); - } - let capabilities = serde_json::Value::Object(capabilities); - let mut body = serde_json::json!({ - "jsonrpc": "2.0", - "id": request_id, - "method": "tools/call", - "params": { - "name": name, - "arguments": arguments, - "_meta": { - META_PROTOCOL_VERSION: PROTOCOL_VERSION, - META_PROGRESS_TOKEN: progress_token, - META_CLIENT_CAPABILITIES: capabilities, - }, - }, - }); - if let Some(continuation) = continuation { - // EXACTLY TWO members cross, by name. The continuation value is read off the caller's - // retry, and copying it wholesale into `params` would make that retry a second author of - // this envelope — and the day it carries a third member, that member ships - // without anyone deciding it should. - if let (Some(params), Some(responses)) = ( - body["params"].as_object_mut(), - continuation.get("inputResponses"), - ) { - params.insert("inputResponses".to_string(), responses.clone()); - if let Some(state) = continuation.get("requestState") { - params.insert("requestState".to_string(), state.clone()); - } - } - } - envelope(url, "tools/call", Some(name), body, authorization) -} - -/// Build a `tools/list` for one server. -/// The CONNECT-path request. Nothing fetches a live tool list yet — that is the gap named in -/// `client::catalogue`. -#[allow(dead_code)] -pub fn tools_list(url: &str, request_id: u64, authorization: Option<&str>) -> OutboundRequest { - let body = serde_json::json!({ - "jsonrpc": "2.0", - "id": request_id, - "method": "tools/list", - "params": { - "_meta": { - META_PROTOCOL_VERSION: PROTOCOL_VERSION, - META_CLIENT_CAPABILITIES: {}, - }, - }, - }); - envelope(url, "tools/list", None, body, authorization) -} - -/// The shared envelope: the mirrored headers this revision REQUIRES, written from the same values -/// that went into the body so they cannot disagree. -/// -/// Built from the body's own values rather than from the caller's arguments a second time. A -/// mirrored header whose value is computed twice is a mirrored header that can differ, which is the -/// request-smuggling primitive the mirroring exists to close — and busbar's own ingress answers -/// `-32020` to it, so a bug here would be caught only by an upstream strict enough to check. -/// -/// Public so `super::verb` — the CLOSED SET of methods busbar issues — builds through this -/// one function rather than assembling a second header block. Two envelope builders is two places -/// the mirrored headers can be got wrong, and only one of them would be scanned by -/// `no_key_passthrough_tests`. -pub fn envelope( - url: &str, - method: &str, - name: Option<&str>, - body: serde_json::Value, - authorization: Option<&str>, -) -> OutboundRequest { - let mut headers = vec![ - ("content-type".to_string(), "application/json".to_string()), - // An event stream is a permitted content type for the RESPONSE to a POST in this revision, - // and nothing more — there is no GET stream. Accepting both is what lets an upstream stream - // a long tool result without busbar having to speak a second transport. - ( - "accept".to_string(), - "application/json, text/event-stream".to_string(), - ), - (H_PROTOCOL_VERSION.to_string(), PROTOCOL_VERSION.to_string()), - (H_MCP_METHOD.to_string(), method.to_string()), - ]; - if let Some(name) = name { - headers.push((H_MCP_NAME.to_string(), encode_sentinel(name))); - } - if let Some(auth) = authorization { - headers.push(("authorization".to_string(), format!("Bearer {auth}"))); - } - OutboundRequest { - url: url.to_string(), - headers, - body: serde_json::to_vec(&body).unwrap_or_default(), - } -} - -/// Encode a name for `Mcp-Name` using this revision's `=?base64?…?=` sentinel WHEN IT MUST BE. -/// -/// A header value may carry only visible ASCII plus space. A tool name is validated to -/// `[A-Za-z0-9_.-]` at registration (`super::identity`), so in practice the sentinel is never -/// needed — but "in practice never" is exactly the branch that rots, so the encoder is written and -/// exercised rather than assumed away. The decision is on the VALUE, never on a flag, so an encoded -/// and a plain name cannot both describe the same request. -pub fn encode_sentinel(name: &str) -> String { - use base64::Engine as _; - if name - .bytes() - .all(|b| b.is_ascii_graphic() && b != b'=' && b != b'?') - { - return name.to_string(); - } - format!( - "=?base64?{}?=", - base64::engine::general_purpose::STANDARD.encode(name) - ) -} - -/// The parsed shape of a JSON-RPC response, reduced to the outcomes dispatch cares about. -#[derive(Clone, Debug, PartialEq)] -pub enum RpcOutcome { - /// A normal result. - Result(serde_json::Value), - /// A JSON-RPC error object. - Error { - /// The error's `code`. Always an integer: an error object without one is not a JSON-RPC - /// error and reads as [`RpcOutcome::Malformed`], never as a code nobody sent. - code: i64, - /// The error's `message`. - message: String, - }, - /// The upstream is asking busbar to spend busbar's OWN authority: an `InputRequiredResult`. - /// Carries the kind of ask so the grant check names the right grant. - InputRequired { - /// The most privileged ask the result names. - kind: ServerAsk, - }, - /// The body was not a JSON-RPC response at all. - Malformed(String), - /// THE BODY WAS A JSON-RPC RESPONSE TO SOMETHING ELSE — a mismatched `id`, a `null` id, or no - /// `id` member at all. - /// - /// Its own arm rather than folded into [`RpcOutcome::Malformed`], because the two are different - /// facts and an operator has to act on them differently: `Malformed` says the upstream is - /// broken, `Uncorrelated` says the upstream answered something busbar did not ask, which is - /// either a broken multiplexer or a deliberate attempt to have busbar serve one caller another - /// caller's answer. Making it a distinct arm also means every `match` on this enum had to be - /// revisited when it was added, which is exactly the review the change wanted. - Uncorrelated(String), -} - -/// The three things an upstream may ask for, kept as a closed enum so a fourth cannot be handled by -/// a default arm that says yes. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ServerAsk { - /// `sampling/createMessage`: an LLM completion, which the caller runs if it answers. - Sampling, - /// Ask a human for input. - Elicitation, - /// Disclose filesystem roots. - Roots, -} - -impl ServerAsk { - /// The ask's grant key, as the registry and the refusal text spell it. - pub fn key(self) -> &'static str { - match self { - ServerAsk::Sampling => "sampling", - ServerAsk::Elicitation => "elicitation", - ServerAsk::Roots => "roots", - } - } - - /// The ORDER OF DANGER, used to collapse a multi-method `inputRequests` map to the one kind the - /// grant gate will test. Ranked by what answering the ask would cost the caller it is - /// relayed to: - /// - /// - `sampling` is an LLM completion — money, and unbounded; - /// - `elicitation` interrupts a human; - /// - `roots` discloses filesystem structure. - /// - /// Written as a rank rather than a `match` on pairs so a fourth kind cannot be added without - /// giving it a position — which is the decision that would otherwise be made by whichever arm - /// somebody happened to write first. - fn rank(self) -> u8 { - match self { - ServerAsk::Roots => 0, - ServerAsk::Elicitation => 1, - ServerAsk::Sampling => 2, - } - } - - fn most_privileged(self, other: Self) -> Self { - if other.rank() > self.rank() { - other - } else { - self - } - } -} - -// ══ THE OUTBOUND ID SPACE ════════════════════════════════════════════════════════════════════════ -// -// ONE JSON-RPC id space, split into two ranges that CANNOT MEET — and the split is a property of a -// TYPE rather than of a number anyone has to remember. -// -// [`parse_response`] below is the whole correlation defence on this plane. There is no table of -// pending ids (this revision deleted sessions — see the module header), so "is this the answer to -// what I asked?" is decided by comparing one id against one id. That defence is worth exactly as -// much as the ids being distinct, and on the child-process carrier the handshake and the dispatch share ONE -// byte stream: they are the two messages that must never be confused. -// -// THEY WERE CONFUSABLE. The handshake's id was the literal `0`, and a dispatch's id is the round -// number — which starts at `0` on every call, at both minting sites (`method::tools_call`'s leg -// seam and the task runner's, each widening a `round: u32`). A child that left a spare `initialize` -// response in its pipe (one that answers twice, or answers a handshake whose reader went away) had -// that response read by the next `tools/call`, and `parse_response` CORRELATED it: same id, so the -// handshake's result was served to a caller as their tool's result. The `Uncorrelated` arm that -// exists for exactly this could not fire, because there was nothing for it to tell apart. -// -// So the handshake's id is the first value ABOVE every id [`dispatch_request_id`] can return. Every -// dispatch id is a `u32` widened to `u64`, so the dispatch range is `0..=u32::MAX` BY TYPE; the -// handshake sits at `u32::MAX + 1`. A dispatch cannot reach it without first widening a round -// counter — and the `const` assertion below fails the BUILD, not a test, on the day one does. - -/// THE ID BUSBAR PUTS ON AN OUTBOUND DISPATCH: the round number, widened, and nothing else. -/// -/// A function rather than a `u64::from(round)` written out at each minting site, because the -/// disjointness asserted below is a claim about the RANGE of the dispatch id — and that claim is -/// only true while this is the one way to mint one. The `u32` argument is the bound: it is what -/// makes `0..=u32::MAX` a fact about the type rather than a convention about the counter. -pub const fn dispatch_request_id(round: u32) -> u64 { - round as u64 -} - -/// THE JSON-RPC ID BUSBAR PUTS ON ITS CHILD-PROCESS HANDSHAKE. -/// -/// DISJOINT BY CONSTRUCTION from every dispatch id — see the block above for what the collision -/// did. It does not need to be unique across CHILDREN: correlation is per exchange, and there is no -/// table of pending ids for two children to collide in. -pub const HANDSHAKE_REQUEST_ID: u64 = u32::MAX as u64 + 1; - -/// THE DISJOINTNESS, CHECKED BY THE COMPILER rather than asserted by a comment or a test. -/// -/// [`dispatch_request_id`] is monotonic in its argument, so its maximum is its value at `u32::MAX`; -/// the handshake must sit strictly above that. Widening a round counter to `u64` — the one edit -/// that would silently re-open the collision — stops being possible here rather than being -/// discovered by an operator whose caller was served a handshake. -const _: () = assert!(HANDSHAKE_REQUEST_ID > dispatch_request_id(u32::MAX)); - -/// Parse an upstream response body AS THE ANSWER TO THE REQUEST `sent_id` NAMES. -/// -/// ## `sent_id` is not decoration, and it is not a session either -/// -/// Every builder in this module writes `"id": request_id` into the body it produces. Until this -/// argument existed nothing ever read that member back: a response carrying a DIFFERENT id, a -/// `null` id, or no `id` member at all was accepted as the answer to whatever busbar had just sent, -/// because the only thing that associated the two was that they were the same function call. That -/// holds right up until an upstream (or anything between busbar and it) puts a different answer on -/// that socket, and then busbar serves one caller another caller's result while every test that -/// pairs one request with one response stays green. -/// -/// The correlation is therefore checked, and it is checked with a value that lives no longer than -/// this dispatch. `sent_id` comes from the same `request_id` the caller handed the builder, a few -/// lines up in the same function; nothing is stored, so there is nothing to invalidate and no -/// counter that "outlived a dispatch" — which is the constraint this module's header sets, and it -/// is a real one: this revision deleted sessions, and re-inventing one to hold pending ids would be -/// re-adding the thing the revision removed. -/// -/// The envelope rules themselves — the `jsonrpc` member, the `id` cases, exactly-one-of -/// `result`/`error` — are [`busbar_contract::jsonrpc::read_response`]'s, shared with the A2A relay, -/// for the reason the request side already established: one concern, one reader. -pub fn parse_response(body: &[u8], sent_id: u64) -> RpcOutcome { - use busbar_contract::jsonrpc::{read_response, NotAnAnswerKind, Reply}; - - let Ok(value) = serde_json::from_slice::(body) else { - return RpcOutcome::Malformed("upstream response is not valid JSON".to_string()); - }; - let reply = match read_response(&value, &serde_json::Value::from(sent_id)) { - Ok(reply) => reply, - // The shared reader names WHICH kind of non-answer it is; this plane maps the two onto its - // own two arms rather than re-deriving the distinction from the prose. - Err(e) => { - return match e.kind { - NotAnAnswerKind::Uncorrelated => RpcOutcome::Uncorrelated(e.reason), - NotAnAnswerKind::NotAResponse => RpcOutcome::Malformed(e.reason), - } - } - }; - match reply { - Reply::Error { code, message } => match code.as_ref().and_then(serde_json::Value::as_i64) { - Some(code) => RpcOutcome::Error { code, message }, - // JSON-RPC 2.0 section 5.1: `code` MUST be an integer. A missing or non-integer code is - // a broken upstream, and inventing `0` for it would hand the caller a code the upstream - // never sent. - None => RpcOutcome::Malformed(format!( - "upstream error object has no integer `code` (message: {message})" - )), - }, - Reply::Result(result) => match input_required_kind(&result) { - Some(ask) => RpcOutcome::InputRequired { kind: ask }, - None => RpcOutcome::Result(result), - }, - } -} - -/// Recognise an `InputRequiredResult` and which authority it is asking for. -/// -/// ## The shape this reads is the SPECIFICATION'S, and the correction matters more than it looks -/// -/// This function used to test `result.type == "input_required"` and read `result.request` as a -/// string. Neither key exists in MRTR. `mrtr.mdx:126-180` and the schema's `InputRequiredResult` -/// agree: the discriminator is **`resultType`**, and the asks are a **map** at `inputRequests` -/// whose values are request objects each naming a `method`. -/// -/// So a CONFORMANT upstream's ask failed this predicate, was returned as an ordinary -/// [`RpcOutcome::Result`], became `Outcome::Completed`, and was written onto the wire to busbar's -/// own caller verbatim — `resultType`, `inputRequests`, `requestState` and all. The type-level -/// guarantee in the engine's `mcp::inputreq` (an `Outcome` with no arm that can carry an `Ask`) was real -/// and was never reached, because whether control reaches it is decided HERE. A rule enforced by a -/// missing enum variant still needs the predicate that routes to it to be correct. -/// -/// The reason the tests did not catch it is worth recording next to the fix: the only fixtures that -/// exercised this minted busbar's own invented shape, so the parser and the fixture agreed with each -/// other and with no server in existence. -/// -/// ## Why an ask with no `inputRequests` is still an ask -/// -/// `mrtr.mdx:245` makes `inputRequests` and `requestState` individually OPTIONAL — a server must -/// include at least ONE of them. Keying recognition off `inputRequests` alone would therefore let an -/// upstream slip an ask past this gate by omitting the field the spec says it may omit. -/// -/// ## Why the MOST PRIVILEGED method in the map decides -/// -/// One `inputRequests` map may name several methods at once. Judging against the first key iterated -/// would let an upstream smuggle a `sampling/createMessage` — a real LLM call for the caller to -/// run — behind a `roots/list` it knows the operator granted. The gate downstream tests ONE kind, -/// so the kind reported here must be the one with the most to lose. -fn input_required_kind(result: &serde_json::Value) -> Option { - let obj = result.as_object()?; - let by_spec = obj.get("resultType").and_then(|v| v.as_str()) == Some("input_required"); - // The shape busbar itself once invented. No conformant server emits it, so this can only ever - // catch a NON-conformant upstream — and refusing that upstream's ask is still the right answer, - // which is the whole reason to keep a line that should never fire. - let legacy = obj.get("type").and_then(|v| v.as_str()) == Some("input_required"); - if !by_spec && !legacy { - return None; - } - - let mut kind: Option = None; - if let Some(requests) = obj.get("inputRequests").and_then(|v| v.as_object()) { - for request in requests.values() { - let named = match request.get("method").and_then(|v| v.as_str()) { - Some("sampling/createMessage") => ServerAsk::Sampling, - Some("elicitation/create") => ServerAsk::Elicitation, - Some("roots/list") => ServerAsk::Roots, - // An ask naming something we do not recognise is STILL AN ASK, and an unrecognised - // ask is refused rather than passed through — which is what dropping it would do. - // Mapping it to the most privileged kind makes the refusal land on a grant an - // operator has to have set deliberately. - _ => ServerAsk::Sampling, - }; - kind = Some(match kind { - None => named, - Some(worst) => worst.most_privileged(named), - }); - } - } - // An ask that names no method at all — `requestState` only, or the legacy shape — is judged - // against the most privileged grant for the same reason. - Some(kind.unwrap_or(ServerAsk::Sampling)) -} - -/// The per-server grants, unchanged by the stateless revision. What moved is WHERE they are -/// consulted: from refusing a server's inbound request to refusing to relay its inline ask. -/// -/// All false at construction, and there is no `all()` constructor. Deny-by-default is a property of -/// the type, not of a config default somebody can invert. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct ServerRequestGrants { - /// The server may ask for `sampling/createMessage`. - pub sampling: bool, - /// The server may ask for `elicitation/create`. - pub elicitation: bool, - /// The server may ask for `roots/list`. - pub roots: bool, -} - -impl ServerRequestGrants { - /// Whether the operator lets this server put `ask` to callers: read by the child-message - /// classifier (`super::peer::decide_ask`) and the child exchange's relay. - pub fn allows(&self, ask: ServerAsk) -> bool { - match ask { - ServerAsk::Sampling => self.sampling, - ServerAsk::Elicitation => self.elicitation, - ServerAsk::Roots => self.roots, - } - } -} - -#[cfg(test)] -#[path = "tests/wire_tests.rs"] -mod wire_tests; - -#[cfg(test)] -#[path = "tests/ask_tests.rs"] -mod ask_tests; diff --git a/crates/busbar-plane-mcp/src/client/mod.rs b/crates/busbar-plane-mcp/src/client/mod.rs deleted file mode 100644 index 2d83de3878..0000000000 --- a/crates/busbar-plane-mcp/src/client/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE CLIENT DIRECTION'S WIRE DIALECT: what busbar sends an upstream MCP server and how it reads -//! the answer. Pure builders and readers over values; nothing here opens a connection, holds a -//! session or reads a clock. The engine that drives them hands every fact in as an argument. - -/// The session revisions, spoken as a client: lowering, the ladder's reading, the message address. -pub mod compat; -pub mod jsonrpc; -/// One upstream call carried across the revisions, as a sans-I/O state machine. -pub mod negotiate; -pub mod peer; -pub mod verb; diff --git a/crates/busbar-plane-mcp/src/client/negotiate.rs b/crates/busbar-plane-mcp/src/client/negotiate.rs deleted file mode 100644 index 745a443448..0000000000 --- a/crates/busbar-plane-mcp/src/client/negotiate.rs +++ /dev/null @@ -1,420 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! ONE UPSTREAM CALL, NEGOTIATED: the client ladder as a sans-I/O state machine (OWNER 2026-09-29 -//! compat scope, both directions). -//! -//! busbar builds every request in the stateless `2026-07-28` shape. [`Negotiator`] carries ONE such -//! request to an upstream whose revision busbar may not know, and says, one [`Action`] at a time, -//! what the driver must put on the wire: the stateless request itself; on a refusal an `initialize` -//! and `notifications/initialized` and then the request lowered into the session revision the -//! upstream chose; and on a refusal of `initialize`, the `2024-11-05` event stream, with the -//! request POSTed to the address the stream names and its answer read off the stream. -//! -//! THE ADAPTER CONTRACT, which is all a driver implements (the engine's legacy client leg today, -//! the plane driver's far end at the flip): -//! -//! - [`Action::Send`]: send the request with its verb; feed the answer to [`Negotiator::on_answer`] -//! as a [`HopAnswer`] (status, the response head's kept fields, body), or `None` when there was no -//! answer at all. The only head field read is [`super::compat::H_SESSION`], which the plane -//! declares in `keep_response_headers`. -//! - [`Action::OpenStream`]: open a GET stream; feed its head to [`Negotiator::on_answer`] (body -//! empty), then every event read off it to [`Negotiator::on_event`] until the machine finishes. -//! Close the stream when it does. -//! - [`Action::AwaitEvent`]: read the next event off the open stream. -//! - [`Action::Finish`]: the answer to hand back, in the shape the stateless request would have -//! had it; [`Negotiator::remembered`] is what to keep for the next call to this upstream. -//! - [`Action::Fail`]: stop; nothing more is sent. -//! -//! Nothing here opens a connection, reads a clock or holds anything across calls but the value. - -use serde_json::Value; - -use super::compat::{ - initialize_request, initialized_notification, lower_request, offered_revision, probe_outcome, - EventStreamLink, LinkEffect, StreamEvent, H_SESSION, -}; -use super::jsonrpc::OutboundRequest; -use crate::revision::{self, Revision, StepOutcome}; -use crate::tool_sessions::Remembered; - -/// The verb a hop is sent with. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Verb { - /// A JSON-RPC message. - Post, - /// The end of a session. - Delete, -} - -/// What came back from one hop. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HopAnswer { - /// The response status. - pub status: u16, - /// The response head's kept fields, lower-case names. - pub fields: Vec<(String, String)>, - /// The body (the last JSON-RPC message of an event-stream answer, as the engine reads it). - pub body: Vec, -} - -impl HopAnswer { - fn field(&self, name: &str) -> Option<&str> { - self.fields - .iter() - .find(|(k, _)| k.eq_ignore_ascii_case(name)) - .map(|(_, v)| v.as_str()) - } - - const fn ok(&self) -> bool { - self.status >= 200 && self.status < 300 - } -} - -/// Why a negotiated call stopped without an answer to hand back. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Refusal { - /// The upstream could not be reached, or answered 5xx, on a step whose failure says nothing - /// about its revision. The last answer, if any, is carried. - Unreachable(Option), - /// Every rung of the ladder was refused: the upstream speaks no revision this plane carries. - NoCommonRevision, - /// The upstream's `initialize` answer named a revision this plane cannot carry; the spec's rule - /// is to disconnect. - OfferedUnsupported, - /// The `2024-11-05` stream named a message address on another origin, or none busbar can read. - AddressRefused, - /// The stream ended, or refused a POST, before the answer arrived. - StreamLost, -} - -/// The next thing the driver does. -#[derive(Clone, Debug, PartialEq)] -pub enum Action { - /// Send `request` with `verb`, then call [`Negotiator::on_answer`]. - Send { - /// The verb. - verb: Verb, - /// The request. - request: OutboundRequest, - }, - /// Open a GET event stream with `request`'s address and credential, then call - /// [`Negotiator::on_answer`] with its head. - OpenStream { - /// The stream request (its body is empty). - request: OutboundRequest, - }, - /// Read the next event off the open stream and call [`Negotiator::on_event`]. - AwaitEvent, - /// The answer to hand back. - Finish(HopAnswer), - /// Stop. - Fail(Refusal), -} - -#[derive(Clone, Debug, PartialEq, Eq)] -enum Phase { - Probe, - Initialize, - Initialized { - revision: Revision, - session: Option, - }, - InSession { - revision: Revision, - session: Option, - }, - StreamOpening, - StreamWaitAddress, - StreamInitPosted, - StreamInitialized, - StreamCallPosted, - Done, -} - -/// How many requests one `2024-11-05` link waits on at once: `initialize` and the call itself. -const LINK_WAITING: usize = 2; -/// The id busbar's own `initialize` carries. Its own request ids come from -/// [`super::jsonrpc::dispatch_request_id`], which never reaches this value. -const INITIALIZE_ID: u64 = u64::MAX; - -/// ONE CALL'S NEGOTIATION. See the module header for the driver's side. -#[derive(Debug)] -pub struct Negotiator { - original: OutboundRequest, - original_id: Option, - client_version: String, - phase: Phase, - remembered: Option, - reinitialized: bool, - link: Option, -} - -impl Negotiator { - /// Carries `original` (a stateless request) to its upstream, starting from what busbar - /// `remembered` about that upstream. `client_version` names busbar in `initialize`. - #[must_use] - pub fn new( - original: OutboundRequest, - remembered: Option, - client_version: &str, - ) -> Self { - let original_id = serde_json::from_slice::(&original.body) - .ok() - .and_then(|v| v.get("id").cloned()) - .filter(|v| !v.is_null()); - Self { - original, - original_id, - client_version: client_version.to_string(), - phase: Phase::Probe, - remembered, - reinitialized: false, - link: None, - } - } - - /// What to keep for the next call to this upstream: the negotiated revision and, on a - /// session revision, its session. `None` means forget (nothing was learnt, or it was lost). - #[must_use] - pub fn remembered(&self) -> Option { - self.remembered.clone() - } - - /// The first action. - pub fn start(&mut self) -> Action { - match self.remembered.clone() { - None => self.send(Phase::Probe, self.original.clone()), - Some(r) if r.revision == Revision::R2026_07_28 => { - self.send(Phase::Probe, self.original.clone()) - } - Some(r) if r.revision.is_event_stream_revision() => self.open_stream(), - Some(r) => self.in_session(r.revision, r.session), - } - } - - fn send(&mut self, phase: Phase, request: OutboundRequest) -> Action { - self.phase = phase; - Action::Send { - verb: Verb::Post, - request, - } - } - - fn in_session(&mut self, revision: Revision, session: Option) -> Action { - let url = self.original.url.clone(); - let request = lower_request(&self.original, &url, revision, session.as_deref()); - self.send(Phase::InSession { revision, session }, request) - } - - fn initialize(&mut self) -> Action { - let request = initialize_request( - &self.original, - &self.original.url, - revision::SESSION_REVISIONS[0], - INITIALIZE_ID, - &self.client_version, - ); - self.send(Phase::Initialize, request) - } - - fn open_stream(&mut self) -> Action { - self.phase = Phase::StreamOpening; - let mut request = lower_request( - &self.original, - &self.original.url, - Revision::R2024_11_05, - None, - ); - request.body = Vec::new(); - request - .headers - .retain(|(k, _)| k != "content-type" && k != "accept"); - request - .headers - .push(("accept".to_string(), "text/event-stream".to_string())); - self.link = Some(EventStreamLink::new(&self.original.url, LINK_WAITING)); - Action::OpenStream { request } - } - - fn finish(&mut self, answer: HopAnswer) -> Action { - self.phase = Phase::Done; - Action::Finish(answer) - } - - fn fail(&mut self, refusal: Refusal) -> Action { - self.phase = Phase::Done; - Action::Fail(refusal) - } - - /// Feeds the answer to the last [`Action::Send`] or [`Action::OpenStream`]; `None` when there - /// was no answer at all. - pub fn on_answer(&mut self, answer: Option) -> Action { - let Some(answer) = answer else { - return self.fail(Refusal::Unreachable(None)); - }; - match self.phase.clone() { - Phase::Probe => match probe_outcome(answer.status, &answer.body) { - StepOutcome::Answered => { - self.remembered = Some(Remembered { - revision: Revision::R2026_07_28, - session: None, - message_address: None, - }); - self.finish(answer) - } - StepOutcome::Unreachable => self.fail(Refusal::Unreachable(Some(answer))), - StepOutcome::Refused4xx => { - // A remembered stateless upstream that now refuses is renegotiated once. - self.remembered = None; - self.initialize() - } - }, - Phase::Initialize => { - if answer.ok() { - if let Some(r) = offered_revision(&answer.body) { - if r.is_event_stream_revision() { - return self.fail(Refusal::OfferedUnsupported); - } - let session = answer.field(H_SESSION).map(str::to_string); - let url = self.original.url.clone(); - let request = - initialized_notification(&self.original, &url, r, session.as_deref()); - return self.send( - Phase::Initialized { - revision: r, - session, - }, - request, - ); - } - } - match probe_outcome(answer.status, &answer.body) { - StepOutcome::Unreachable => self.fail(Refusal::Unreachable(Some(answer))), - StepOutcome::Refused4xx => self.open_stream(), - StepOutcome::Answered => self.fail(Refusal::OfferedUnsupported), - } - } - Phase::Initialized { revision, session } => { - if !answer.ok() { - return self.fail(Refusal::Unreachable(Some(answer))); - } - self.remembered = Some(Remembered { - revision, - session: session.clone(), - message_address: None, - }); - self.in_session(revision, session) - } - Phase::InSession { session, .. } => { - if answer.status == 404 && session.is_some() && !self.reinitialized { - // The upstream's session is gone (ended, expired, or another replica): the - // spec's recovery is a fresh `initialize`, once. - self.reinitialized = true; - self.remembered = None; - return self.initialize(); - } - self.finish(answer) - } - Phase::StreamOpening => { - if answer.ok() { - self.phase = Phase::StreamWaitAddress; - Action::AwaitEvent - } else if (400..500).contains(&answer.status) { - self.remembered = None; - self.fail(Refusal::NoCommonRevision) - } else { - self.fail(Refusal::Unreachable(Some(answer))) - } - } - Phase::StreamInitPosted | Phase::StreamCallPosted => { - if answer.ok() { - Action::AwaitEvent - } else { - self.fail(Refusal::StreamLost) - } - } - Phase::StreamInitialized => { - if !answer.ok() { - return self.fail(Refusal::StreamLost); - } - self.post_call_on_stream() - } - Phase::StreamWaitAddress | Phase::Done => Action::AwaitEvent, - } - } - - fn post_call_on_stream(&mut self) -> Action { - let Some(address) = self - .link - .as_ref() - .and_then(|l| l.address().map(str::to_string)) - else { - return self.fail(Refusal::StreamLost); - }; - let request = lower_request(&self.original, &address, Revision::R2024_11_05, None); - match self.original_id.clone() { - Some(id) => { - if let Some(link) = self.link.as_mut() { - link.expect(&id); - } - self.send(Phase::StreamCallPosted, request) - } - // A notification has no answer on the stream: its POST's status is the whole of it. - None => self.send(Phase::StreamCallPosted, request), - } - } - - /// Feeds one event read off the open stream. - pub fn on_event(&mut self, event: &StreamEvent) -> Action { - let Some(link) = self.link.as_mut() else { - return self.fail(Refusal::StreamLost); - }; - match (self.phase.clone(), link.on_event(event)) { - (Phase::StreamWaitAddress, LinkEffect::Ready(address)) => { - link.expect(&Value::from(INITIALIZE_ID)); - let request = initialize_request( - &self.original, - &address, - Revision::R2024_11_05, - INITIALIZE_ID, - &self.client_version, - ); - self.send(Phase::StreamInitPosted, request) - } - (_, LinkEffect::Refused(_)) => self.fail(Refusal::AddressRefused), - (Phase::StreamInitPosted, LinkEffect::Answer { body, .. }) => { - if offered_revision(&body) != Some(Revision::R2024_11_05) { - return self.fail(Refusal::OfferedUnsupported); - } - let address = link.address().unwrap_or_default().to_string(); - self.remembered = Some(Remembered { - revision: Revision::R2024_11_05, - session: None, - message_address: None, - }); - let request = - initialized_notification(&self.original, &address, Revision::R2024_11_05, None); - self.send(Phase::StreamInitialized, request) - } - (Phase::StreamCallPosted, LinkEffect::Answer { body, .. }) => self.finish(HopAnswer { - status: 200, - fields: Vec::new(), - body, - }), - _ => Action::AwaitEvent, - } - } - - /// Whether a notification's call is complete: a notification POSTed on the stream is finished - /// by its POST's status, which [`Self::on_answer`] reports as [`Action::AwaitEvent`]; the driver - /// asks this instead of waiting for an answer that never comes. - #[must_use] - pub fn expects_no_answer(&self) -> bool { - self.phase == Phase::StreamCallPosted && self.original_id.is_none() - } -} - -#[cfg(test)] -#[path = "tests/negotiate_tests.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/client/peer.rs b/crates/busbar-plane-mcp/src/client/peer.rs deleted file mode 100644 index 12c497df3e..0000000000 --- a/crates/busbar-plane-mcp/src/client/peer.rs +++ /dev/null @@ -1,386 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! WHAT A CHILD SENDS BUSBAR — the `busbar-as-client / server-originated` half of the matrix. -//! -//! ## The defect this module exists to fix, stated first because it is the whole reason -//! -//! `StdioChild::call` used to write one line and read ONE line, and treat that line as the answer. -//! On streamable HTTP that is right: a POST has exactly one response and a notification arrives on -//! its own frame. On stdio it is WRONG, and wrong in the worst available way. A child's stdout is -//! ONE byte stream carrying everything the child ever says: its answers, its log records, its -//! progress, its list-changed notifications, and — under every revision an installed SDK server -//! actually speaks — its own REQUESTS. So a child that emitted a single `notifications/message` -//! before answering would have that log line adopted as the answer to `tools/call`, and every -//! subsequent call on that child would be answered by the PREVIOUS call's response. One -//! well-behaved, conformant, entirely ordinary child desynchronises the stream permanently and -//! silently. -//! -//! The fix is not "skip lines that are not responses". It is to CLASSIFY every line and give each -//! class an answer, because two of the classes are ones a peer is waiting on: -//! -//! | class | what busbar does | -//! |---|---| -//! | the correlated response | returned to the caller; the exchange ends | -//! | a notification busbar knows | the effect in [`NotificationEffect`], then keep reading | -//! | a notification busbar does not know | counted and dropped, then keep reading — never adopted | -//! | `ping` | ANSWERED on the child's stdin, then keep reading | -//! | a granted authority ask | RELAYED to busbar's caller, who answers it (Law 11) | -//! | an ungranted authority ask | refused on the child's stdin, then keep reading | -//! | a request busbar does not know | answered `-32601`, then keep reading | -//! -//! A request left unanswered is a child blocked forever on a reply, which presents as a hang — the -//! same failure mode a piped-and-undrained stderr produces, and it is refused for the same reason. -//! -//! ## THE THREE AUTHORITY ASKS ARE THE CALLER'S, BEHIND A DENY-BY-DEFAULT GRANT -//! -//! `sampling/createMessage`, `elicitation/create` and `roots/list` are asks for something only the -//! caller has: an LLM completion, a human's attention, the disclosure of filesystem structure. -//! Busbar answers none of them on the caller's behalf (Law 11): a granted ask goes to the caller of -//! the call the child is serving as plane traffic (BUSBAR-1.6.0 Part 3 B.3 item 10), and the -//! caller's answer comes back to the child under its own request id. Arriving over a child's stdout -//! does not make an ask cheaper than arriving inline in an `InputRequiredResult`, so it gets the -//! same gate: `super::jsonrpc::ServerRequestGrants`, all-false unless an operator set them, read as -//! a RELAY PERMISSION. -//! -//! - **[`AskOutcome::Relay`]** — the operator lets this server put that ask to its callers. -//! - **[`AskOutcome::Ungranted`]** — it does not. The ask is refused on the child's input -//! (`ask_ungranted`): busbar enforcing the operator's policy, not answering for the caller. The -//! remedy is `tools..grants.: true`. -//! -//! ## `ping` IS ANSWERED, and it is the one that is not an ask -//! -//! A ping carries no authority, discloses nothing, and its whole purpose is to let a peer tell a -//! live process from a wedged one. Refusing it would make busbar look dead to every child that -//! probes, which is the outcome the probe exists to detect. So it is answered with an empty result, -//! unconditionally, and it is the only server-originated request that is. -//! -//! ## Contents are NEVER read for a routing or trust decision -//! -//! The four "something changed" notifications can only bring a re-pull FORWARD, through -//! the engine's `mcp::client::catalogue::RefreshGate`, which is rate-limited. Their payloads are not parsed and not -//! believed. That is the engine's `mcp::client::catalogue`'s own rule — an attacker-controlled trigger may not choose -//! the moment freely and may not choose the content at all — and this module is the second place it -//! is now enforced rather than the first place it is bypassed. - -use super::jsonrpc::ServerRequestGrants; - -/// JSON-RPC standard: the method is not implemented. -/// -/// A local re-statement rather than an import of the engine's `mcp::envelope::code`, and it is the one -/// duplicated number in this module: that module's codes are `pub(super)`/`pub(in crate::mcp)` on -/// the SERVER plane's ingress, and widening their visibility so the client leg could borrow one -/// would make the ingress vocabulary reachable from the outbound half. The value is the JSON-RPC -/// specification's, not busbar's, so there is no busbar decision here for the two copies to drift on. -const METHOD_NOT_FOUND: i64 = -32601; - -/// The code busbar refuses an authority ask it will not relay with. -/// -/// `-32001`, in the implementation-defined server-error range, and NOT `-32601`: the method is -/// recognised and implemented, and the answer is a policy decision. Telling a child "no such method" -/// when the truth is "you may not have that" would send its author looking for a version mismatch. -pub const ASK_REFUSED: i64 = -32001; - -/// ONE MESSAGE A CHILD SENT, classified. -/// -/// Note what is NOT an arm: a RESPONSE. [`classify`] returns `None` for anything carrying `result` -/// or `error`, so a response can only ever leave this module as "not a server message" and be -/// correlated by the caller. Making a response representable here would create a second place a -/// response could be consumed, which is the desynchronisation this module exists to prevent. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ServerMessage { - /// A notification busbar recognises. No reply, ever. - Notification(ServerNotification), - /// A notification busbar does not recognise. Still no reply — a notification is defined as - /// unanswerable, and answering an unknown one would be busbar inventing a response to a message - /// whose sender is not listening. - UnknownNotification(String), - /// A request busbar recognises. MUST be answered. - Request { - /// The request's JSON-RPC id, echoed on the reply. - id: serde_json::Value, - /// Which of the four requests it is. - verb: ServerRequestVerb, - }, - /// A request busbar does not recognise. MUST STILL BE ANSWERED, with `-32601`. Dropping it - /// leaves the child blocked on a reply that never comes. - UnknownRequest { - /// The request's JSON-RPC id, echoed on the `-32601` reply. - id: serde_json::Value, - /// The method name as the peer sent it. - method: String, - }, -} - -/// THE NINE NOTIFICATIONS A SERVER MAY SEND, closed. -/// -/// Closed so that a tenth cannot be handled by a default arm that treats it as one of these — which -/// on the four refresh triggers would mean an unknown method able to drive busbar's re-pull. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ServerNotification { - /// The peer withdrew a request it had sent busbar. - Cancelled, - /// A log record. RFC 5424 severity in `params.level`. - Message, - /// Progress on a call busbar has in flight. - Progress, - /// The peer's prompt list changed. - PromptsListChanged, - /// The peer's resource list changed. - ResourcesListChanged, - /// One resource's contents changed. Distinct from `ResourcesListChanged`: the LIST is the same - /// and one member of it moved. - ResourcesUpdated, - /// The peer accepted a `subscriptions/listen` busbar sent. - SubscriptionsAcknowledged, - /// A task busbar created moved. - Tasks, - /// The peer's tool list changed. - ToolsListChanged, -} - -/// THE FOUR REQUESTS A SERVER MAY SEND, closed. Three of them are authority asks; one is a ping. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ServerRequestVerb { - /// Liveness. The only one that is not gated — see the module header. - Ping, - /// `roots/list`: disclose the caller's filesystem roots, if it answers. - RootsList, - /// `sampling/createMessage`: an LLM completion, which the caller runs if it answers. - SamplingCreateMessage, - /// `elicitation/create`: solicit user input. - ElicitationCreate, -} - -impl ServerRequestVerb { - /// The grant this request asks to spend, or `None` for the one that spends nothing. - /// - /// Returns the same three key words `ServerRequestGrants::allows` reads, taken from - /// [`super::jsonrpc::ServerAsk`] rather than spelled again — a second spelling of `"sampling"` - /// is a second thing to get wrong, and the failure mode is a grant check that silently never - /// matches and therefore always denies, which looks exactly like a correctly-configured denial. - pub fn ask(self) -> Option { - match self { - ServerRequestVerb::Ping => None, - ServerRequestVerb::RootsList => Some(super::jsonrpc::ServerAsk::Roots), - ServerRequestVerb::SamplingCreateMessage => Some(super::jsonrpc::ServerAsk::Sampling), - ServerRequestVerb::ElicitationCreate => Some(super::jsonrpc::ServerAsk::Elicitation), - } - } -} - -/// WHAT BUSBAR DOES about a notification it recognises. A closed set, so adding a notification -/// forces a decision about what it MEANS rather than letting it default to "nothing". -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum NotificationEffect { - /// Bring a `tools/list` re-pull forward, subject to the engine's `mcp::client::catalogue::RefreshGate`. The - /// notification's CONTENTS are not read — see the module header. - BringRefreshForward, - /// `notifications/resources/updated` — everything [`NotificationEffect::BringRefreshForward`] - /// does, PLUS record `(server, params.uri)` into the engine's `mcp::client::pool::ResourceUpdates` so the - /// server leg can relay the update onto `subscriptions/listen`'s `resourceSubscriptions` - /// category. The ONE effect that reads a field of a peer's notification, and the field is - /// believed nowhere — see that type's header for the exact bounds on what the reading can do. - RelayResourceUpdate, - /// Relay to the caller's progress channel, if this dispatch has one. - RelayProgress, - /// Record it at the mapped log level and nothing else. - Log, -} - -impl ServerNotification { - /// The effect, exhaustive and with no wildcard. - pub fn effect(self) -> NotificationEffect { - match self { - // The three "the shape of what you can call has moved" triggers. A refresh re-pulls - // the authoritative list and re-hashes it, which is the ONLY way an upstream can - // influence busbar's catalogue — and it influences the TIMING, never the content. - ServerNotification::ToolsListChanged - | ServerNotification::PromptsListChanged - | ServerNotification::ResourcesListChanged => NotificationEffect::BringRefreshForward, - // The fourth trigger still brings the refresh forward AND is the one notification with - // a second, named effect: the announced uri is recorded for the server-leg relay. - ServerNotification::ResourcesUpdated => NotificationEffect::RelayResourceUpdate, - ServerNotification::Progress => NotificationEffect::RelayProgress, - // Cancelled, Message, SubscriptionsAcknowledged and Tasks are RECORDED and no more. - // Acting on a peer's `cancelled` by aborting busbar's in-flight leg would hand a child - // process the ability to cancel a call its own operator's caller paid for, on nothing - // but an unauthenticated line of its own stdout. - ServerNotification::Cancelled - | ServerNotification::Message - | ServerNotification::SubscriptionsAcknowledged - | ServerNotification::Tasks => NotificationEffect::Log, - } - } -} - -/// CLASSIFY one line a child wrote. -/// -/// `None` means "this is not a server-originated message" — a response, or a line that is not a -/// JSON-RPC object at all. The caller correlates the former and refuses the latter; neither is this -/// module's decision. -/// -/// ## THE HANG THIS FUNCTION USED TO PRODUCE -/// -/// JSON-RPC 2.0 section 4 is explicit: a notification is a request whose `id` member is ABSENT — not one -/// whose `id` holds `null`. This function used to decide notification-ness by filtering `id` on -/// nullness (`.filter(|v| !v.is_null())`), which collapses a PRESENT-but-null id into "absent". Many -/// JSON-RPC encoders — serde's default among them, for any struct whose `id` field always serializes -/// — spell an absent id as an explicit `null` on the wire. Such a peer's `roots/list` was therefore -/// read as a notification, busbar never replied, and the child blocked forever on an answer that was -/// never coming: a well-behaved, conformant peer produced a permanent, silent hang. -/// -/// The discriminator is now the METHOD FIRST, against the closed notification table -/// ([`notification_of`]): a known notification method is a notification regardless of what `id` -/// carries. Everything else is read by whether the `id` MEMBER IS PRESENT, never by its value — so a -/// request method with an explicit `null` id is still a request, and MUST be answered, echoing that -/// same `null` back as the JSON-RPC base specification allows. An `id`-absent line on an unrecognised -/// method still reads as a notification (see [`ServerMessage::UnknownNotification`]): the fix is -/// "read presence, not nullness", never "answer everything". -pub fn classify(value: &serde_json::Value) -> Option { - let obj = value.as_object()?; - // A response, not a message. Checked FIRST and by the presence of the members rather than by the - // absence of `method`, so a malformed line carrying both is read as the response it claims to be - // and fails correlation — rather than being executed as a request the peer never meant to send. - if obj.contains_key("result") || obj.contains_key("error") { - return None; - } - let method = obj.get("method").and_then(|m| m.as_str())?; - // METHOD FIRST: a known notification method is a notification no matter what `id` holds — see - // the header above for why reading `id`'s value at all, before this check, is the defect. - if let Some(n) = notification_of(method) { - return Some(ServerMessage::Notification(n)); - } - // Now `id` is read by PRESENCE only. `unwrap_or(Value::Null)` cannot silently invent a value: the - // `contains_key` guard above already proved the member is there, so this only ever unwraps a - // `Some` — a defensive fallback, not a second decision. - if !obj.contains_key("id") { - return Some(ServerMessage::UnknownNotification(method.to_string())); - } - let id = obj.get("id").cloned().unwrap_or(serde_json::Value::Null); - Some(match request_of(method) { - Some(verb) => ServerMessage::Request { id, verb }, - None => ServerMessage::UnknownRequest { - id, - method: method.to_string(), - }, - }) -} - -/// The method-name table for notifications. One table, read in one direction, so a name cannot be -/// recognised here and spelled differently anywhere else. -fn notification_of(method: &str) -> Option { - Some(match method { - "notifications/cancelled" => ServerNotification::Cancelled, - "notifications/message" => ServerNotification::Message, - "notifications/progress" => ServerNotification::Progress, - "notifications/prompts/list_changed" => ServerNotification::PromptsListChanged, - "notifications/resources/list_changed" => ServerNotification::ResourcesListChanged, - "notifications/resources/updated" => ServerNotification::ResourcesUpdated, - "notifications/subscriptions/acknowledged" => ServerNotification::SubscriptionsAcknowledged, - "notifications/tasks" => ServerNotification::Tasks, - "notifications/tools/list_changed" => ServerNotification::ToolsListChanged, - _ => return None, - }) -} - -fn request_of(method: &str) -> Option { - Some(match method { - "ping" => ServerRequestVerb::Ping, - "roots/list" => ServerRequestVerb::RootsList, - "sampling/createMessage" => ServerRequestVerb::SamplingCreateMessage, - "elicitation/create" => ServerRequestVerb::ElicitationCreate, - _ => return None, - }) -} - -/// WHAT BECOMES OF an authority ask: relayed to the caller, or refused by the operator's grant. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum AskOutcome { - /// The grant is held: the ask goes to busbar's caller, who answers it. - Relay, - /// No grant. The remedy is `tools..grants.: true`. - Ungranted, -} - -/// DECIDE an authority ask against the operator's per-server grants. -/// -/// Takes the grants as a VALUE read at the moment of the decision rather than a captured one: there -/// is no handshake to authorise once, so a revocation has to bite on the next message and not at the -/// end of a stream that has no end. -pub fn decide_ask(ask: super::jsonrpc::ServerAsk, grants: &ServerRequestGrants) -> AskOutcome { - if grants.allows(ask) { - AskOutcome::Relay - } else { - AskOutcome::Ungranted - } -} - -/// THE REPLY BUSBAR WRITES BACK on the child's stdin, as one JSON-RPC response value: `ping`'s empty -/// result, or an ungranted ask's refusal. `None` for a granted ask: it is the caller's to answer, -/// and busbar writes nothing in its place (Law 11). -pub fn answer( - id: &serde_json::Value, - verb: ServerRequestVerb, - grants: &ServerRequestGrants, - server: &str, -) -> Option { - let Some(ask) = verb.ask() else { - // `ping`. An empty result is the whole of the specified answer. - return Some(serde_json::json!({ "jsonrpc": "2.0", "id": id, "result": {} })); - }; - let kind = ask.key(); - match decide_ask(ask, grants) { - AskOutcome::Relay => None, - AskOutcome::Ungranted => Some(refused( - id, - "ask_ungranted", - format!( - "server `{server}` asked for `{kind}` and its registry entry carries no `{kind}` \ - grant, so the ask is not relayed to busbar's caller. Set \ - `tools.{server}.grants.{kind}: true` if the operator intends this server to put \ - that ask to its callers." - ), - )), - } -} - -/// A REFUSAL of an ask of the child's own, written on its input: `-32001`, `message`, and the audit -/// reason word in `data` (the operator's grant, the round cap, or a relay the deployment cannot -/// carry). -pub fn refused(id: &serde_json::Value, reason: &str, message: String) -> serde_json::Value { - serde_json::json!({ - "jsonrpc": "2.0", - "id": id, - "error": { "code": ASK_REFUSED, "message": message, "data": { "reason": reason } }, - }) -} - -/// The `-32601` a child gets for a request busbar does not implement. -/// -/// ANSWERED rather than dropped, which is the point: a dropped request is a child blocked on a -/// reply forever, and a hang is a worse diagnosis than a refusal for exactly the reason -/// the engine's `mcp::client::stdio` inherits stderr. -pub fn method_not_found(id: &serde_json::Value, method: &str) -> serde_json::Value { - error_reply( - id, - METHOD_NOT_FOUND, - format!( - "busbar's MCP client leg does not implement `{method}`; it answers `ping` and gates \ - `roots/list`, `sampling/createMessage` and `elicitation/create` on the operator's \ - per-server grants." - ), - ) -} - -fn error_reply(id: &serde_json::Value, code: i64, message: String) -> serde_json::Value { - serde_json::json!({ - "jsonrpc": "2.0", - "id": id, - "error": { "code": code, "message": message }, - }) -} - -#[cfg(test)] -#[path = "tests/peer_tests.rs"] -mod peer_tests; diff --git a/crates/busbar-plane-mcp/src/client/tests/ask_tests.rs b/crates/busbar-plane-mcp/src/client/tests/ask_tests.rs deleted file mode 100644 index fe168a16de..0000000000 --- a/crates/busbar-plane-mcp/src/client/tests/ask_tests.rs +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! An upstream's `InputRequiredResult`, recognised as an ask and judged against grants that are -//! deny-by-default. A granted ask is relayed to the caller, who answers it; busbar answers none -//! (Law 11). The relay itself, and the per-dispatch round cap, are `crate::call`'s and its tests'. - -use crate::client::jsonrpc::{parse_response, RpcOutcome, ServerAsk, ServerRequestGrants}; - -/// An `InputRequiredResult` in the shape the SPECIFICATION defines, which is the only shape a -/// conformant upstream ever puts on the wire. -/// -/// `mrtr.mdx:126-180` and the SDK's `spec.types.2026-07-28.ts`: the discriminator is `resultType`, -/// the asks are a MAP at `inputRequests` keyed by server-assigned identifiers, and each value is a -/// request object naming a `method`. There is no `type` field and no `request` field anywhere in -/// `InputRequiredResult`. -/// -/// This function used to mint `{"type":"input_required","request":"…"}` — a shape busbar invented. -/// The fixture and the parser agreed with each other and with no server in existence, so every test -/// below passed while the gate they describe was open against every real upstream. -fn input_required(method: &str) -> Vec { - serde_json::to_vec(&serde_json::json!({ - "jsonrpc": "2.0", - "id": 1, - "result": { - "resultType": "input_required", - "inputRequests": { "ask_0": { "method": method, "params": {} } }, - "requestState": "opaque-to-busbar", - }, - })) - .expect("fixture serialises") -} - -#[test] -fn an_input_required_result_is_recognised_and_not_reported_as_a_plain_result() { - let cases = [ - ("sampling/createMessage", ServerAsk::Sampling), - ("elicitation/create", ServerAsk::Elicitation), - ("roots/list", ServerAsk::Roots), - ]; - assert_eq!(cases.len(), 3); - for (request, expected) in cases { - assert_eq!( - parse_response(&input_required(request), 1), - RpcOutcome::InputRequired { kind: expected }, - "`{request}` must be recognised as an ask" - ); - } -} - -/// An ask naming something we do not recognise is still an ask. Returning it as a plain result would -/// hand an unrecognised request for authority straight through. -#[test] -fn an_unrecognised_ask_is_refused_rather_than_passed_through_as_a_result() { - assert_eq!( - parse_response(&input_required("some/future/method"), 1), - RpcOutcome::InputRequired { - kind: ServerAsk::Sampling - } - ); -} - -/// `mrtr.mdx:245`: a server MUST include AT LEAST ONE of `inputRequests` or `requestState`. So a -/// result carrying `resultType: "input_required"` and nothing else is a conformant ask, and it must -/// terminate exactly like one that names methods. Keying the recognition off `inputRequests` alone -/// would let an upstream slip an ask through by omitting the field the spec makes optional. -#[test] -fn an_ask_carrying_only_request_state_is_still_an_ask() { - let body = serde_json::to_vec(&serde_json::json!({ - "jsonrpc": "2.0", "id": 1, - "result": { "resultType": "input_required", "requestState": "opaque" }, - })) - .expect("fixture serialises"); - assert_eq!( - parse_response(&body, 1), - RpcOutcome::InputRequired { - kind: ServerAsk::Sampling - }, - "an ask with no `inputRequests` is still an ask" - ); -} - -/// The recogniser must not over-match either: a plain, complete result is a result, and answering -/// `InputRequired` to one would refuse every ordinary tool call. -#[test] -fn a_complete_result_is_not_mistaken_for_an_ask() { - for result in [ - serde_json::json!({ "content": [{ "type": "text", "text": "hi" }] }), - serde_json::json!({ "resultType": "complete", "content": [] }), - // `type` appearing INSIDE content is the everyday case and must stay a result. - serde_json::json!({ "content": [{ "type": "input_required", "text": "not a discriminator" }] }), - ] { - let body = serde_json::to_vec(&serde_json::json!({ - "jsonrpc": "2.0", "id": 1, "result": result, - })) - .expect("fixture serialises"); - assert!( - matches!(parse_response(&body, 1), RpcOutcome::Result(_)), - "must remain a plain result: {result}" - ); - } -} - -/// A single `inputRequests` map can name SEVERAL methods (`mrtr.mdx` allows it, and the conformance -/// suite's `multiple-input-requests` scenario requires a server to emit three at once). busbar must -/// judge against the MOST PRIVILEGED grant in the set, not the first key it happens to iterate: -/// judging against the cheapest ask would let an upstream smuggle a sampling request behind a -/// `roots/list` it knows the operator lets it relay. -#[test] -fn a_multi_method_ask_is_judged_against_the_most_privileged_method_it_names() { - let body = serde_json::to_vec(&serde_json::json!({ - "jsonrpc": "2.0", "id": 1, - "result": { - "resultType": "input_required", - "inputRequests": { - "a": { "method": "roots/list", "params": {} }, - "b": { "method": "sampling/createMessage", "params": {} }, - }, - }, - })) - .expect("fixture serialises"); - assert_eq!( - parse_response(&body, 1), - RpcOutcome::InputRequired { - kind: ServerAsk::Sampling - } - ); -} - -#[test] -fn grants_are_all_false_at_construction_and_there_is_no_way_to_turn_them_all_on() { - let g = ServerRequestGrants::default(); - for ask in [ - ServerAsk::Sampling, - ServerAsk::Elicitation, - ServerAsk::Roots, - ] { - assert!(!g.allows(ask), "{ask:?} must be denied by default"); - } -} diff --git a/crates/busbar-plane-mcp/src/client/tests/compat_tests.rs b/crates/busbar-plane-mcp/src/client/tests/compat_tests.rs deleted file mode 100644 index 2d273a4555..0000000000 --- a/crates/busbar-plane-mcp/src/client/tests/compat_tests.rs +++ /dev/null @@ -1,232 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -use super::*; -use crate::client::jsonrpc::envelope; -use serde_json::json; - -fn stateless_call() -> OutboundRequest { - envelope( - "https://up.example/rpc", - "tools/call", - Some("echo"), - json!({"jsonrpc":"2.0","id":5,"method":"tools/call","params":{ - "name":"echo","arguments":{"x":"y"}, - "_meta":{META_PROTOCOL_VERSION:"2026-07-28",META_CLIENT_CAPABILITIES:{},"progressToken":3}}}), - Some("tok"), - ) -} - -fn header<'a>(r: &'a OutboundRequest, name: &str) -> Option<&'a str> { - r.headers - .iter() - .find(|(k, _)| k == name) - .map(|(_, v)| v.as_str()) -} - -#[test] -fn a_refusal_moves_the_ladder_and_an_outage_does_not() { - assert_eq!(probe_outcome(400, b""), StepOutcome::Refused4xx); - assert_eq!(probe_outcome(404, b"{}"), StepOutcome::Refused4xx); - assert_eq!(probe_outcome(502, b""), StepOutcome::Unreachable); - for code in [-32601, -32600, -32602, -32022] { - let body = json!({"jsonrpc":"2.0","id":1,"error":{"code":code,"message":"m"}}).to_string(); - assert_eq!( - probe_outcome(200, body.as_bytes()), - StepOutcome::Refused4xx, - "{code}" - ); - } - let other = json!({"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"m"}}).to_string(); - assert_eq!(probe_outcome(200, other.as_bytes()), StepOutcome::Answered); - assert_eq!( - probe_outcome(200, br#"{"jsonrpc":"2.0","id":1,"result":{}}"#), - StepOutcome::Answered - ); -} - -#[test] -fn lowering_drops_the_stateless_meta_and_mirror_and_keeps_the_credential() { - let lowered = lower_request( - &stateless_call(), - "https://up.example/rpc", - Revision::R2025_06_18, - Some("sid1"), - ); - let body: Value = serde_json::from_slice(&lowered.body).unwrap(); - assert_eq!( - body["params"]["_meta"], - json!({"progressToken":3}), - "only the client's own _meta survives" - ); - assert_eq!(body["params"]["arguments"], json!({"x":"y"})); - assert_eq!(header(&lowered, H_PROTOCOL_VERSION), Some("2025-06-18")); - assert_eq!(header(&lowered, H_SESSION), Some("sid1")); - assert_eq!(header(&lowered, H_MCP_METHOD), None); - assert_eq!(header(&lowered, H_MCP_NAME), None); - assert_eq!(header(&lowered, "authorization"), Some("Bearer tok")); -} - -#[test] -fn the_event_stream_revision_carries_no_version_header_and_an_empty_meta_goes() { - let mut req = stateless_call(); - let mut body: Value = serde_json::from_slice(&req.body).unwrap(); - body["params"]["_meta"] = - json!({META_PROTOCOL_VERSION:"2026-07-28",META_CLIENT_CAPABILITIES:{}}); - req.body = serde_json::to_vec(&body).unwrap(); - let lowered = lower_request( - &req, - "https://up.example/messages?sessionId=a", - Revision::R2024_11_05, - None, - ); - assert_eq!(lowered.url, "https://up.example/messages?sessionId=a"); - assert_eq!(header(&lowered, H_PROTOCOL_VERSION), None); - let body: Value = serde_json::from_slice(&lowered.body).unwrap(); - assert!(body["params"].get("_meta").is_none()); -} - -#[test] -fn initialize_asks_for_a_revision_and_names_busbar() { - let init = initialize_request( - &stateless_call(), - "https://up.example/rpc", - Revision::R2025_11_25, - 9, - "1.6.0", - ); - let body: Value = serde_json::from_slice(&init.body).unwrap(); - assert_eq!(body["method"], "initialize"); - assert_eq!(body["params"]["protocolVersion"], "2025-11-25"); - assert_eq!(body["params"]["clientInfo"]["version"], "1.6.0"); - assert_eq!( - header(&init, H_PROTOCOL_VERSION), - None, - "initialize precedes the header" - ); - assert_eq!(header(&init, "authorization"), Some("Bearer tok")); - let done = initialized_notification( - &init, - "https://up.example/rpc", - Revision::R2025_11_25, - Some("s"), - ); - let body: Value = serde_json::from_slice(&done.body).unwrap(); - assert!(body.get("id").is_none()); - assert_eq!(header(&done, H_SESSION), Some("s")); -} - -#[test] -fn only_a_revision_this_plane_carries_is_accepted_from_initialize() { - let ok = json!({"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18"}}).to_string(); - assert_eq!(offered_revision(ok.as_bytes()), Some(Revision::R2025_06_18)); - let old = json!({"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26"}}).to_string(); - assert_eq!(offered_revision(old.as_bytes()), None); - assert_eq!(offered_revision(b"not json"), None); -} - -/// RED: the message address a `2024-11-05` stream names can never move busbar's POSTs (and the -/// credential on them) to another origin. -#[test] -fn red_a_message_address_on_another_origin_is_refused() { - let base = "https://up.example:8443/events"; - assert_eq!( - message_address(base, "/messages?sessionId=1").as_deref(), - Ok("https://up.example:8443/messages?sessionId=1") - ); - assert_eq!( - message_address(base, "https://UP.example:8443/m").as_deref(), - Ok("https://UP.example:8443/m") - ); - for hostile in [ - "https://evil.example/m", - "https://up.example/m", - "//evil.example/m", - ] { - assert!(message_address(base, hostile).is_err(), "{hostile}"); - } - assert_eq!( - message_address(base, "https://evil.example/m"), - Err(AddressRefused::CrossOrigin) - ); - assert_eq!( - message_address(base, "messages"), - Err(AddressRefused::Malformed) - ); - assert_eq!( - message_address(base, "/a b"), - Err(AddressRefused::Malformed) - ); -} - -#[test] -fn the_event_reader_joins_split_chunks_and_is_bounded() { - let mut r = EventReader::new(64); - assert!(r.feed(b"event: endpoint\r\ndata: /m?s").is_empty()); - let got = r.feed(b"=1\r\n\r\n: keepalive\n\nid: 0-1\ndata: {\"a\":\ndata: 1}\n\n"); - assert_eq!(got.len(), 2); - assert_eq!(got[0].event.as_deref(), Some("endpoint")); - assert_eq!(got[0].data, "/m?s=1"); - assert_eq!(got[1].id.as_deref(), Some("0-1")); - assert_eq!(got[1].data, "{\"a\":\n1}"); - assert!(r.feed(&[b'x'; 100]).is_empty()); - assert!( - r.overflowed(), - "an event that never ends is dropped, not buffered forever" - ); -} - -fn ev(event: Option<&str>, data: &str) -> StreamEvent { - StreamEvent { - event: event.map(str::to_string), - id: None, - data: data.to_string(), - } -} - -#[test] -fn a_link_is_ready_once_and_answers_only_what_it_waits_for() { - let mut link = EventStreamLink::new("https://up.example/events", 2); - assert_eq!(link.address(), None); - assert_eq!( - link.on_event(&ev(Some("endpoint"), "/messages?sessionId=a")), - LinkEffect::Ready("https://up.example/messages?sessionId=a".to_string()) - ); - assert_eq!( - link.on_event(&ev(Some("endpoint"), "/elsewhere")), - LinkEffect::Ignored, - "the address is named once" - ); - assert!(link.expect(&json!(7))); - assert!(!link.expect(&json!(7)), "one waiter per id"); - assert!(link.expect(&json!("x"))); - assert!(!link.expect(&json!(9)), "bounded"); - let answer = r#"{"jsonrpc":"2.0","id":7,"result":{}}"#; - assert!( - matches!(link.on_event(&ev(Some("message"), answer)), LinkEffect::Answer { ref id, .. } if id == "7") - ); - assert_eq!( - link.on_event(&ev(Some("message"), answer)), - LinkEffect::Ignored, - "answered once" - ); - let unsolicited = r#"{"jsonrpc":"2.0","id":99,"result":{}}"#; - assert_eq!(link.on_event(&ev(None, unsolicited)), LinkEffect::Ignored); - let note = r#"{"jsonrpc":"2.0","method":"notifications/tools/list_changed"}"#; - assert!(matches!( - link.on_event(&ev(Some("message"), note)), - LinkEffect::FromPeer(_) - )); - link.abandon(&json!("x")); - assert!(link.expect(&json!(9))); -} - -#[test] -fn red_a_link_refuses_a_cross_origin_endpoint() { - let mut link = EventStreamLink::new("https://up.example/events", 2); - assert_eq!( - link.on_event(&ev(Some("endpoint"), "https://evil.example/m")), - LinkEffect::Refused(AddressRefused::CrossOrigin) - ); - assert_eq!(link.address(), None); -} diff --git a/crates/busbar-plane-mcp/src/client/tests/negotiate_tests.rs b/crates/busbar-plane-mcp/src/client/tests/negotiate_tests.rs deleted file mode 100644 index 0d7439d981..0000000000 --- a/crates/busbar-plane-mcp/src/client/tests/negotiate_tests.rs +++ /dev/null @@ -1,244 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! The negotiator driven against FAKE upstreams, one per revision: the fake hop the adapter -//! contract describes, answering as each revision's server would. - -use super::*; -use crate::client::jsonrpc::envelope; -use crate::codec::{H_PROTOCOL_VERSION, META_CLIENT_CAPABILITIES, META_PROTOCOL_VERSION}; -use serde_json::json; - -const URL: &str = "https://up.example/rpc"; - -fn call() -> OutboundRequest { - envelope( - URL, - "tools/call", - Some("echo"), - json!({"jsonrpc":"2.0","id":5,"method":"tools/call","params":{ - "name":"echo","arguments":{}, - "_meta":{META_PROTOCOL_VERSION:"2026-07-28",META_CLIENT_CAPABILITIES:{}}}}), - Some("tok"), - ) -} - -fn answer(status: u16, body: Value) -> Option { - Some(HopAnswer { - status, - fields: Vec::new(), - body: serde_json::to_vec(&body).unwrap(), - }) -} - -fn with_session(status: u16, body: Value, sid: &str) -> Option { - Some(HopAnswer { - status, - fields: vec![("mcp-session-id".to_string(), sid.to_string())], - body: serde_json::to_vec(&body).unwrap(), - }) -} - -fn accepted() -> Option { - Some(HopAnswer { - status: 202, - fields: Vec::new(), - body: Vec::new(), - }) -} - -fn sent(a: &Action) -> (&OutboundRequest, Value) { - match a { - Action::Send { request, .. } => ( - request, - serde_json::from_slice(&request.body).unwrap_or(Value::Null), - ), - other => panic!("expected a Send, got {other:?}"), - } -} - -fn header<'a>(r: &'a OutboundRequest, name: &str) -> Option<&'a str> { - r.headers - .iter() - .find(|(k, _)| k == name) - .map(|(_, v)| v.as_str()) -} - -fn result(id: Value, result: Value) -> Value { - json!({"jsonrpc":"2.0","id":id,"result":result}) -} - -fn event(name: Option<&str>, data: &str) -> StreamEvent { - StreamEvent { - event: name.map(str::to_string), - id: None, - data: data.to_string(), - } -} - -#[test] -fn a_stateless_upstream_is_answered_by_the_request_itself() { - let mut n = Negotiator::new(call(), None, "1.6.0"); - let first = n.start(); - assert_eq!( - sent(&first).0, - &call(), - "the stateless request goes out unchanged" - ); - let done = n.on_answer(answer(200, result(json!(5), json!({"content":[]})))); - assert!(matches!(done, Action::Finish(ref a) if a.status == 200)); - assert_eq!(n.remembered().unwrap().revision, Revision::R2026_07_28); -} - -#[test] -fn a_session_upstream_is_initialized_and_the_call_lowered_into_its_session() { - let mut n = Negotiator::new(call(), None, "1.6.0"); - n.start(); - let init = n.on_answer(answer( - 400, - json!({"jsonrpc":"2.0","id":null,"error":{"code":-32000,"message":"no session"}}), - )); - let (req, body) = sent(&init); - assert_eq!(body["method"], "initialize"); - assert_eq!(body["params"]["protocolVersion"], "2025-11-25"); - assert_eq!(header(req, "authorization"), Some("Bearer tok")); - let done_init = n.on_answer(with_session( - 200, - result(body["id"].clone(), json!({"protocolVersion":"2025-06-18"})), - "abc", - )); - let (req, body) = sent(&done_init); - assert_eq!(body["method"], "notifications/initialized"); - assert_eq!(header(req, "mcp-session-id"), Some("abc")); - assert_eq!(header(req, H_PROTOCOL_VERSION), Some("2025-06-18")); - let the_call = n.on_answer(accepted()); - let (req, body) = sent(&the_call); - assert_eq!(body["method"], "tools/call"); - assert!( - body["params"].get("_meta").is_none(), - "lowered: the stateless _meta is gone" - ); - assert_eq!(header(req, "mcp-session-id"), Some("abc")); - let fin = n.on_answer(answer(200, result(json!(5), json!({"content":[]})))); - assert!(matches!(fin, Action::Finish(_))); - let r = n.remembered().unwrap(); - assert_eq!( - (r.revision, r.session.as_deref()), - (Revision::R2025_06_18, Some("abc")) - ); -} - -#[test] -fn a_lost_session_is_reinitialized_once_and_never_looped() { - let memo = Remembered { - revision: Revision::R2025_11_25, - session: Some("old".into()), - message_address: None, - }; - let mut n = Negotiator::new(call(), Some(memo), "1.6.0"); - let first = n.start(); - let (req, _) = sent(&first); - assert_eq!(header(req, "mcp-session-id"), Some("old")); - let init = n.on_answer(answer(404, json!({}))); - assert_eq!(sent(&init).1["method"], "initialize"); - let id = sent(&init).1["id"].clone(); - n.on_answer(with_session( - 200, - result(id, json!({"protocolVersion":"2025-11-25"})), - "new", - )); - let again = n.on_answer(accepted()); - assert_eq!(header(sent(&again).0, "mcp-session-id"), Some("new")); - // The fresh session is lost too: the answer is handed back, not a second re-initialise. - let fin = n.on_answer(answer(404, json!({}))); - assert!(matches!(fin, Action::Finish(ref a) if a.status == 404)); -} - -#[test] -fn an_outage_does_not_move_the_ladder() { - let mut n = Negotiator::new(call(), None, "1.6.0"); - n.start(); - assert!(matches!( - n.on_answer(answer(503, json!({}))), - Action::Fail(Refusal::Unreachable(_)) - )); - let mut n = Negotiator::new(call(), None, "1.6.0"); - n.start(); - assert_eq!(n.on_answer(None), Action::Fail(Refusal::Unreachable(None))); - assert_eq!(n.remembered(), None); -} - -#[test] -fn an_offer_this_plane_cannot_carry_is_a_disconnect() { - let mut n = Negotiator::new(call(), None, "1.6.0"); - n.start(); - let init = n.on_answer(answer(400, json!({}))); - let id = sent(&init).1["id"].clone(); - let fin = n.on_answer(answer( - 200, - result(id, json!({"protocolVersion":"2025-03-26"})), - )); - assert_eq!(fin, Action::Fail(Refusal::OfferedUnsupported)); -} - -#[test] -fn an_event_stream_upstream_is_reached_through_the_address_its_stream_names() { - let mut n = Negotiator::new(call(), None, "1.6.0"); - n.start(); - n.on_answer(answer(404, json!({}))); - let open = n.on_answer(answer(404, json!({}))); - let Action::OpenStream { request } = open else { - panic!("expected the stream, got {open:?}") - }; - assert!(request.body.is_empty()); - assert_eq!(header(&request, "accept"), Some("text/event-stream")); - assert_eq!(header(&request, "authorization"), Some("Bearer tok")); - assert_eq!(n.on_answer(answer(200, json!(null))), Action::AwaitEvent); - let init = n.on_event(&event(Some("endpoint"), "/messages?sessionId=s1")); - let (req, body) = sent(&init); - assert_eq!(req.url, "https://up.example/messages?sessionId=s1"); - assert_eq!(body["params"]["protocolVersion"], "2024-11-05"); - assert_eq!(header(req, H_PROTOCOL_VERSION), None); - let id = body["id"].clone(); - assert_eq!(n.on_answer(accepted()), Action::AwaitEvent); - let initialized = n.on_event(&event( - Some("message"), - &result(id, json!({"protocolVersion":"2024-11-05"})).to_string(), - )); - assert_eq!(sent(&initialized).1["method"], "notifications/initialized"); - let the_call = n.on_answer(accepted()); - let (req, body) = sent(&the_call); - assert_eq!(req.url, "https://up.example/messages?sessionId=s1"); - assert_eq!(body["id"], 5); - assert_eq!(n.on_answer(accepted()), Action::AwaitEvent); - let unrelated = result(json!(77), json!({})).to_string(); - assert_eq!( - n.on_event(&event(Some("message"), &unrelated)), - Action::AwaitEvent - ); - let fin = n.on_event(&event( - Some("message"), - &result(json!(5), json!({"content":[]})).to_string(), - )); - let Action::Finish(a) = fin else { - panic!("expected the answer, got {fin:?}") - }; - assert_eq!(serde_json::from_slice::(&a.body).unwrap()["id"], 5); - assert_eq!(n.remembered().unwrap().revision, Revision::R2024_11_05); -} - -/// RED: a stream naming a message address on another origin gets nothing POSTed to it, so the -/// credential never leaves for a host the operator did not register. -#[test] -fn red_a_cross_origin_message_address_gets_no_request() { - let memo = Remembered { - revision: Revision::R2024_11_05, - session: None, - message_address: None, - }; - let mut n = Negotiator::new(call(), Some(memo), "1.6.0"); - assert!(matches!(n.start(), Action::OpenStream { .. })); - n.on_answer(answer(200, json!(null))); - let a = n.on_event(&event(Some("endpoint"), "https://evil.example/steal")); - assert_eq!(a, Action::Fail(Refusal::AddressRefused)); -} diff --git a/crates/busbar-plane-mcp/src/client/tests/peer_tests.rs b/crates/busbar-plane-mcp/src/client/tests/peer_tests.rs deleted file mode 100644 index 9d34811e56..0000000000 --- a/crates/busbar-plane-mcp/src/client/tests/peer_tests.rs +++ /dev/null @@ -1,348 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! WHAT A CHILD SENDS BUSBAR — classification, the deny-by-default gate on the three authority -//! asks, and the two facts that stop a well-behaved child from desynchronising the stream. -//! -//! The paired end-to-end battery is the engine's `mcp::tests/stdio_client_leg_tests.rs`: this file proves -//! what the classifier DECIDES, that one proves a real child process's real notifications and real -//! requests reach it. Neither substitutes for the other, and the reason is the one -//! `stdio_dispatch_tests.rs` records: a complete, adversarially tested classifier that nothing calls -//! is what the deleted stdio transport was. - -use super::super::jsonrpc::ServerAsk; -use super::super::peer::{ - answer, classify, decide_ask, method_not_found, AskOutcome, NotificationEffect, ServerMessage, - ServerNotification, ServerRequestVerb, -}; -use crate::client::jsonrpc::ServerRequestGrants; - -fn line(raw: &str) -> serde_json::Value { - serde_json::from_str(raw).expect("the fixture is JSON") -} - -/// EVERY server-originated method in the inventory column is classified, and none of them lands in -/// an "unknown" arm. -/// -/// Read from the GENERATED matrix for the reason `verb_tests` gives: a list written from knowledge -/// of the specification is how a column silently ends early. -#[test] -fn every_server_originated_method_in_the_inventory_is_classified() { - // Compiled in rather than read at run time: the plane performs no input or output, its tests - // included (tests/purity.rs). - let raw = include_str!("../../../../../qa/method-inventory.json"); - let doc: serde_json::Value = serde_json::from_str(raw).expect("parses"); - let owed: Vec = doc["cells"] - .as_array() - .expect("cells") - .iter() - .filter(|c| { - c["protocol"] == "mcp" - && c["transport"] == "stdio" - && c["role"] == "client" - && c["originator"] == "server" - && c["na_reason"].is_null() - }) - .map(|c| c["method"].as_str().expect("method").to_string()) - .collect(); - assert!( - owed.len() >= 12, - "only {} server-originated stdio cells found; a filter that matched nothing would report \ - complete handling of nothing", - owed.len() - ); - for method in &owed { - // A NOTIFICATION-SHAPED LINE (no id) and a REQUEST-SHAPED one (with id) are BOTH tried, and - // exactly one of them must be recognised: `ping` is only ever a request and - // `notifications/message` is only ever a notification, so requiring both to work would be - // requiring busbar to accept a shape the specification does not define. - let as_notification = classify(&line(&format!( - r#"{{"jsonrpc":"2.0","method":"{method}","params":{{}}}}"# - ))); - let as_request = classify(&line(&format!( - r#"{{"jsonrpc":"2.0","id":7,"method":"{method}","params":{{}}}}"# - ))); - let known_notification = matches!(as_notification, Some(ServerMessage::Notification(_))); - let known_request = matches!(as_request, Some(ServerMessage::Request { .. })); - assert!( - known_notification || known_request, - "`{method}` is in the matrix as a message a server sends busbar and the classifier \ - recognises neither shape of it: {as_notification:?} / {as_request:?}" - ); - } -} - -/// A RESPONSE IS NOT A SERVER MESSAGE, and this is the assertion that keeps the stream in sync. -/// -/// If a response could be classified as anything, the read loop would consume it as an interleaved -/// message and go on waiting for an answer that has already arrived. -#[test] -fn a_response_is_never_a_server_message() { - for raw in [ - r#"{"jsonrpc":"2.0","id":1,"result":{"content":[]}}"#, - r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"no"}}"#, - // The adversarial one: a line carrying BOTH a result and a method. It must read as the - // response it claims to be, never be executed as a request the peer did not send. - r#"{"jsonrpc":"2.0","id":1,"result":{},"method":"sampling/createMessage"}"#, - ] { - assert_eq!( - classify(&line(raw)), - None, - "a response must never classify as a server-originated message: {raw}" - ); - } -} - -/// A NOTIFICATION METHOD IS A NOTIFICATION REGARDLESS OF WHAT `id` CARRIES. -/// -/// Notification-ness is decided by the METHOD, against the closed table, BEFORE `id` is ever read — -/// see [`classify`]'s header. A peer that sends `notifications/message` with an explicit `id: null` -/// is still not asking for a reply: the method itself is one the specification defines as -/// fire-and-forget, and that does not change because the wire happens to carry a stray `id` member. -#[test] -fn a_notification_method_is_a_notification_even_with_a_null_id() { - assert_eq!( - classify(&line( - r#"{"jsonrpc":"2.0","id":null,"method":"notifications/message","params":{}}"# - )), - Some(ServerMessage::Notification(ServerNotification::Message)) - ); -} - -/// THE HANG THIS MODULE USED TO PRODUCE: A PRESENT-BUT-NULL `id` ON A REQUEST METHOD MUST STILL BE -/// ANSWERED. -/// -/// JSON-RPC 2.0 section 4 defines a notification as a request whose `id` MEMBER IS ABSENT — not one whose -/// `id` holds `null`. Many JSON-RPC encoders (serde's default among them) spell an absent field as an -/// explicit `null` when the struct always emits the key. A classifier that filtered `id` by nullness -/// before consulting the method table read such a peer's `roots/list` as a notification, never -/// replied, and left the child blocked forever waiting for the response that was never coming — the -/// defect this module's header now states first. -#[test] -fn a_present_but_null_id_on_a_request_method_is_still_a_request() { - assert_eq!( - classify(&line( - r#"{"jsonrpc":"2.0","id":null,"method":"roots/list","params":{}}"# - )), - Some(ServerMessage::Request { - id: serde_json::Value::Null, - verb: ServerRequestVerb::RootsList, - }), - "an explicit null id on a REQUEST method must still be answered, not silently dropped" - ); -} - -/// A TRUE NOTIFICATION — `id` ABSENT ENTIRELY — STILL GETS NO REPLY. -/// -/// The fix for the hang above must not be "answer everything": an `id`-absent line is exactly what -/// the base specification calls a notification, and answering one would put a response on the wire -/// that nothing sent an id to correlate. -#[test] -fn a_true_notification_with_id_absent_is_still_unanswered() { - assert_eq!( - classify(&line( - r#"{"jsonrpc":"2.0","method":"notifications/message","params":{}}"# - )), - Some(ServerMessage::Notification(ServerNotification::Message)) - ); -} - -/// AN UNKNOWN REQUEST IS ANSWERED `-32601`. AN UNKNOWN NOTIFICATION IS NOT ANSWERED AT ALL. -/// -/// The asymmetry is the whole point: a dropped request is a child blocked on a reply forever, and a -/// hang is a worse diagnosis than a refusal. A notification, by definition, has nobody listening. -#[test] -fn an_unknown_request_is_answered_and_an_unknown_notification_is_not() { - let Some(ServerMessage::UnknownRequest { id, method }) = classify(&line( - r#"{"jsonrpc":"2.0","id":5,"method":"totally/unknown","params":{}}"#, - )) else { - panic!("an unrecognised request must be classified as one"); - }; - assert_eq!(method, "totally/unknown"); - let reply = method_not_found(&id, &method); - assert_eq!(reply["id"], 5); - assert_eq!(reply["error"]["code"], -32601); - - assert_eq!( - classify(&line( - r#"{"jsonrpc":"2.0","method":"totally/unknown","params":{}}"# - )), - Some(ServerMessage::UnknownNotification("totally/unknown".into())), - "an unrecognised notification is counted and dropped, never answered" - ); -} - -/// `ping` IS ANSWERED UNCONDITIONALLY, and it is the ONLY request that is. -/// -/// A ping carries no authority and its whole purpose is to let a peer tell a live process from a -/// wedged one; gating it would make busbar look dead to every child that probes. -#[test] -fn ping_is_answered_with_an_empty_result_under_no_grants() { - let id = serde_json::json!(9); - let reply = answer( - &id, - ServerRequestVerb::Ping, - &ServerRequestGrants::default(), - "fs", - ) - .expect("busbar answers a ping itself: it is keepalive, not an ask"); - assert_eq!(reply["id"], 9); - assert_eq!(reply["result"], serde_json::json!({})); - assert!( - reply.get("error").is_none(), - "a ping is never refused: {reply}" - ); - assert_eq!( - ServerRequestVerb::Ping.ask(), - None, - "a ping spends no authority, so it must name no grant" - ); -} - -/// THE THREE AUTHORITY ASKS ARE DENY-BY-DEFAULT, and the refusal NAMES THE GRANT TO SET. -/// -/// All three, not a sample. A gate proven on one kind is a gate whose other two arms have never been -/// executed, and the arm that is never executed is the arm that says yes. -#[test] -fn the_three_authority_asks_are_denied_by_default_and_name_their_grant() { - let none = ServerRequestGrants::default(); - for (verb, kind) in [ - (ServerRequestVerb::RootsList, "roots"), - (ServerRequestVerb::SamplingCreateMessage, "sampling"), - (ServerRequestVerb::ElicitationCreate, "elicitation"), - ] { - let ask = verb.ask().expect("an authority ask names a grant"); - assert_eq!(ask.key(), kind); - assert_eq!( - decide_ask(ask, &none), - AskOutcome::Ungranted, - "{kind} must be denied when the operator granted nothing" - ); - let reply = answer(&serde_json::json!(3), verb, &none, "fs") - .expect("an ungranted ask is refused on the child's input"); - let message = reply["error"]["message"].as_str().unwrap_or_default(); - assert!( - message.contains(&format!("tools.fs.grants.{kind}: true")), - "the refusal must name the exact key an operator sets: {message}" - ); - assert_eq!(reply["error"]["code"], -32001, "{reply}"); - assert_eq!(reply["error"]["data"]["reason"], "ask_ungranted", "{reply}"); - assert!( - reply.get("result").is_none(), - "a refused ask carries no result: {reply}" - ); - } -} - -/// LAW 11: A GRANTED ASK IS THE CALLER'S, AND BUSBAR WRITES NOTHING IN ITS PLACE. The grant is a -/// relay permission: held, the ask is relayed to busbar's caller and the child gets no reply from -/// busbar at all (the caller's answer is written under the child's id); not held, the child is -/// refused (`ask_ungranted`, the operator's policy). RED against the deleted "no satisfier" reply, -/// which answered a granted ask on the caller's behalf. -#[test] -fn a_granted_ask_is_relayed_and_never_answered_by_busbar() { - let all = ServerRequestGrants { - sampling: true, - elicitation: true, - roots: true, - }; - for (verb, ask) in [ - ( - ServerRequestVerb::SamplingCreateMessage, - ServerAsk::Sampling, - ), - (ServerRequestVerb::ElicitationCreate, ServerAsk::Elicitation), - (ServerRequestVerb::RootsList, ServerAsk::Roots), - ] { - assert_eq!(decide_ask(ask, &all), AskOutcome::Relay, "{}", ask.key()); - assert_eq!( - decide_ask(ask, &ServerRequestGrants::default()), - AskOutcome::Ungranted - ); - assert_eq!( - answer(&serde_json::json!(1), verb, &all, "fs"), - None, - "busbar answers a granted `{}` ask with nothing: the caller does", - ask.key() - ); - assert!( - answer( - &serde_json::json!(1), - verb, - &ServerRequestGrants::default(), - "fs" - ) - .is_some(), - "an ungranted ask is still refused" - ); - } -} - -/// ONE GRANT DOES NOT OPEN THE OTHER TWO. The three are independent authorities and a table that -/// leaked between them would let `roots: true` buy an LLM completion. -#[test] -fn each_grant_opens_only_its_own_ask() { - let roots_only = ServerRequestGrants { - roots: true, - ..ServerRequestGrants::default() - }; - assert_eq!(decide_ask(ServerAsk::Roots, &roots_only), AskOutcome::Relay); - assert_eq!( - decide_ask(ServerAsk::Sampling, &roots_only), - AskOutcome::Ungranted - ); - assert_eq!( - decide_ask(ServerAsk::Elicitation, &roots_only), - AskOutcome::Ungranted - ); -} - -/// THE FOUR "SOMETHING CHANGED" NOTIFICATIONS CAN ONLY BRING A REFRESH FORWARD, and no others can. -/// -/// The set matters in both directions. A fifth notification mapped to `BringRefreshForward` would -/// give a peer another lever on busbar's outbound fetch rate; one of these four mapped to `Log` -/// would silently stop rug-pull detection reacting to the signal that exists to trigger it. -#[test] -fn only_the_change_notifications_trigger_a_refresh() { - let expected = [ - (ServerNotification::ToolsListChanged, true), - (ServerNotification::PromptsListChanged, true), - (ServerNotification::ResourcesListChanged, true), - (ServerNotification::ResourcesUpdated, true), - (ServerNotification::Cancelled, false), - (ServerNotification::Message, false), - (ServerNotification::SubscriptionsAcknowledged, false), - (ServerNotification::Tasks, false), - (ServerNotification::Progress, false), - ]; - assert_eq!( - expected.len(), - 9, - "all nine server notifications must be covered; a shrinking denominator is the failure mode" - ); - for (n, triggers) in expected { - assert_eq!( - matches!( - n.effect(), - NotificationEffect::BringRefreshForward | NotificationEffect::RelayResourceUpdate - ), - triggers, - "{n:?} maps to the wrong effect" - ); - } - // `ResourcesUpdated` is the ONE arm with a second, named effect — the recorded uri for the - // server-leg relay — and it must be exactly one: a second recording arm is a second reader of - // peer-chosen content. - for n in expected.map(|(n, _)| n) { - assert_eq!( - n.effect() == NotificationEffect::RelayResourceUpdate, - n == ServerNotification::ResourcesUpdated, - "{n:?}: only `resources/updated` records an announcement" - ); - } - assert_eq!( - ServerNotification::Progress.effect(), - NotificationEffect::RelayProgress, - "progress is the one notification a caller is entitled to see relayed" - ); -} diff --git a/crates/busbar-plane-mcp/src/client/tests/verb_tests.rs b/crates/busbar-plane-mcp/src/client/tests/verb_tests.rs deleted file mode 100644 index 6c938b04a9..0000000000 --- a/crates/busbar-plane-mcp/src/client/tests/verb_tests.rs +++ /dev/null @@ -1,210 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE CLOSED SET OF ISSUED METHODS, checked against the GENERATED matrix rather than against a -//! list in a comment. -//! -//! The failure this file exists to catch is the one `tests/method_coverage.rs` was written for, one -//! level down: a column of the matrix whose contents are a property of its call sites drifts from -//! the specification silently, because an absent verb and a considered-and-inapplicable verb look -//! identical in source. So [`UpstreamVerb::all`] is compared against `qa/method-inventory.json` — -//! which is generated from rmcp's own model — in BOTH directions. - -use super::super::verb::UpstreamVerb; - -/// Every `mcp|stdio|client|client|` cell the generated inventory owes an implementation. -/// -/// Read from the file rather than restated, for the reason the coverage gate itself gives: a list -/// written from knowledge of the specification is exactly how a list ends at J with nobody noticing. -fn inventory_methods() -> std::collections::BTreeSet { - // Compiled in rather than read at run time: the plane performs no input or output, its tests - // included (tests/purity.rs). - let raw = include_str!("../../../../../qa/method-inventory.json"); - let doc: serde_json::Value = serde_json::from_str(raw).expect("method-inventory.json parses"); - doc["cells"] - .as_array() - .expect("a `cells` array") - .iter() - .filter(|c| { - c["protocol"] == "mcp" - && c["transport"] == "stdio" - && c["role"] == "client" - && c["originator"] == "client" - && c["na_reason"].is_null() - }) - .map(|c| c["method"].as_str().expect("a method").to_string()) - .collect() -} - -/// THE COLUMN IS COMPLETE, AND IT IS NOT WIDER THAN THE SPECIFICATION. -/// -/// Both directions, because each catches a different mistake. A method in the inventory with no -/// variant is a cell busbar cannot serve and would be claiming. A variant with no inventory row is -/// busbar sending an upstream a method the specification does not define, which is not coverage — -/// it is a made-up verb that an upstream will answer `-32601` to. -#[test] -fn the_issued_set_is_exactly_the_inventory_column() { - let owed = inventory_methods(); - assert!( - owed.len() >= 20, - "only {} issuable stdio methods in the inventory. This test refuses to pass vacuously: a \ - filter that matched nothing would report a complete column of nothing.", - owed.len() - ); - let built: std::collections::BTreeSet = UpstreamVerb::all() - .iter() - .map(|v| v.method().to_string()) - .collect(); - let missing: Vec<&String> = owed.difference(&built).collect(); - let invented: Vec<&String> = built.difference(&owed).collect(); - assert!( - missing.is_empty(), - "the inventory owes these methods on the stdio client leg and `UpstreamVerb` has no variant \ - for them: {missing:#?}" - ); - assert!( - invented.is_empty(), - "`UpstreamVerb` sends methods the generated matrix does not define: {invented:#?}\n\ - Do not edit qa/method-inventory.json — it is generated from the SDKs and will come back." - ); -} - -/// `all()` IS ONE INSTANCE OF EVERY VARIANT, not most of them. -/// -/// Without this, a variant added to the enum and forgotten in `all()` would narrow every test above -/// and below silently — the exact shape of the four batteries this release found reporting green -/// over a shrinking denominator. -#[test] -fn every_variant_appears_in_all_exactly_once() { - let all = UpstreamVerb::all(); - let mut names: Vec<&str> = all.iter().map(UpstreamVerb::method).collect(); - let before = names.len(); - names.sort_unstable(); - names.dedup(); - assert_eq!( - before, - names.len(), - "`UpstreamVerb::all()` lists a method twice, which would double-count the column" - ); -} - -/// A NOTIFICATION HAS NO `id` AND A REQUEST DOES, and it is the VARIANT that decides. -/// -/// This is the property `McpWire::notify` exists for. A notification carrying an `id` is a request, -/// and a peer would be right to answer it — which on a stdio child puts an unexpected line on the -/// stream and desynchronises every later call. -#[test] -fn notifications_carry_no_id_and_requests_do() { - let mut notifications = 0; - for verb in UpstreamVerb::all() { - let request = verb.build("https://u.example.com/mcp", 42, None); - let body: serde_json::Value = - serde_json::from_slice(&request.body).expect("the body is JSON"); - assert_eq!( - body["jsonrpc"], - "2.0", - "{} must carry the JSON-RPC version", - verb.method() - ); - assert_eq!( - body["method"], - verb.method(), - "the body's method must be the verb's own" - ); - if verb.is_notification() { - notifications += 1; - assert!( - body.get("id").is_none(), - "{} is a notification and must carry NO `id` member: {body}", - verb.method() - ); - } else { - assert_eq!( - body["id"], - 42, - "{} is a request and must carry the id it was built with: {body}", - verb.method() - ); - } - } - assert_eq!( - notifications, 5, - "the five client-originated notifications of this revision must be classified as \ - notifications; a request misclassified as one would never be answered, and a notification \ - misclassified as a request hangs the leg" - ); -} - -/// EVERY VERB CARRIES `params._meta`, which this revision REQUIRES on every request. -/// -/// Asserted over the whole set rather than on a sample, because the failure mode is exactly one verb -/// forgetting it — and busbar's own ingress answers `-32602` to a request whose `_meta` is missing, -/// so a verb that omitted it would be one busbar could not itself accept. -#[test] -fn every_verb_carries_the_required_meta() { - for verb in UpstreamVerb::all() { - let request = verb.build("https://u.example.com/mcp", 1, None); - let body: serde_json::Value = serde_json::from_slice(&request.body).expect("json"); - let meta = &body["params"]["_meta"]; - assert!( - meta.is_object(), - "{} must carry `params._meta`: {body}", - verb.method() - ); - assert_eq!( - meta["io.modelcontextprotocol/protocolVersion"], - super::super::verb::CLIENT_PROTOCOL_VERSION, - "{} must state the protocol revision in `_meta`", - verb.method() - ); - } -} - -/// THE MIRRORED `Mcp-Method` HEADER IS THE BODY'S OWN METHOD, on every verb. -/// -/// A mirrored header whose value is computed twice is a mirrored header that can differ, which is -/// the request-smuggling primitive the mirroring exists to close. -#[test] -fn the_mirrored_method_header_matches_the_body() { - for verb in UpstreamVerb::all() { - let request = verb.build("https://u.example.com/mcp", 1, None); - let sent: Vec<&str> = request - .headers - .iter() - .filter(|(n, _)| n == "mcp-method") - .map(|(_, v)| v.as_str()) - .collect(); - assert_eq!( - sent, - vec![verb.method()], - "{} must mirror its method in exactly one `Mcp-Method` header", - verb.method() - ); - } -} - -/// THE CREDENTIAL IS SENT WHEN THERE IS ONE AND NEVER OTHERWISE. -/// -/// The negative half is the one that matters: a verb that attached a bearer unconditionally would -/// send an empty `Authorization: Bearer ` to a public upstream, which is a malformed credential -/// header rather than an absent one. -#[test] -fn the_credential_is_present_only_when_one_was_planned() { - for verb in UpstreamVerb::all() { - let bare = verb.build("https://u.example.com/mcp", 1, None); - assert!( - !bare.headers.iter().any(|(n, _)| n == "authorization"), - "{} must send no `Authorization` when no credential was planned", - verb.method() - ); - let credentialled = verb.build("https://u.example.com/mcp", 1, Some("tok")); - assert!( - credentialled - .headers - .iter() - .any(|(n, v)| n == "authorization" && v == "Bearer tok"), - "{} must send the planned credential", - verb.method() - ); - } -} diff --git a/crates/busbar-plane-mcp/src/client/tests/wire_tests.rs b/crates/busbar-plane-mcp/src/client/tests/wire_tests.rs deleted file mode 100644 index 5a5388995e..0000000000 --- a/crates/busbar-plane-mcp/src/client/tests/wire_tests.rs +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! The `2026-07-28` outbound wire: the mirrored headers, `params._meta`, and the un-namespacing. -//! -//! The strongest assertion here is the SYMMETRY one: a request this module builds is fed to -//! the engine's `mcp::envelope` reader, and must be accepted. busbar refusing a request busbar sent -//! would be a silent, mutual misunderstanding that no single-direction test can see. - -use crate::client::jsonrpc::{ - parse_response, tools_call, tools_list, RpcOutcome, META_CLIENT_CAPABILITIES, - META_PROTOCOL_VERSION, -}; - -/// A routing key for a valid test server and tool. -fn tkey(server: &str, tool: &str) -> crate::identity::ToolKey { - let server = crate::identity::ServerId::new(server).expect("test server id must be valid"); - crate::identity::ToolKey::new(server, tool).expect("test tool key must be valid") -} - -fn body_of(req: &crate::client::jsonrpc::OutboundRequest) -> serde_json::Value { - serde_json::from_slice(&req.body).expect("the body we build must be JSON") -} - -fn header<'a>(req: &'a crate::client::jsonrpc::OutboundRequest, name: &str) -> Option<&'a str> { - req.headers - .iter() - .find(|(n, _)| n == name) - .map(|(_, v)| v.as_str()) -} - -#[test] -fn the_outbound_name_is_the_un_namespaced_tool() { - let req = tools_call( - "https://u.example/mcp", - &tkey("filesystem", "read_file"), - &serde_json::json!({"path": "/tmp/x"}), - 1, - None, - Default::default(), - None, - ); - let body = body_of(&req); - // The upstream has never heard of `filesystem_read_file` and would answer -32602 to it. - assert_eq!(body["params"]["name"], "read_file"); - assert_eq!(header(&req, "mcp-name"), Some("read_file")); - // ...and the namespaced form appears nowhere on the wire. - assert!(!String::from_utf8_lossy(&req.wire_bytes()).contains("filesystem_read_file")); -} - -#[test] -fn meta_lives_at_params_meta_and_not_at_the_top_level() { - // This is the exact defect the third-party conformance suite caught on the ingress side, where - // every one of our own tests had agreed with the reader that read the top level. Building the - // client against the same wrong shape would have made both halves agree and both halves wrong. - let req = tools_list("https://u.example/mcp", 1, None); - let body = body_of(&req); - assert!(body.get("_meta").is_none(), "no top-level `_meta`"); - assert_eq!( - body["params"]["_meta"][META_PROTOCOL_VERSION], - crate::codec::PROTOCOL_VERSION - ); - // Capabilities are declared EMPTY: busbar will refuse sampling/elicitation/roots unless the - // operator granted them, and declaring a capability we then refuse invites a call sequence - // built around it. - assert_eq!( - body["params"]["_meta"][META_CLIENT_CAPABILITIES], - serde_json::json!({}) - ); -} - -#[test] -fn the_mirrored_headers_agree_with_the_body_they_mirror() { - let req = tools_call( - "https://u.example/mcp", - &tkey("fs", "read"), - &serde_json::json!({}), - 9, - Some("tok"), - Default::default(), - None, - ); - let body = body_of(&req); - assert_eq!(header(&req, "mcp-method"), body["method"].as_str()); - assert_eq!(header(&req, "mcp-name"), body["params"]["name"].as_str()); - assert_eq!( - header(&req, "mcp-protocol-version"), - body["params"]["_meta"][META_PROTOCOL_VERSION].as_str() - ); -} - -#[test] -fn tools_list_carries_no_mcp_name_because_it_has_no_target() { - let req = tools_list("https://u.example/mcp", 1, None); - assert_eq!(header(&req, "mcp-method"), Some("tools/list")); - assert!(header(&req, "mcp-name").is_none()); -} - -#[test] -fn there_is_no_session_header_on_any_request() { - // `2026-07-28` deleted sessions. Sending `Mcp-Session-Id` would be busbar claiming a protocol - // feature that does not exist. - for req in [ - tools_list("https://u.example/mcp", 1, Some("t")), - tools_call( - "https://u.example/mcp", - &tkey("fs", "read"), - &serde_json::json!({}), - 2, - Some("t"), - Default::default(), - None, - ), - ] { - assert!(header(&req, "mcp-session-id").is_none()); - assert!(header(&req, "last-event-id").is_none()); - } -} - -#[test] -fn the_authorization_header_is_present_exactly_when_a_credential_was_planned() { - let with = tools_list("https://u.example/mcp", 1, Some("abc")); - assert_eq!(header(&with, "authorization"), Some("Bearer abc")); - let without = tools_list("https://u.example/mcp", 1, None); - assert!(header(&without, "authorization").is_none()); -} - -// ── THE SOURCE SCAN THAT USED TO LIVE HERE ────────────────────────────────────────────────────── -// -// `the_client_and_the_ingress_agree_on_every_wire_literal` `include_str!`-ed `../../ingress.rs` and -// checked that the five shared wire words — the two `_meta` keys and the three header names — -// appeared on BOTH sides. It was comparing two copies, and a disagreement between them was silent -// and mutual: busbar would have refused a request busbar sent, and no single-direction test could -// see it, because each side was internally consistent with its own copy. -// -// There is now ONE copy. `mcp::envelope` owns all five constants and `mcp::client::jsonrpc` `use`s -// them, so the two directions cannot disagree — the module header there already claimed this was -// true, and until 2026-08-12 it was true as a claim and false as code. Nothing is left to compare, -// so the comparison is gone rather than ported. -// -// What replaces it is not a symmetry check but a SINGULARITY check: the `structure-lint` gate's -// declaration census requires each of the five literals to occur EXACTLY ONCE in production code. -// A second spelling anywhere in the tree is RED — that is the drift this test existed to catch, now -// caught without naming a file — and ZERO occurrences is RED too, reported as SUBJECT-MISSING -// rather than as a pass. Both arms were proven: re-typing `"mcp-method"` in `client/jsonrpc.rs` -// reported WIRE-WORD-RESPELT with both sites, and renaming `"mcp-name"` to `"mcp-target-name"` -// reported SUBJECT-MISSING. - -#[test] -fn a_jsonrpc_error_is_not_reported_as_a_result() { - let out = parse_response( - br#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"no such method"}}"#, - 1, - ); - assert_eq!( - out, - RpcOutcome::Error { - code: -32601, - message: "no such method".into() - } - ); -} - -/// An error object whose `code` is missing or not an integer is not a JSON-RPC error. It reads as -/// malformed, and no caller is ever handed a `0` the upstream never sent. -#[test] -fn an_error_without_an_integer_code_is_malformed_not_code_zero() { - for body in [ - br#"{"jsonrpc":"2.0","id":1,"error":{"message":"no code"}}"#.as_slice(), - br#"{"jsonrpc":"2.0","id":1,"error":{"code":"-32601","message":"string code"}}"#.as_slice(), - br#"{"jsonrpc":"2.0","id":1,"error":{"code":1.5,"message":"fractional code"}}"#.as_slice(), - ] { - let out = parse_response(body, 1); - assert!( - matches!(out, RpcOutcome::Malformed(_)), - "{} read as {out:?}", - String::from_utf8_lossy(body) - ); - } -} - -#[test] -fn a_result_is_returned_verbatim() { - let out = parse_response(br#"{"jsonrpc":"2.0","id":1,"result":{"content":[]}}"#, 1); - assert_eq!(out, RpcOutcome::Result(serde_json::json!({"content": []}))); -} - -#[test] -fn every_malformed_shape_is_refused_rather_than_guessed_at() { - let bad: [&[u8]; 5] = [ - b"not json", - br#"[{"jsonrpc":"2.0","id":1,"result":{}}]"#, - br#"{"jsonrpc":"1.0","id":1,"result":{}}"#, - br#"{"jsonrpc":"2.0","id":1}"#, - b"", - ]; - assert_eq!(bad.len(), 5, "the malformed set must not shrink"); - for body in bad { - assert!( - matches!(parse_response(body, 1), RpcOutcome::Malformed(_)), - "{:?} must be malformed", - String::from_utf8_lossy(body) - ); - } -} diff --git a/crates/busbar-plane-mcp/src/client/verb.rs b/crates/busbar-plane-mcp/src/client/verb.rs deleted file mode 100644 index 5fa4c1d120..0000000000 --- a/crates/busbar-plane-mcp/src/client/verb.rs +++ /dev/null @@ -1,477 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE CLOSED SET OF METHODS BUSBAR ISSUES TO AN UPSTREAM MCP SERVER. -//! -//! ## Why a closed enum and not a `&str` method name -//! -//! `qa/method-inventory.json` is a MATRIX and this module is one of its columns: `mcp × -//! × busbar-as-client × client-originated`. A builder that took the method as a string would make -//! the column's contents a property of its call sites, so a method could be added by one caller and -//! be invisible to every gate, every audit word and every test that enumerates the surface. The enum -//! is what makes an enumeration of the whole column possible, and -//! `tests/verb_tests.rs::the_issued_set_is_exactly_the_inventory_column` compares that enumeration -//! against the generated inventory IN BOTH DIRECTIONS — so a verb this file forgets is a RED TEST -//! rather than an absent row, and a verb it invents is a red test rather than a `-32601` in -//! production. -//! -//! ## busbar is a PROXY here, not an originator -//! -//! The owner's ruling on the coverage matrix, 2026-08-12, is the reason every one of these exists -//! rather than the obvious-looking subset: -//! -//! > *a gateway probably would not use this* is NOT a reason. busbar sits between a real client and -//! > a real server, so its client-side verb is not busbar originating anything — it is busbar -//! > PROXYING the real client's. -//! -//! So `resources/subscribe` is here because a caller subscribed; `notifications/cancelled` is here -//! because a caller cancelled; `initialize` is here because a CHILD PROCESS demands one. Which -//! brings us to the asymmetry with the HTTP leg, which is the one design decision in this file: -//! -//! ## WHY `initialize`, `ping` AND `logging/setLevel` ARE BUILT HERE AND IMPOSSIBLE ON THE HTTP LEG -//! -//! `qa/WAIVERS.md` records `mcp|streamable-http|client|client|initialize` (and -//! `notifications/initialized`, `ping`, `logging/setLevel`, `resources/subscribe`, -//! `resources/unsubscribe`) as *removed by SEP-2575* — recorded impossibilities, not waivers, -//! since the waiver list went to zero. That record is correct for HTTP and WRONG for stdio, and -//! the difference is not a preference: -//! -//! - Over streamable HTTP, busbar's peer is a server that speaks the same revision busbar's own -//! front door serves. That revision deleted the handshake, so there is nothing to send. -//! - Over stdio, busbar's peer is a LOCAL CHILD PROCESS the operator named in `command:`. It is -//! whatever binary the operator installed — overwhelmingly an SDK server speaking a revision in -//! which `initialize` is a MUST and a server may refuse every other request until it arrives. -//! busbar does not get to choose the child's revision; the operator chose it by installing it. -//! -//! A leg that could not send a handshake could not talk to the stdio ecosystem at all. So the -//! handshake is BUILT, and it is sent as the child's first message by -//! the engine's `mcp::client::stdio` — see `StdioChild::handshake`. -//! -//! ## Params are DATA, never argv -//! -//! Every variant's payload is serialised into the JSON-RPC `params` object and travels on the -//! child's stdin. Nothing here can reach `StdioCommand::args`, which is config-only. That is the -//! fourth spawn decision in the engine's `mcp::client::stdio`'s header, expressed here as the absence of any path from -//! a verb to an argument vector. - -use super::jsonrpc::{envelope, OutboundRequest}; -use crate::codec::{META_CLIENT_CAPABILITIES, META_PROTOCOL_VERSION, PROTOCOL_VERSION}; - -/// THE PROTOCOL REVISION BUSBAR OFFERS A CHILD IN ITS HANDSHAKE. -/// -/// The same string busbar's own front door serves, and it is offered rather than imposed: a child -/// that answers `initialize` with a different `protocolVersion` has told busbar what it speaks, and -/// that answer is recorded rather than argued with. busbar is a gateway, not a conformance test. -pub const CLIENT_PROTOCOL_VERSION: &str = PROTOCOL_VERSION; - -/// What busbar calls itself to a child. A constant, not a config key: an operator who could rename -/// busbar in a handshake could make one deployment impersonate another in the child's own logs. -const CLIENT_NAME: &str = "busbar"; - -/// ONE METHOD BUSBAR SENDS TO AN UPSTREAM, with its parameters. -/// -/// Every variant carries the data its `params` needs and nothing else. There is deliberately no -/// `Other(String)` arm: an arm that accepted an arbitrary method name would reopen exactly the -/// hole this enum closes, and `-32601` from an upstream is a better answer than a method busbar -/// invented. -/// -/// `PartialEq` and NOT `Eq`: `notifications/progress` carries an `f64`, which is the wire type the -/// specification gives it. Quantising progress to an integer to buy `Eq` would be lying about the -/// wire to satisfy a derive. -/// -/// ## WHAT HAS A PRODUCTION CALLER TODAY, and what does not -/// -/// `ToolsList` is built by the engine's `mcp::connect::refresh` on every scheduled and operator-driven -/// re-pull, and `ToolsCall`'s wire form is `super::jsonrpc::tools_call` on the dispatch path. The -/// other twenty-one variants are reached by the engine's `mcp::client::issue::issue` — which is itself reached today -/// only from the batteries in the engine's `mcp::tests/stdio_client_leg_tests.rs`, because busbar's own -/// FRONT DOOR does not yet expose a `prompts/list` that proxies through to an upstream's. The verb -/// exists, is governed, is audited and is proven against a real child process; what is missing is -/// the inbound method that would call it, and that is the server plane's surface rather than this -/// module's. -/// -/// The allow is on the ENUM and states that, rather than being a blanket over the module: anything -/// else in this file losing its caller still breaks the build. Deleting the variants instead would -/// be deleting a proven, security-reviewed leg so that a linter is quiet, which is the trade the -/// stdio transport was already deleted over once. -#[allow(dead_code)] -#[derive(Clone, Debug, PartialEq)] -pub enum UpstreamVerb { - /// The handshake. See the module header for why this exists here and is waived on HTTP. - Initialize { - /// busbar's release, sent as `clientInfo.version`. Handed in by the engine: a plane reads - /// no build environment of its own. - client_version: &'static str, - }, - /// The handshake's acknowledgement. A NOTIFICATION: it has no reply, and a client that waited - /// for one would hang against every conformant server. - NotificationsInitialized, - /// Liveness. Sent by busbar's own supervisor as a readiness probe, and proxied when a caller - /// pings a specific upstream. - Ping, - /// The merged capability advertisement this revision added. - ServerDiscover, - /// Set the upstream's log verbosity. - LoggingSetLevel { - /// The RFC 5424 level name. - level: String, - }, - /// The upstream's tool list. The refresh path's request — see the engine's `mcp::connect`. - ToolsList, - /// Invoke one tool. `name` is the UN-namespaced tool: the upstream has never heard of busbar's - /// `{server}_{tool}` namespacing and would answer `-32602` to it. - ToolsCall { - /// The un-namespaced tool name. - name: String, - /// The tool's arguments object. - arguments: serde_json::Value, - }, - /// The upstream's prompt list. - PromptsList, - /// One prompt, rendered with arguments. - PromptsGet { - /// The prompt name. - name: String, - /// The prompt's arguments object. - arguments: serde_json::Value, - }, - /// The upstream's resource list. - ResourcesList, - /// The upstream's resource templates. - ResourcesTemplatesList, - /// Read one resource. - ResourcesRead { - /// The resource URI. - uri: String, - }, - /// Subscribe to one resource's updates. - ResourcesSubscribe { - /// The resource URI. - uri: String, - }, - /// Drop one resource subscription. - ResourcesUnsubscribe { - /// The resource URI. - uri: String, - }, - /// SEP-2575's replacement for the removed GET stream. A METHOD in this revision. - SubscriptionsListen { - /// The notification categories asked for. - notifications: serde_json::Value, - }, - /// Argument completion. - CompletionComplete { - /// The prompt or resource template being completed. - reference: serde_json::Value, - /// The argument name and partial value. - argument: serde_json::Value, - }, - /// SEP-2663 tasks. `taskId` is the wire spelling — see the engine's `mcp::tasks`. - TasksGet { - /// The task id. - task_id: String, - }, - /// Hand a task the input it asked for. - TasksUpdate { - /// The task id. - task_id: String, - /// The answers, keyed as the task asked. - input_responses: serde_json::Value, - }, - /// Cancel one task. - TasksCancel { - /// The task id. - task_id: String, - }, - /// A NOTIFICATION: the caller withdrew a request busbar had already forwarded. - NotificationsCancelled { - /// The id of the withdrawn request. - request_id: serde_json::Value, - /// Why it was withdrawn. - reason: String, - }, - /// A NOTIFICATION: progress on a long-running call, relayed upstream. - NotificationsProgress { - /// The token the upstream call was issued with. - progress_token: serde_json::Value, - /// Progress so far. - progress: f64, - /// The total, when known. - total: Option, - }, - /// A NOTIFICATION: busbar's exposed root set changed. - NotificationsRootsListChanged, - /// A NOTIFICATION: the answer to an `elicitation/create` the upstream asked for. - NotificationsElicitationResponse { - /// The id of the upstream's `elicitation/create`. - request_id: serde_json::Value, - /// The user's answer. - content: serde_json::Value, - }, -} - -impl UpstreamVerb { - /// THE WIRE METHOD NAME. Exhaustive, with no wildcard arm, so a variant added without a name is - /// a compile error rather than a method that silently sends the wrong word. - pub fn method(&self) -> &'static str { - match self { - UpstreamVerb::Initialize { .. } => "initialize", - UpstreamVerb::NotificationsInitialized => "notifications/initialized", - UpstreamVerb::Ping => "ping", - UpstreamVerb::ServerDiscover => "server/discover", - UpstreamVerb::LoggingSetLevel { .. } => "logging/setLevel", - UpstreamVerb::ToolsList => "tools/list", - UpstreamVerb::ToolsCall { .. } => "tools/call", - UpstreamVerb::PromptsList => "prompts/list", - UpstreamVerb::PromptsGet { .. } => "prompts/get", - UpstreamVerb::ResourcesList => "resources/list", - UpstreamVerb::ResourcesTemplatesList => "resources/templates/list", - UpstreamVerb::ResourcesRead { .. } => "resources/read", - UpstreamVerb::ResourcesSubscribe { .. } => "resources/subscribe", - UpstreamVerb::ResourcesUnsubscribe { .. } => "resources/unsubscribe", - UpstreamVerb::SubscriptionsListen { .. } => "subscriptions/listen", - UpstreamVerb::CompletionComplete { .. } => "completion/complete", - UpstreamVerb::TasksGet { .. } => "tasks/get", - UpstreamVerb::TasksUpdate { .. } => "tasks/update", - UpstreamVerb::TasksCancel { .. } => "tasks/cancel", - UpstreamVerb::NotificationsCancelled { .. } => "notifications/cancelled", - UpstreamVerb::NotificationsProgress { .. } => "notifications/progress", - UpstreamVerb::NotificationsRootsListChanged => "notifications/roots/list_changed", - UpstreamVerb::NotificationsElicitationResponse { .. } => { - "notifications/elicitation/response" - } - } - } - - /// Whether this verb is a JSON-RPC NOTIFICATION — no `id`, and NO REPLY EVER. - /// - /// Decided on the VARIANT rather than by testing whether the method name starts with - /// `notifications/`, because that string test is a rule about spelling that would silently - /// reclassify any future method whose name happened to match. The distinction matters at the - /// transport: the engine's `mcp::client::wire::McpWire::notify` does not read, and a request sent down that path - /// would wait for an answer nobody would ever read — which on a stdio child means the answer - /// stays in the pipe and desynchronises every subsequent call. - pub fn is_notification(&self) -> bool { - matches!( - self, - UpstreamVerb::NotificationsInitialized - | UpstreamVerb::NotificationsCancelled { .. } - | UpstreamVerb::NotificationsProgress { .. } - | UpstreamVerb::NotificationsRootsListChanged - | UpstreamVerb::NotificationsElicitationResponse { .. } - ) - } - - /// The `params` object as the ARGUMENT GUARD sees it, before `_meta` is added. - /// - /// Exposed for exactly one caller, the engine's `mcp::client::issue::issue`, which walks it for URL and host - /// fields. `_meta` is deliberately absent: it is BUSBAR's block, not the caller's, so judging it - /// would be judging busbar's own protocol declaration as though a caller had chosen it. - pub fn params_for_guard(&self) -> serde_json::Value { - self.params() - } - - /// The `params` object, WITHOUT `_meta` — which [`Self::build`] adds, once, for every verb. - /// - /// Split that way so no variant can forget the revision's required `_meta`: a per-variant - /// `params` that included it would be twenty-three chances to omit one key. - fn params(&self) -> serde_json::Value { - match self { - UpstreamVerb::Initialize { client_version } => serde_json::json!({ - "protocolVersion": CLIENT_PROTOCOL_VERSION, - // THE EMPTY CAPABILITY SET, and it is the honest one. Sampling, elicitation and - // roots are deny-by-default per server (`super::jsonrpc::ServerRequestGrants`), so - // declaring a capability busbar then refuses to honour would invite a child to build - // a call sequence around authority it will not be given. - "capabilities": {}, - "clientInfo": { "name": CLIENT_NAME, "version": client_version }, - }), - UpstreamVerb::NotificationsInitialized - | UpstreamVerb::Ping - | UpstreamVerb::ServerDiscover - | UpstreamVerb::ToolsList - | UpstreamVerb::PromptsList - | UpstreamVerb::ResourcesList - | UpstreamVerb::ResourcesTemplatesList - | UpstreamVerb::NotificationsRootsListChanged => serde_json::json!({}), - UpstreamVerb::LoggingSetLevel { level } => serde_json::json!({ "level": level }), - UpstreamVerb::ToolsCall { name, arguments } => { - serde_json::json!({ "name": name, "arguments": arguments }) - } - UpstreamVerb::PromptsGet { name, arguments } => { - serde_json::json!({ "name": name, "arguments": arguments }) - } - UpstreamVerb::ResourcesRead { uri } - | UpstreamVerb::ResourcesSubscribe { uri } - | UpstreamVerb::ResourcesUnsubscribe { uri } => serde_json::json!({ "uri": uri }), - UpstreamVerb::SubscriptionsListen { notifications } => { - serde_json::json!({ "notifications": notifications }) - } - UpstreamVerb::CompletionComplete { - reference, - argument, - } => serde_json::json!({ "ref": reference, "argument": argument }), - UpstreamVerb::TasksGet { task_id } | UpstreamVerb::TasksCancel { task_id } => { - serde_json::json!({ "taskId": task_id }) - } - UpstreamVerb::TasksUpdate { - task_id, - input_responses, - } => serde_json::json!({ "taskId": task_id, "inputResponses": input_responses }), - UpstreamVerb::NotificationsCancelled { request_id, reason } => { - serde_json::json!({ "requestId": request_id, "reason": reason }) - } - UpstreamVerb::NotificationsProgress { - progress_token, - progress, - total, - } => serde_json::json!({ - "progressToken": progress_token, - "progress": progress, - "total": total, - }), - UpstreamVerb::NotificationsElicitationResponse { - request_id, - content, - } => serde_json::json!({ "requestId": request_id, "content": content }), - } - } - - /// The `Mcp-Name` header's value for this verb, when the revision requires one. - /// - /// `Some` only where the request NAMES a target the header can mirror. A header that mirrored a - /// value the body does not carry would be a header busbar's own ingress answers `-32020` to. - /// WHICH member of this verb's `params` the `Mcp-Name` header mirrors, and its value. - /// - /// The RULE is `crate::codec::name_source_of`'s and is not restated here. This function - /// once carried its own copy and the two disagreed about the three tasks methods, so a - /// `tasks/get` issued over streamable HTTP went out with no `Mcp-Name` — the exact header - /// busbar's own ingress answers `-32020` to. Reading the ingress's table means the requests - /// busbar SENDS satisfy the MUSTs busbar ENFORCES, by construction rather than by two authors - /// agreeing. - /// - /// It returns an owned `String` because the value is read back out of the serialised `params` - /// rather than off the variant: the member the header mirrors is decided by the wire method, so - /// looking it up by name is what keeps the two in step. A per-variant `match` would be the - /// second copy again, wearing a different shape. - fn target(&self) -> Option { - let source = crate::codec::name_source_of(self.method())?; - self.params() - .get(source) - .and_then(|v| v.as_str()) - .map(str::to_string) - } - - /// BUILD the request. One envelope function, shared with `tools/call`, so the mirrored headers - /// this revision REQUIRES cannot be got right on one verb and wrong on another. - /// - /// `request_id` is IGNORED for a notification, which carries no `id` member at all — a - /// notification with an `id` is a request, and a peer would be right to answer it. - pub fn build( - &self, - url: &str, - request_id: u64, - authorization: Option<&str>, - ) -> OutboundRequest { - let mut params = self.params(); - if let Some(obj) = params.as_object_mut() { - obj.insert( - "_meta".to_string(), - serde_json::json!({ - META_PROTOCOL_VERSION: CLIENT_PROTOCOL_VERSION, - META_CLIENT_CAPABILITIES: {}, - }), - ); - } - let method = self.method(); - let body = if self.is_notification() { - serde_json::json!({ "jsonrpc": "2.0", "method": method, "params": params }) - } else { - serde_json::json!({ - "jsonrpc": "2.0", - "id": request_id, - "method": method, - "params": params, - }) - }; - envelope(url, method, self.target().as_deref(), body, authorization) - } -} - -impl UpstreamVerb { - /// ONE INSTANCE OF EVERY VARIANT, for the tests that enumerate the surface. - /// - /// A `const` array rather than a derive, because the point is that a NEW VARIANT must be added - /// here by hand — `every_issued_method_is_in_the_inventory` counts these against the generated - /// matrix, so a variant missing from this list fails that test instead of quietly narrowing - /// what the suite covers. - pub fn all() -> Vec { - vec![ - UpstreamVerb::Initialize { - client_version: "0.0.0", - }, - UpstreamVerb::NotificationsInitialized, - UpstreamVerb::Ping, - UpstreamVerb::ServerDiscover, - UpstreamVerb::LoggingSetLevel { - level: "info".to_string(), - }, - UpstreamVerb::ToolsList, - UpstreamVerb::ToolsCall { - name: "read".to_string(), - arguments: serde_json::json!({}), - }, - UpstreamVerb::PromptsList, - UpstreamVerb::PromptsGet { - name: "greet".to_string(), - arguments: serde_json::json!({}), - }, - UpstreamVerb::ResourcesList, - UpstreamVerb::ResourcesTemplatesList, - UpstreamVerb::ResourcesRead { - uri: "file:///a".to_string(), - }, - UpstreamVerb::ResourcesSubscribe { - uri: "file:///a".to_string(), - }, - UpstreamVerb::ResourcesUnsubscribe { - uri: "file:///a".to_string(), - }, - UpstreamVerb::SubscriptionsListen { - notifications: serde_json::json!({ "toolsListChanged": true }), - }, - UpstreamVerb::CompletionComplete { - reference: serde_json::json!({ "type": "ref/prompt", "name": "greet" }), - argument: serde_json::json!({ "name": "who", "value": "b" }), - }, - UpstreamVerb::TasksGet { - task_id: "t1".to_string(), - }, - UpstreamVerb::TasksUpdate { - task_id: "t1".to_string(), - input_responses: serde_json::json!([]), - }, - UpstreamVerb::TasksCancel { - task_id: "t1".to_string(), - }, - UpstreamVerb::NotificationsCancelled { - request_id: serde_json::json!(7), - reason: "the caller withdrew".to_string(), - }, - UpstreamVerb::NotificationsProgress { - progress_token: serde_json::json!("busbar-7"), - progress: 0.5, - total: Some(1.0), - }, - UpstreamVerb::NotificationsRootsListChanged, - UpstreamVerb::NotificationsElicitationResponse { - request_id: serde_json::json!(9), - content: serde_json::json!({ "answer": "yes" }), - }, - ] - } -} - -#[cfg(test)] -#[path = "tests/verb_tests.rs"] -mod verb_tests; diff --git a/crates/busbar-plane-mcp/src/codec.rs b/crates/busbar-plane-mcp/src/codec.rs deleted file mode 100644 index b124d1b2f7..0000000000 --- a/crates/busbar-plane-mcp/src/codec.rs +++ /dev/null @@ -1,385 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE MCP WIRE VOCABULARY — the dialect this plane speaks, written down once. -//! -//! Every name here is a fact about the Model Context Protocol's wire and about nothing else: the -//! mount path, the revision string, the five method names, the two `_meta` keys, the JSON-RPC error -//! code table and the notification pair. Definition 16–20 puts a protocol's wire dialects in the -//! protocol's own plane crate, so this is where they live, and the kernel-side declaration that -//! registers MCP with the engine reads them FROM here rather than restating them. -//! -//! That direction is the whole point of the move. Before it, the codec crate held the literals and -//! the plane held a mirror pinned to them by a test; the mirror is gone and the pin is unnecessary, -//! because there is now one definition. A constant that exists once cannot drift. -//! -//! WHAT IS NOT HERE. The `ProtocolDecl` registry row, the request handler and the two operation -//! cells name the engine's handler matrix, which is kernel-side machinery a pure kind may not -//! reach; they stayed behind with the engine. Nothing in this module opens anything, reads a clock -//! or names a kernel crate. - -use bytes::Bytes; -use serde::{Deserialize, Serialize}; - -/// THE RESOURCE-UPDATED NOTIFICATION'S PARAMETER OBJECT — `{"uri": ""}`. -/// -/// The SDK's `ResourceUpdatedNotificationParam` stayed behind with `rmcp` for the reason -/// [`subscribe`]'s parameter type states: `rmcp` hard-depends on `tokio`, and a plane's ENTIRE -/// transitive closure is scanned with one whole-workspace feature resolve, so naming the SDK here -/// would put a socket-capable runtime back in `busbar-plane-mcp`'s closure. The wire is unchanged — -/// serde ignores the `_meta` this codec never read, and the SDK's constructor left `_meta: None` -/// where it is skipped on serialize, so both directions emit and accept the same bytes. -#[derive(Debug, Clone, Serialize, Deserialize)] -struct ResourceUpdatedParam { - /// The URI of the resource that was updated. - uri: String, -} - -impl ResourceUpdatedParam { - fn new(uri: impl Into) -> Self { - Self { uri: uri.into() } - } -} - -/// The single mount path. MCP names the operation in the BODY (`method`), not the path — the -/// opposite of OpenAI, and the reason [`handler::McpRequestHandler::resolve_operation`] reads the -/// body. -/// -/// `pub` for the same reason [`PROTOCOL_VERSION`] is: `busbar-plane-mcp` declares its transport -/// claims against this path and may not name the server half, so it reads the value here. A path -/// the plane copied instead of read is a plane claiming a route nothing is served at. -pub const PATH_MCP: &str = "/mcp"; - -/// THE PREFIX THE PROTECTED-RESOURCE METADATA DOCUMENT IS SERVED UNDER (RFC 9728). -/// -/// It sits on the CODEC side because two halves read it and they must agree: the server half -/// composes the route it mounts from it, and `busbar-plane-mcp` declares an OPEN claim on the -/// composed path. The composition itself is [`protected_resource_metadata_path`] so that neither -/// half can spell the join differently. -pub const PROTECTED_RESOURCE_WELL_KNOWN: &str = "/.well-known/oauth-protected-resource"; - -/// THE DISCOVERY PATH FOR A GIVEN MOUNT — `/.well-known/oauth-protected-resource` + the mount. -/// -/// RFC 9728's metadata-path construction inserts the well-known segment between the origin -/// and the resource's own path, so the answer is a FUNCTION OF THE MOUNT and not a constant: the -/// mount is operator-configurable, and a hard-coded discovery path would point at the default -/// while the resource moved. One function, read from both sides, is what keeps the served route -/// and the plane's claim the same string. -#[must_use] -pub fn protected_resource_metadata_path(mount_path: &str) -> String { - format!("{PROTECTED_RESOURCE_WELL_KNOWN}{mount_path}") -} - -/// THE STATELESS REVISION: the one the plane's dispatch is written against, and the one busbar -/// speaks to an upstream first. -/// -/// It is not the only revision served. The session revisions (`2025-11-25`, `2025-06-18`) and the -/// `2024-11-05` event stream are served too, in both directions, by revision negotiation and never -/// by configuration (THE DESIGN section 2, the mcp bullet): a session request is RAISED into this -/// revision's shape before the one dispatch and its answer LOWERED after it -/// ([`crate::adapt`]), and the revisions and their negotiation are [`crate::revision`]'s. Every -/// request of this revision is answered on the stateless path exactly as it was before sessions -/// existed. -/// -/// It lives on the CODEC side of the split because it is protocol vocabulary — the plane's envelope -/// layer (`busbar_mcp::mcp::envelope`) re-exports it under its historical path, and the conformance -/// suite of `busbar-plane-mcp`, which may not name the server half, reads it here. -pub const PROTOCOL_VERSION: &str = "2026-07-28"; - -/// The JSON-RPC method names this dialect serves. -/// -/// These used to be read off `rmcp`'s own const-string types, so a name the SDK retired stopped -/// compiling here. `rmcp` hard-depends on `tokio` and could not cross into a pure kind's closure, -/// so THE SDK IS STILL THE ACCEPTANCE TEST — it just holds the pin one crate over. `busbar-mcp`, -/// which keeps the `rmcp` edge, asserts every one of these five against the SDK's const string in -/// its own test binary, so a retired name is a red test rather than a served one. `pub` rather than -/// `pub(crate)` because that assertion is now a cross-crate read. -/// Invoke a tool by name, with arguments. -pub const METHOD_TOOLS_CALL: &str = "tools/call"; -/// Register interest in a resource's changes. -pub const METHOD_RESOURCES_SUBSCRIBE: &str = "resources/subscribe"; -/// Withdraw that interest. -pub const METHOD_RESOURCES_UNSUBSCRIBE: &str = "resources/unsubscribe"; -/// The server's notice that its tool list is no longer what it was. -pub const METHOD_NOTIFY_TOOLS_LIST_CHANGED: &str = "notifications/tools/list_changed"; -/// The server's notice that a subscribed resource changed. -pub const METHOD_NOTIFY_RESOURCES_UPDATED: &str = "notifications/resources/updated"; - -/// THE `_meta` KEY CARRYING A REQUEST'S PROTOCOL VERSION. -/// -/// Under this revision negotiation is ON DEMAND — there is no handshake — so every request states -/// its own version, and this key is where it states it. It lives at `params._meta`, NOT at the top -/// level of the JSON-RPC envelope. -/// -/// It sits on the CODEC side because THREE readers must agree on the spelling and they are in -/// different crates: the server half requires it inbound, the client half emits it outbound, and -/// `busbar-plane-mcp` names it as a correlation fact key. The two directions were once written -/// down twice and each copy was internally consistent with its own side, so busbar would have -/// refused a request busbar itself sent; one definition is what makes that unrepresentable. -pub const META_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion"; - -/// THE `_meta` KEY CARRYING THE CLIENT'S CAPABILITIES FOR ONE REQUEST — REQUIRED, and absent is a -/// refusal rather than the empty capability set. -/// -/// With no `initialize` there is no earlier message the capabilities could have been stated in, so -/// a request that omits this has never stated them at all, and a server that fills the gap in has -/// decided on the client's behalf what the client can do. Defined here for the same three-reader -/// reason [`META_PROTOCOL_VERSION`] is. -pub const META_CLIENT_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities"; - -/// The header mirroring the body's `method`. REQUIRED on every request. -pub const H_MCP_METHOD: &str = "mcp-method"; -/// The header mirroring the target name. REQUIRED on `tools/call`, `resources/read`, `prompts/get`. -pub const H_MCP_NAME: &str = "mcp-name"; -/// The header mirroring the `_meta` protocol version. -pub const H_PROTOCOL_VERSION: &str = "mcp-protocol-version"; - -// ══ THE ERROR CODES, DEFINED EXACTLY ONCE ═══════════════════════════════════════════════════════ -// -// Both halves write these: the server half emits them, and `busbar-plane-mcp` publishes the set it -// may write as part of its own surface. The plane may not name the server half at all, so a code -// it could only COPY is a code the two sides can come to disagree about — and a JSON-RPC code is -// exactly the kind of value that reads plausibly while being wrong. The server half and the plane -// both define their constants AS these, so the compiler holds the equality. - -/// The bytes could not be read at all. JSON-RPC standard. -pub const CODE_PARSE_ERROR: i64 = -32700; -/// The envelope was not a request. JSON-RPC standard. -pub const CODE_INVALID_REQUEST: i64 = -32600; -/// The method named is not one this node answers. JSON-RPC standard; MCP pairs it with `404`. -pub const CODE_METHOD_NOT_FOUND: i64 = -32601; -/// The parameters were not admissible — including a missing or incomplete `params._meta`, which is -/// a member of `params` and so is the standard "invalid params" rather than a header defect. -pub const CODE_INVALID_PARAMS: i64 = -32602; -/// Something on this side failed. JSON-RPC standard. -pub const CODE_INTERNAL: i64 = -32603; -/// MCP `HeaderMismatchError`: an HTTP header disagreed with the body it mirrors. Always `400`. -pub const CODE_HEADER_MISMATCH: i64 = -32020; -/// MCP `MissingRequiredClientCapability`: the caller did not declare a capability the answer would -/// have needed. -pub const CODE_MISSING_CLIENT_CAPABILITY: i64 = -32021; -/// MCP `UnsupportedProtocolVersionError`: carries `data.requested` and `data.supported`. Always -/// `400`. -pub const CODE_UNSUPPORTED_PROTOCOL_VERSION: i64 = -32022; -/// A policy said no. The first code in JSON-RPC's implementation-defined server-error range, -/// because every reserved code is wrong for a specific reason. -pub const CODE_REFUSED: i64 = -32000; -/// The server this call would have reached could not be reached — the call NEVER HAPPENED, which -/// is why it is an extension rather than `-32603` (busbar broke) or `-32602` (the caller erred). -pub const CODE_UPSTREAM_UNAVAILABLE: i64 = -32030; - -/// EVERY CODE THIS DIALECT DEFINES. The plane asserts the set it may write is a subset of this, so -/// a code invented on one side of the seam is a red test rather than a wire nobody recognises. -pub const CODES: &[i64] = &[ - CODE_PARSE_ERROR, - CODE_INVALID_REQUEST, - CODE_METHOD_NOT_FOUND, - CODE_INVALID_PARAMS, - CODE_INTERNAL, - CODE_HEADER_MISMATCH, - CODE_MISSING_CLIENT_CAPABILITY, - CODE_UNSUPPORTED_PROTOCOL_VERSION, - CODE_REFUSED, - CODE_UPSTREAM_UNAVAILABLE, -]; - -/// The codes THIS revision retired, which a conformant node must never write. A retired code is -/// worse than an unknown one: a peer that still recognises it will act on a meaning this node did -/// not intend. -pub const RETIRED_CODES: &[i64] = &[-32002, -32042]; - -/// THE METHODS THIS DIALECT DISPATCHES. A method absent from here takes the `-32601` / `404` arm. -/// -/// On the codec side because it is the one list two crates read: the server half dispatches over it -/// and advertises it on `server/discover`, and `busbar-plane-mcp` carries a row for each. Two lists -/// that can disagree is a client told it may call something it may not — or a method the server -/// answers that the plane reports as an unsupported operation. -/// -/// `subscriptions/listen` is spelled here as a literal rather than read off the SDK's const-string -/// type: `rmcp` hard-depends on `tokio` and a plane's entire transitive closure is scanned, so the -/// SDK cannot cross into this crate. THE SDK IS STILL THE ACCEPTANCE TEST — `busbar-mcp`, which -/// keeps the `rmcp` edge, asserts this entry against `SubscriptionsListenRequestMethod::VALUE`, -/// exactly as it does for the five `METHOD_*` constants above. -pub const IMPLEMENTED_METHODS: &[&str] = &[ - "server/discover", - "tools/list", - METHOD_TOOLS_CALL, - "prompts/list", - "prompts/get", - "resources/list", - "resources/templates/list", - "resources/read", - "completion/complete", - // SEP-2663. The three v2 tasks methods, and ONLY the three: `tasks/result` and `tasks/list` - // were REMOVED by the extension's v2 wire — the result is inlined on `tasks/get` and there is - // no list — so their absence here is what makes them answer `-32601`, which is the conformant - // answer and not a gap. - "tasks/get", - "tasks/update", - "tasks/cancel", - // SEP-2575's replacement for the GET stream. It is a METHOD in this revision, so it belongs in - // this list rather than in the route table. - METHOD_SUBSCRIPTIONS_LISTEN, -]; - -/// The wire name of SEP-2575's listen method. See [`IMPLEMENTED_METHODS`] for why it is a literal -/// here and an SDK-pinned assertion one crate over. -pub const METHOD_SUBSCRIPTIONS_LISTEN: &str = "subscriptions/listen"; - -/// WHICH `params` MEMBER CARRIES A REQUEST'S SUBJECT, for the methods that address one. -/// -/// SEP-2243 and SEP-2663 require the subject to be mirrored into the `Mcp-Name` header, so this -/// rule is read from BOTH directions — the server half validates the mirror, the client half -/// composes it — and `busbar-plane-mcp` derives its own name pointers from it. It carried a second -/// copy on the client side once and the two DISAGREED, which sent a `tasks/get` out with no -/// `Mcp-Name` against a server that answers `-32020` to exactly that. -/// -/// The methods are ENUMERATED rather than matched on a prefix, and the tasks namespace is why: -/// `tasks/get` carries the header and `tasks/result` — a method this revision REMOVED — does not, -/// so a `tasks/*` prefix rule would answer `-32020` (your headers are wrong) to a request whose -/// only defect is naming a method that no longer exists, which must be `-32601`. -#[must_use] -pub fn name_source_of(method: &str) -> Option<&'static str> { - match method { - "tools/call" | "prompts/get" => Some("name"), - "resources/read" => Some("uri"), - "tasks/get" | "tasks/update" | "tasks/cancel" => Some("taskId"), - _ => None, - } -} - -// ══ THE NOTIFICATION HALF ════════════════════════════════════════════════════════════════════════ - -/// THE SERVER-ORIGINATED NOTIFICATIONS THIS PROTOCOL CARRIES, and the reason a subscription is worth -/// registering at all. -/// -/// ## WHY THIS IS NOT AN `OperationHandler` -/// -/// A notification has no id, no answer and no correlation: JSON-RPC 2.0 section 4.1 forbids replying -/// to one. It is therefore not a request/response codec and modelling it as one would give it a -/// response half that must never be produced. It sits beside the codecs, in the protocol's own -/// crate, because the thing it is specific to is the DIALECT — the same reason the JSON-RPC envelope -/// itself is read once, in one place, for every plane that speaks it. -/// -/// ## BOTH DIRECTIONS READ THE SAME BYTES -/// -/// When busbar is the server it EMITS these to its caller; when busbar is the client it RECEIVES -/// them from an upstream. That is one wire message and two directions of travel, so it is one -/// reader and one writer here rather than a pair per direction — which is exactly the arrangement -/// that stopped the JSON-RPC envelope from being parsed two ways that disagreed. -/// -/// ## WHAT A RECEIVED `notifications/tools/list_changed` MAY AND MAY NOT DO -/// -/// It is a HINT, and it arrives from a party whose timing and content are not busbar's to trust. It -/// may prompt a re-read of a catalogue through the ordinary approval path; it may never itself -/// install, approve or promote anything, because that would let the party being catalogued decide -/// when its own catalogue is believed. This type carries the message and takes no such action, which -/// is what keeps that decision at the call site that has the approval context. -#[cfg_attr(not(test), allow(dead_code))] -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum McpNotification { - /// The peer's tool list is no longer what it was. - ToolsListChanged, - /// A subscribed resource changed. The delivery half of [`subscribe::SubscribeOperation`]. - ResourceUpdated { - /// The resource that changed, in the peer's vocabulary. - uri: String, - }, -} - -#[cfg_attr(not(test), allow(dead_code))] -impl McpNotification { - /// The wire method name, off `rmcp`'s const-string types. - pub fn method(&self) -> &'static str { - match self { - McpNotification::ToolsListChanged => METHOD_NOTIFY_TOOLS_LIST_CHANGED, - McpNotification::ResourceUpdated { .. } => METHOD_NOTIFY_RESOURCES_UPDATED, - } - } - - /// Read a notification that has ALREADY been established as a JSON-RPC notification envelope by - /// the engine's JSON-RPC ingress step. That step is named by SHAPE and not by crate, because a - /// doc comment is a name the `kind-isolation:matrix` scanner counts exactly like a `use`, and - /// #40(a) admits one dependency here and no other. `None` means "a notification this protocol - /// does not carry", which is the correct answer to give and the correct thing to do nothing - /// about: section 4.1 forbids replying, so an unknown notification is dropped rather than - /// refused. - pub fn read(method: &str, params: Option<&serde_json::Value>) -> Option { - match method { - METHOD_NOTIFY_TOOLS_LIST_CHANGED => Some(McpNotification::ToolsListChanged), - METHOD_NOTIFY_RESOURCES_UPDATED => { - // The SDK's parameter type is the acceptance test here too: a notification that - // names no resource says a resource changed without saying which, and acting on it - // would mean guessing. - let p: ResourceUpdatedParam = serde_json::from_value(params?.clone()).ok()?; - (!p.uri.is_empty()).then_some(McpNotification::ResourceUpdated { uri: p.uri }) - } - _ => None, - } - } - - /// The complete JSON-RPC notification envelope for this message. - /// - /// **There is no `id` member and there must never be one.** Section 4.1 makes the absence of - /// `id` the definition of a notification; an id would make this a request, and a request obliges - /// the receiver to answer something busbar is not waiting for. - pub fn write(&self) -> Bytes { - let mut envelope = serde_json::json!({ "jsonrpc": "2.0", "method": self.method() }); - if let McpNotification::ResourceUpdated { uri } = self { - // `params` is emitted only where the message has any, so the notification that carries - // none stays byte-identical to what the specification describes. - envelope["params"] = serde_json::to_value(ResourceUpdatedParam::new(uri.clone())) - .unwrap_or_else(|_| serde_json::json!({ "uri": uri })); - } - Bytes::from(serde_json::to_vec(&envelope).unwrap_or_default()) - } -} - -/// The fields THIS BODY implies, by lower-case name: the `Accept` preference (a request that named -/// a logging level or a progress token asked for the frames only a stream carries), the method, -/// the target name where the method has one, and the protocol version its `_meta` states. -#[must_use] -pub fn mirrored(value: &serde_json::Value) -> Vec<(String, String)> { - let mut out = Vec::new(); - let meta = value.get("params").and_then(|p| p.get("_meta")); - let wants_stream = meta.is_some_and(|m| { - m.get(crate::framing::META_LOGGING_LEVEL).is_some() - || m.get("progressToken").is_some_and(|t| !t.is_null()) - }); - out.push(( - "accept".to_string(), - if wants_stream { - "text/event-stream, application/json" - } else { - "application/json, text/event-stream" - } - .to_string(), - )); - if let Some(method) = value.get("method").and_then(serde_json::Value::as_str) { - out.push(( - H_MCP_METHOD.to_string(), - crate::client::jsonrpc::encode_sentinel(method), - )); - if let Some(name) = name_source_of(method) - .and_then(|source| value.get("params").and_then(|p| p.get(source))) - .and_then(serde_json::Value::as_str) - { - out.push(( - H_MCP_NAME.to_string(), - crate::client::jsonrpc::encode_sentinel(name), - )); - } - } - if let Some(version) = meta - .and_then(|m| m.get(META_PROTOCOL_VERSION)) - .and_then(serde_json::Value::as_str) - .filter(|v| !v.is_empty()) - { - out.push((H_PROTOCOL_VERSION.to_string(), version.to_string())); - } - out -} - -#[cfg(test)] -#[path = "tests/codec_tests.rs"] -mod codec_tests; diff --git a/crates/busbar-plane-mcp/src/declares.json b/crates/busbar-plane-mcp/src/declares.json deleted file mode 100644 index 4b7ca5a3c5..0000000000 --- a/crates/busbar-plane-mcp/src/declares.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "breaker": { - "bench_below_trip_threshold": false - }, - "diagnostics": [ - { - "code": 2040, - "slug": "mcp-calllog-chain-verify-failed", - "title": "MCP per-call log failed hash-chain verification on restore (tamper evidence)", - "severity": "actionable", - "summary": "The persisted MCP per-call log was read at boot but does NOT verify against its own hash chain, which is tamper evidence — a persisted call record was altered out from under busbar, or its store is corrupt. The records are still restored and the chain resumes from the broken tail, because refusing to restore would let anyone able to write to the store DELETE a caller's history by corrupting one record.", - "action": "Treat the durable governance store as compromised until explained: capture it for forensic review before it is overwritten, then restore from a trusted backup once the cause is understood.", - "since": "1.6.0" - }, - { - "code": 7060, - "slug": "mcp-calllog-empty-chains", - "title": "Durable MCP call log enumerates principals with NO records", - "severity": "actionable", - "summary": "At boot the durable MCP per-call log named one or more principals but returned no records for them, so their chains reopen at seq 1. The verifier cannot distinguish this from a caller's evidence being deleted wholesale, so it is surfaced rather than summed silently into the restored total.", - "action": "Confirm whether these principals were expected to have call history. If they were, treat the durable governance store as possibly tampered and capture it for review before it is overwritten.", - "since": "1.6.0" - }, - { - "code": 7061, - "slug": "mcp-calllog-unread", - "title": "Durable MCP per-call log could not be read at boot", - "severity": "actionable", - "summary": "The durable MCP per-call log could not be read back at boot, so the persisted tail is unknown and a principal that already has rows in the store may reopen its chain at seq 1 and collide with a persisted sequence number.", - "action": "Check the durable governance store's health and connectivity. Once it answers, restart so the per-call chains restore from a known tail.", - "since": "1.6.0" - }, - { - "code": 7062, - "slug": "mcp-demotions-restored", - "title": "MCP upstream demotions restored from the durable store", - "severity": "actionable", - "summary": "One or more MCP upstream servers were quarantined before the last restart and their demotion records were replayed from the durable governance store, so they are refused until an operator works the change or a sweep observes them serving what was approved.", - "action": "Investigate why each named server was demoted and either remediate it or clear its demotion. Until then, requests routed to it are refused by design.", - "since": "1.6.0" - }, - { - "code": 7063, - "slug": "mcp-stdio-read-error", - "title": "MCP stdio serve read error on stdin (session ending)", - "severity": "benign_recurring", - "summary": "The MCP stdio server hit a read error on stdin and is shutting the session down. This is the expected outcome when the peer closes the pipe, so it is logged at debug rather than as an operator alert.", - "action": "None — self-heals. Expected when a stdio MCP client disconnects.", - "since": "1.6.0" - }, - { - "code": 7064, - "slug": "mcp-ask-recogniser-missed", - "title": "MCP input-required result reached the terminal check (ask recogniser missed)", - "severity": "actionable", - "summary": "An upstream MCP tool returned an input-required result that reached the terminal check without the ask recogniser catching it — an internal invariant breach, since such a result should have been recognised and handled earlier. The call is refused rather than handing the caller an upstream's demand for a secret.", - "action": "Report the named tool and field: the ask-recognition path has a gap that let an input-required shape through. This is a code-level fix, not an operator misconfig.", - "since": "1.6.0" - }, - { - "code": 7065, - "slug": "mcp-output-schema-violation", - "title": "MCP upstream structuredContent violated the published outputSchema — RETIRED", - "severity": "benign_recurring", - "summary": "RETIRED. An upstream MCP tool's `structuredContent` that did not validate against the tool's published `outputSchema` was once refused and replaced by busbar's own tool error. 1.6.0 removed that check: busbar relays an upstream's tool result unchanged (THE DESIGN Law 11), and the caller judges it against the published schema.", - "action": "Nothing emits this code.", - "since": "1.6.0" - }, - { - "code": 7066, - "slug": "mcp-toolcall-refused", - "title": "MCP tools/call refused by policy", - "severity": "benign_recurring", - "summary": "An MCP `tools/call` was refused by busbar's policy (budget, gate, or capability). This is a routine per-request governance outcome, logged at debug so a busy caller cannot spam the operator log.", - "action": "None — self-heals. The refusal reason is recorded in the audit and call log if a specific caller needs to be understood.", - "since": "1.6.0" - }, - { - "code": 7067, - "slug": "mcp-toolcall-upstream-failed", - "title": "MCP tools/call upstream failed", - "severity": "benign_recurring", - "summary": "An MCP `tools/call` was dispatched and the upstream server failed to execute it. This is reported to the model as a tool execution error (not a busbar refusal) and can recur per request, so it is logged at debug.", - "action": "None locally — self-heals. If a specific upstream fails persistently, check that server's health.", - "since": "1.6.0" - }, - { - "code": 7068, - "slug": "mcp-toolcall-refused-pre-upstream", - "title": "MCP tools/call refused before the upstream", - "severity": "benign_recurring", - "summary": "An MCP `tools/call` was refused before it reached the upstream (a pre-dispatch policy denial). Routine per-request governance, logged at debug to avoid spamming the operator log under load.", - "action": "None — self-heals. The refusal reason is in the audit and call log.", - "since": "1.6.0" - }, - { - "code": 7069, - "slug": "mcp-caller-ask-refused", - "title": "MCP caller-ask refused", - "severity": "benign_recurring", - "summary": "A caller's MCP ask for a capability was refused by policy. This is a routine per-request governance outcome, logged at debug so it cannot spam the operator log.", - "action": "None — self-heals. The refusal reason is recorded in the audit and call log.", - "since": "1.6.0" - } - ] -} diff --git a/crates/busbar-plane-mcp/src/diagnostics.rs b/crates/busbar-plane-mcp/src/diagnostics.rs deleted file mode 100644 index f216ea7258..0000000000 --- a/crates/busbar-plane-mcp/src/diagnostics.rs +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! MCP plane diagnostics — the `MCP_*` catalog entries this crate OWNS. -//! -//! These consts are plane-specific vocabulary, so the host's neutral catalog names no `MCP_*` -//! diagnostic. Each keeps its stable `BUSBAR-NNNN` number and slug: codes are REGISTERED, never -//! renumbered or collapsed. -//! -//! [`DIAGNOSTICS`] is the slice the composition root hands to the host's `install_diagnostics`, so -//! these codes join the runtime catalog (`REGISTRY ∪ installed`) and resolve through `by_code`. - -use busbar_contract::diagnostic::{Class, Diagnostic, Severity}; - -/// MCP per-call log failed hash-chain verification on restore (tamper evidence). -pub const MCP_CALLLOG_CHAIN_VERIFY_FAILED: Diagnostic = Diagnostic { - code: 2040, - class: Class::Audit, - slug: "mcp-calllog-chain-verify-failed", - title: "MCP per-call log failed hash-chain verification on restore (tamper evidence)", - severity: Severity::Actionable, - summary: "The persisted MCP per-call log was read at boot but does NOT verify against its own \ - hash chain, which is tamper evidence — a persisted call record was altered out from \ - under busbar, or its store is corrupt. The records are still restored and the chain \ - resumes from the broken tail, because refusing to restore would let anyone able to \ - write to the store DELETE a caller's history by corrupting one record.", - action: "Treat the durable governance store as compromised until explained: capture it for \ - forensic review before it is overwritten, then restore from a trusted backup once the \ - cause is understood.", - since: "1.6.0", - retired: false, -}; - -/// Durable MCP call log enumerates principals with NO records. -pub const MCP_CALLLOG_EMPTY_CHAINS: Diagnostic = Diagnostic { - code: 7060, - class: Class::Plane, - slug: "mcp-calllog-empty-chains", - title: "Durable MCP call log enumerates principals with NO records", - severity: Severity::Actionable, - summary: "At boot the durable MCP per-call log named one or more principals but returned no \ - records for them, so their chains reopen at seq 1. The verifier cannot distinguish \ - this from a caller's evidence being deleted wholesale, so it is surfaced rather than \ - summed silently into the restored total.", - action: "Confirm whether these principals were expected to have call history. If they were, \ - treat the durable governance store as possibly tampered and capture it for review \ - before it is overwritten.", - since: "1.6.0", - retired: false, -}; - -/// Durable MCP per-call log could not be read at boot. -pub const MCP_CALLLOG_UNREAD: Diagnostic = Diagnostic { - code: 7061, - class: Class::Plane, - slug: "mcp-calllog-unread", - title: "Durable MCP per-call log could not be read at boot", - severity: Severity::Actionable, - summary: - "The durable MCP per-call log could not be read back at boot, so the persisted tail is \ - unknown and a principal that already has rows in the store may reopen its chain at \ - seq 1 and collide with a persisted sequence number.", - action: - "Check the durable governance store's health and connectivity. Once it answers, restart \ - so the per-call chains restore from a known tail.", - since: "1.6.0", - retired: false, -}; - -/// MCP upstream demotions restored from the durable store. -pub const MCP_DEMOTIONS_RESTORED: Diagnostic = Diagnostic { - code: 7062, - class: Class::Plane, - slug: "mcp-demotions-restored", - title: "MCP upstream demotions restored from the durable store", - severity: Severity::Actionable, - summary: "One or more MCP upstream servers were quarantined before the last restart and their \ - demotion records were replayed from the durable governance store, so they are refused \ - until an operator works the change or a sweep observes them serving what was approved.", - action: "Investigate why each named server was demoted and either remediate it or clear its \ - demotion. Until then, requests routed to it are refused by design.", - since: "1.6.0", - retired: false, -}; - -/// The serve loop could not read its standard input, so the session ends. -pub const MCP_STDIO_READ_ERROR: Diagnostic = Diagnostic { - code: 7063, - class: Class::Plane, - slug: "mcp-stdio-read-error", - title: "MCP stdio serve read error on stdin (session ending)", - severity: Severity::BenignRecurring, - summary: "The MCP stdio server hit a read error on stdin and is shutting the session down. This \ - is the expected outcome when the peer closes the pipe, so it is logged at debug rather \ - than as an operator alert.", - action: "None — self-heals. Expected when a stdio MCP client disconnects.", - since: "1.6.0", - retired: false, -}; - -/// MCP input-required result reached the terminal check (ask recogniser missed). -pub const MCP_ASK_RECOGNISER_MISSED: Diagnostic = Diagnostic { - code: 7064, - class: Class::Plane, - slug: "mcp-ask-recogniser-missed", - title: "MCP input-required result reached the terminal check (ask recogniser missed)", - severity: Severity::Actionable, - summary: - "An upstream MCP tool returned an input-required result that reached the terminal check \ - without the ask recogniser catching it — an internal invariant breach, since such a \ - result should have been recognised and handled earlier. The call is refused rather \ - than handing the caller an upstream's demand for a secret.", - action: "Report the named tool and field: the ask-recognition path has a gap that let an \ - input-required shape through. This is a code-level fix, not an operator misconfig.", - since: "1.6.0", - retired: false, -}; - -/// RETIRED in 1.6.0 with the structured-output check it reported: an upstream's tool result is -/// relayed unchanged (THE DESIGN Law 11). The number is kept so a log line from a pre-release build -/// still resolves to an entry that says what happened. -pub const MCP_OUTPUT_SCHEMA_VIOLATION: Diagnostic = Diagnostic { - code: 7065, - class: Class::Plane, - slug: "mcp-output-schema-violation", - title: "MCP upstream structuredContent violated the published outputSchema — RETIRED", - severity: Severity::BenignRecurring, - summary: "RETIRED. An upstream MCP tool's `structuredContent` that did not validate against \ - the tool's published `outputSchema` was once refused and replaced by busbar's own \ - tool error. 1.6.0 removed that check: busbar relays an upstream's tool result \ - unchanged (THE DESIGN Law 11), and the caller judges it against the published \ - schema.", - action: "Nothing emits this code.", - since: "1.6.0", - retired: true, -}; - -/// MCP tools/call refused by policy. -pub const MCP_TOOLCALL_REFUSED: Diagnostic = Diagnostic { - code: 7066, - class: Class::Plane, - slug: "mcp-toolcall-refused", - title: "MCP tools/call refused by policy", - severity: Severity::BenignRecurring, - summary: - "An MCP `tools/call` was refused by busbar's policy (budget, gate, or capability). This \ - is a routine per-request governance outcome, logged at debug so a busy caller cannot \ - spam the operator log.", - action: "None — self-heals. The refusal reason is recorded in the audit and call log if a \ - specific caller needs to be understood.", - since: "1.6.0", - retired: false, -}; - -/// MCP tools/call upstream failed. -pub const MCP_TOOLCALL_UPSTREAM_FAILED: Diagnostic = Diagnostic { - code: 7067, - class: Class::Plane, - slug: "mcp-toolcall-upstream-failed", - title: "MCP tools/call upstream failed", - severity: Severity::BenignRecurring, - summary: - "An MCP `tools/call` was dispatched and the upstream server failed to execute it. This \ - is reported to the model as a tool execution error (not a busbar refusal) and can \ - recur per request, so it is logged at debug.", - action: "None locally — self-heals. If a specific upstream fails persistently, check that \ - server's health.", - since: "1.6.0", - retired: false, -}; - -/// MCP tools/call refused before the upstream. -pub const MCP_TOOLCALL_REFUSED_PRE_UPSTREAM: Diagnostic = Diagnostic { - code: 7068, - class: Class::Plane, - slug: "mcp-toolcall-refused-pre-upstream", - title: "MCP tools/call refused before the upstream", - severity: Severity::BenignRecurring, - summary: - "An MCP `tools/call` was refused before it reached the upstream (a pre-dispatch policy \ - denial). Routine per-request governance, logged at debug to avoid spamming the \ - operator log under load.", - action: "None — self-heals. The refusal reason is in the audit and call log.", - since: "1.6.0", - retired: false, -}; - -/// MCP caller-ask refused. -pub const MCP_CALLER_ASK_REFUSED: Diagnostic = Diagnostic { - code: 7069, - class: Class::Plane, - slug: "mcp-caller-ask-refused", - title: "MCP caller-ask refused", - severity: Severity::BenignRecurring, - summary: "A caller's MCP ask for a capability was refused by policy. This is a routine \ - per-request governance outcome, logged at debug so it cannot spam the operator log.", - action: "None — self-heals. The refusal reason is recorded in the audit and call log.", - since: "1.6.0", - retired: false, -}; - -/// MCP'S PLANE-CONTRIBUTED DIAGNOSTICS — the `&'static [&'static Diagnostic]` the composition -/// root installs via `install_diagnostics`. Ascending by code, mirroring the neutral `REGISTRY`. -pub static DIAGNOSTICS: &[&Diagnostic] = &[ - &MCP_CALLLOG_CHAIN_VERIFY_FAILED, - &MCP_CALLLOG_EMPTY_CHAINS, - &MCP_CALLLOG_UNREAD, - &MCP_DEMOTIONS_RESTORED, - &MCP_STDIO_READ_ERROR, - &MCP_ASK_RECOGNISER_MISSED, - &MCP_OUTPUT_SCHEMA_VIOLATION, - &MCP_TOOLCALL_REFUSED, - &MCP_TOOLCALL_UPSTREAM_FAILED, - &MCP_TOOLCALL_REFUSED_PRE_UPSTREAM, - &MCP_CALLER_ASK_REFUSED, -]; - -#[cfg(test)] -#[path = "tests/diagnostics_tests.rs"] -mod diagnostics_tests; diff --git a/crates/busbar-plane-mcp/src/door.rs b/crates/busbar-plane-mcp/src/door.rs deleted file mode 100644 index 96b8d5dbea..0000000000 --- a/crates/busbar-plane-mcp/src/door.rs +++ /dev/null @@ -1,780 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE MCP PLANE'S DOOR, its declarations: what the plane states once in its Statement tail, what -//! each generation publishes in its snapshot, and how `validate`/`open` read the settings blob. -//! -//! ## What the kernel hands the plane -//! -//! The settings blob is the plane's own section value, the `tools:` map, as JSON, with the kernel's -//! reserved sub-keys already read and stripped: the hook bindings and `upstream_credentials` never -//! reach the plane, and neither do the kernel-owned trust keys each entry carries -//! ([`crate::tools_config::TRUST_KEYS`]), which the kernel parses and judges first. The plane judges the -//! rest with the grammar the engine uses, [`crate::tools_config::ToolsCfg`] and -//! [`crate::tools_config::validate_server`]. -//! -//! ## What is written down here and pinned elsewhere -//! -//! The tail's strings, the route table and the admin routes are stated here as constants. The -//! engine that still serves MCP states the same facts in its own declaration and router, and a test -//! on the engine's side pins the two equal, entry by entry, until the engine is gone. - -use busbar_contract::abi::host::conn::connector::{ - Need, DIRECTION_OUTBOUND, EGRESS_OPERATOR_INFRASTRUCTURE, EGRESS_PROVIDER, KEEP_NAMED, -}; -use busbar_contract::abi::mechanism::call::{AbiStr, Blob, BLOB_ABSENT}; -use busbar_contract::abi::mechanism::door::{KindTailHead, Section, SECTION_DECLARING}; -use busbar_contract::abi::plane::{ - AdminRoute, BillableClass, OpClass, PinMechanism, PlaneTail, RecordChain, TrustKey, - CHAIN_DIGESTS_SCOPE, CHAIN_LENGTH_PREFIXED, CLAIM_EXACT, CLAIM_OPEN, INGRESS_REQUEST_RESPONSE, - INGRESS_RESPONSE_STREAM, MECHANISM_ROOT, SHAPE_PIECEWISE, TRUST_PIN, TRUST_PRIVATE_REACH, - TRUST_REVERIFY_TTL, -}; -use busbar_contract::abi::sdk::door::abi_str; -use busbar_contract::abi::sdk::publish::{AdminRouteSpec, ClaimSpec, SnapshotSpec}; - -use crate::tool_claims::{ - CARRIER_HTTP, CARRIER_SSE, CARRIER_STDIO, DEFAULT_METADATA, DEFAULT_MOUNT, TASK_RUN_MOUNT, -}; -use crate::tools_config::{ToolsCfg, DEFAULT_MCP_VERIFY_TTL, SUBJECT_NOUN}; - -/// The grant kind that admits traffic on this plane: one registered server. -pub const SCOPE: &str = "mcp_server"; -/// The signing domain busbar's own ask state is sealed under ([`crate::seal`]): the host's `sign` -/// derives this domain's subkey from the deployment's fleet-shared key. -pub const ASK_SIGNING_DOMAIN: &str = "mcp/ask-state/v1"; -/// The key-id prefix of those seals. -pub const ASK_KID_PREFIX: &str = "busbar-mcp-ask-"; -/// The second grant kind: one tool of a registered server, keyed on its published name. -pub const SCOPE_TOOL: &str = "mcp_tool"; -/// The plane's human label. -pub const LABEL: &str = "MCP"; -/// The singular noun for one registration, in admin responses. -pub const ADMIN_NOUN: &str = "mcp-server"; -/// The record resource kind a registration is audited under. -pub const AUDIT_KIND: &str = "mcp_server"; -/// The section this plane owns beside `tools:`: its endpoint block. -pub const ENDPOINT_SECTION: &str = "mcp"; - -/// WHERE EACH REGISTERED SERVER IS REACHED: its registration's `url`, as a member-target path -/// (`busbar_contract::section::MEMBER_TARGET_PREFIX`): one member route per server, at its own URL. -pub const MEMBER_TARGET: &str = "settings.*.url"; -/// The one wire dialect: JSON-RPC 2.0, carried over each of the plane's transports. -pub const DIALECT: &str = "jsonrpc"; -/// The billable class a call is counted in, and its family. -pub const CALLS_FAMILY: &str = "count"; -/// The billable class a hop's moved bytes are counted in, and its family. -pub const BYTES_FAMILY: &str = "byte"; -/// The fee unit this plane declares: one per request. -pub const FEE_PER_REQUEST: &str = busbar_contract::plane::PER_REQUEST; - -/// An absent string. -const NONE: AbiStr = AbiStr { - ptr: std::ptr::null(), - len: 0, -}; - -/// THE SECTIONS THE PLANE'S STATEMENT DECLARES: `tools:`, which it owns and judges, and its endpoint -/// block beside it. -/// The names are literals (the config-schema census reads a door-only plane's sections off this -/// table); a test holds them equal to the grammar's `tools_config::SECTION` and [`ENDPOINT_SECTION`]. -pub const SECTIONS: &[Section] = &[ - Section { - name: abi_str("tools"), - flags: SECTION_DECLARING, - _reserved: 0, - }, - Section { - name: abi_str("mcp"), - flags: 0, - _reserved: 0, - }, -]; - -/// THE SECRET REFERENCES ITS SECTION HOLDS, by settings path (`*` = every registration, every -/// entry): a server's RFC 8693 subject token (busbar's own) and a stdio child's environment values -/// that are references (a pipe has no header block, so `env:` is the one channel a child gets a -/// credential through). The kernel enumerates them so `--validate` and boot resolve each one. -pub const SECRET_REFS: &[AbiStr] = &[ - abi_str("settings.*.token_exchange.subject_token"), - abi_str("settings.*.env.*"), -]; - -/// The far end's response fields the plane reads: the answer's media type (a JSON document or an -/// event stream) and a session revision's session id. -pub const KEEP_RESPONSE_HEADERS: &[AbiStr] = &[ - abi_str(crate::framing::CONTENT_TYPE), - abi_str(crate::adapt::H_SESSION_ID), -]; - -/// The index of [`NEEDS`]' first need: the relayed calls to the registered servers reached at a URL. -pub const NEED_UPSTREAM: u32 = 0; - -/// The index of [`NEEDS`]' second need: the `transport: stdio` registrations, each a PROGRAM the -/// host keeps running (ARCHITECT round 5 Q-L3B-STDIO-UPSTREAM (A)). -pub const NEED_PROGRAM: u32 = 1; - -/// WHERE EACH STDIO SERVER IS REACHED: its registration's own program, as the member-program path -/// (`busbar_contract::section::MEMBER_PROGRAM`): its `command`, `args` and `env`. -pub const MEMBER_PROGRAM: &str = busbar_contract::section::MEMBER_PROGRAM; - -/// The program need's response field the plane reads: the generation of the child a lease reached. -pub const KEEP_PROGRAM_HEADERS: &[AbiStr] = - &[abi_str(busbar_contract::conn::PROGRAM_GENERATION_FIELD)]; - -const NO_DETAILS: Blob = Blob { - ptr: std::ptr::null(), - len: 0, - fmt: BLOB_ABSENT, - flags: 0, -}; - -/// THE PLANE'S CONNECTION NEEDS: outbound over http to the servers the operator registered at a URL, -/// governed as upstreams a plane reaches (no auth style is named here: each member's credential is -/// the binding the kernel seals for it, and it adds the fields when it sends); and the `transport: -/// stdio` servers, each the program its registration names, run by the host as the operator's own -/// infrastructure with no credential (a pipe carries none), one long-lived child per server. -pub const NEEDS: &[Need] = &[ - Need { - direction: DIRECTION_OUTBOUND, - egress_class: EGRESS_PROVIDER, - transport: abi_str(CARRIER_HTTP), - auth: NONE, - // Each registered server is reached at its own `url` (ARCHITECT Q-L3B-ROUTES). - target_from: abi_str(MEMBER_TARGET), - trust_from: NONE, - details: NO_DETAILS, - keep_response_headers: KEEP_RESPONSE_HEADERS.as_ptr(), - keep_response_headers_len: KEEP_RESPONSE_HEADERS.len(), - timeout_ms: 0, - keep_mode: KEEP_NAMED, - _reserved: 0, - deny_response_headers: std::ptr::null(), - deny_response_headers_len: 0, - }, - Need { - direction: DIRECTION_OUTBOUND, - egress_class: EGRESS_OPERATOR_INFRASTRUCTURE, - transport: abi_str(CARRIER_STDIO), - auth: NONE, - target_from: abi_str(MEMBER_PROGRAM), - trust_from: NONE, - details: NO_DETAILS, - keep_response_headers: KEEP_PROGRAM_HEADERS.as_ptr(), - keep_response_headers_len: KEEP_PROGRAM_HEADERS.len(), - timeout_ms: 0, - keep_mode: KEEP_NAMED, - _reserved: 0, - deny_response_headers: std::ptr::null(), - deny_response_headers_len: 0, - }, -]; - -const DIALECTS: &[AbiStr] = &[abi_str(DIALECT)]; - -const SCOPE_KINDS: &[AbiStr] = &[abi_str(SCOPE), abi_str(SCOPE_TOOL)]; - -const BILLABLE_CLASSES: &[BillableClass] = &[ - BillableClass { - class: abi_str(crate::tool_meta::CLASS_TOOL_CALLS.as_str()), - family: abi_str(CALLS_FAMILY), - }, - BillableClass { - class: abi_str(crate::tool_meta::CLASS_BYTES.as_str()), - family: abi_str(BYTES_FAMILY), - }, - // THE FEE UNIT is also a class (the tail check holds `fee_units ⊆ billable_classes`, ARCHITECT - // Q-L5-FEE (C)): reported `1` on an answer the caller is served with a success, so a unit whose - // caller is answered otherwise has its `per_request` refunded, as 1.5.5 refunded it. - BillableClass { - class: abi_str(FEE_PER_REQUEST), - family: abi_str(FEE_FAMILY), - }, -]; - -/// The family of the fee unit's class: a count of 0 or 1. -pub const FEE_FAMILY: &str = "count"; - -/// The tail index of the fee unit's class in [`BILLABLE_CLASSES`]. -pub const CLASS_FEE_INDEX: u32 = 2; - -const FEE_UNITS: &[AbiStr] = &[abi_str(FEE_PER_REQUEST)]; - -const RECORD_KINDS: &[AbiStr] = &[ - abi_str(crate::tool_records::KIND_CALL), - abi_str(crate::tool_records::KIND_DEMOTION), - abi_str(crate::tool_records::KIND_APPROVAL), - abi_str(crate::tool_records::KIND_TASK), -]; - -/// The kind a completed ask exchange's approval is claimed under, once ([`RECORD_KINDS`]): the -/// spent-approval ledger is the host's one-time `records.claim` (ARCHITECT Q-L3B-ASK). -pub const KIND_APPROVAL: &str = crate::tool_records::KIND_APPROVAL; - -/// The index of the call record's kind in [`RECORD_KINDS`]. -pub const RECORD_CALL: u32 = 0; - -/// The index of the task kind in [`RECORD_KINDS`]: a task's result chunks are written under it. -pub const RECORD_TASK: u32 = 3; - -/// The kind a task's work handle is opened under (`work.open`, [`RECORD_KINDS`]). -pub const KIND_TASK: &str = crate::tool_records::KIND_TASK; - -/// THE CALL LOG IS A CHAIN: each caller's call records are hash-chained by the host, the prelude -/// (`prev_hash`, the scope, `seq`) LengthPrefixed with the scope in the digest, then the plane's own -/// fields ([`crate::record::call_suffix`]) joined raw. -const RECORD_CHAINS: &[RecordChain] = &[RecordChain { - kind: RECORD_CALL, - framing: CHAIN_LENGTH_PREFIXED, - flags: CHAIN_DIGESTS_SCOPE, - _reserved: 0, -}]; - -/// The pin's four mechanisms; three are authenticity roots and `unpinned` is not. -const PIN_MECHANISMS: &[PinMechanism] = &[ - PinMechanism { - token: abi_str("pinned_pubkey"), - flags: MECHANISM_ROOT, - _reserved: 0, - }, - PinMechanism { - token: abi_str("cert_spki"), - flags: MECHANISM_ROOT, - _reserved: 0, - }, - PinMechanism { - token: abi_str("mtls"), - flags: MECHANISM_ROOT, - _reserved: 0, - }, - PinMechanism { - token: abi_str("unpinned"), - flags: 0, - _reserved: 0, - }, -]; - -/// The kernel-owned trust keys of one entry, the ABI spelling of [`crate::tools_config::TRUST_KEYS`]; -/// declaration order is judgement order. The pin carries no fingerprint: a server here offers -/// none an operator could approve out of band. -const TRUST_KEYS: &[TrustKey] = &[ - TrustKey { - key: abi_str("pin"), - role: TRUST_PIN, - flags: 0, - default: NONE, - mechanisms: PIN_MECHANISMS.as_ptr(), - mechanisms_len: PIN_MECHANISMS.len(), - }, - TrustKey { - key: abi_str("verify_ttl"), - role: TRUST_REVERIFY_TTL, - flags: 0, - default: abi_str(DEFAULT_MCP_VERIFY_TTL), - mechanisms: std::ptr::null(), - mechanisms_len: 0, - }, - TrustKey { - key: abi_str("allow_private"), - role: TRUST_PRIVATE_REACH, - flags: 0, - default: NONE, - mechanisms: std::ptr::null(), - mechanisms_len: 0, - }, - TrustKey { - key: abi_str(crate::tools_config::TOOL_APPROVALS_KEY), - role: busbar_contract::abi::plane::TRUST_ITEM_APPROVALS, - flags: 0, - default: abi_str(crate::tools_config::TOOL_APPROVAL_FIELD), - mechanisms: std::ptr::null(), - mechanisms_len: 0, - }, -]; - -const fn op_class(i: usize) -> OpClass { - OpClass { - op: abi_str(crate::tool_ops::OP_CLASSES[i].as_str()), - name: abi_str(crate::tool_ops::OP_CLASSES[i].as_str()), - } -} - -/// The operation classes, in [`crate::tool_ops::OP_CLASSES`] order. -const OP_CLASS_TABLE: &[OpClass] = &[ - op_class(0), - op_class(1), - op_class(2), - op_class(3), - op_class(4), - op_class(5), - op_class(6), - op_class(7), - op_class(8), - op_class(9), - op_class(10), - op_class(11), - op_class(12), - op_class(13), - op_class(14), - op_class(15), - op_class(16), -]; - -/// The tail index of an operation class, where the table holds it. -#[must_use] -pub fn op_class_index(op: busbar_contract::ids::OpClassId) -> Option { - crate::tool_ops::OP_CLASSES - .iter() - .position(|c| *c == op) - .map(|i| i as u32) -} - -/// The status of a verb the endpoint does not serve for the request as it came: a GET or DELETE -/// that names no session (a GET naming no revision and accepting an event stream is the -/// `2024-11-05` stream instead), THE DESIGN section 2, the mcp bullet. -pub const STATUS_METHOD_NOT_ALLOWED: u32 = 405; - -/// The body of a GET or DELETE the endpoint does not serve: what each revision opens instead. -#[must_use] -pub fn method_not_allowed_body() -> Vec { - serde_json::to_vec(&serde_json::json!({ - "error": "method_not_allowed", - "error_description": - "A GET or DELETE here must name an MCP session (Mcp-Session-Id). MCP 2026-07-28 is \ - POST only; 2025-11-25 and 2025-06-18 open a session with a POSTed `initialize`; a \ - 2024-11-05 client opens its event stream with a GET that accepts text/event-stream \ - and names no MCP-Protocol-Version.", - })) - .unwrap_or_default() -} - -/// The status an arrival from a browser origin the deployment does not admit is refused with. -pub const STATUS_FORBIDDEN_ORIGIN: u32 = 403; - -/// THE DNS-REBINDING REFUSAL's body (the served engine's `ForbiddenOrigin` words, as its envelope -/// rendered them): an OAuth-style error object, not a JSON-RPC one. -#[must_use] -pub fn forbidden_origin_body() -> Vec { - serde_json::to_vec(&serde_json::json!({ - "error": "invalid_origin", - "error_description": - "This Origin is not allowed. Browser origins must be listed in mcp.allowed_origins.", - })) - .unwrap_or_default() -} - -/// THE BROWSER ORIGINS the deployment admits beyond loopback: the `mcp:` block's -/// `allowed_origins` (none when it writes no block). -/// -/// # Errors -/// -/// The block, written and malformed, in its grammar's words. -pub fn allowed_origins(owned: &[u8]) -> Result, String> { - if owned.is_empty() { - return Ok(Vec::new()); - } - let Some(block) = serde_json::from_slice::(owned) - .map_err(|e| e.to_string())? - .get(ENDPOINT_SECTION) - .cloned() - else { - return Ok(Vec::new()); - }; - let cfg: crate::endpoint::McpCfg = serde_json::from_value(block).map_err(|e| e.to_string())?; - Ok(cfg.allowed_origins) -} - -/// THE STATEMENT TAIL: the plane's static facts. -pub const TAIL: &PlaneTail = &PlaneTail { - head: KindTailHead { - size: std::mem::size_of::() as u32, - _reserved: 0, - }, - // THE GATE-FIRST HOOK ORDER (spec Part 3 section 12 "Hooks"; ARCHITECT ruling on the mcp - // fold): the served engine ran an entry's decision gates over the invocation BEFORE its - // rewrite chain (`tools.hooks` / `tools..hooks`), and the door's units keep that order. - flags: busbar_contract::abi::plane::TAIL_HOOKS_GATED, - ingress: INGRESS_REQUEST_RESPONSE | INGRESS_RESPONSE_STREAM, - dispatch_shape: SHAPE_PIECEWISE, - _reserved: 0, - scope: abi_str(SCOPE), - label: abi_str(LABEL), - subject_noun: abi_str(SUBJECT_NOUN), - admin_noun: abi_str(ADMIN_NOUN), - audit_kind: abi_str(AUDIT_KIND), - signing_domain: abi_str(ASK_SIGNING_DOMAIN), - signing_kid_prefix: abi_str(ASK_KID_PREFIX), - cli_help: abi_str(crate::tool_meta::HELP_FLAGS), - dialects: DIALECTS.as_ptr(), - dialects_len: DIALECTS.len(), - dialect_auth: std::ptr::null(), - dialect_auth_len: 0, - scope_kinds: SCOPE_KINDS.as_ptr(), - scope_kinds_len: SCOPE_KINDS.len(), - op_classes: OP_CLASS_TABLE.as_ptr(), - op_classes_len: OP_CLASS_TABLE.len(), - billable_classes: BILLABLE_CLASSES.as_ptr(), - billable_classes_len: BILLABLE_CLASSES.len(), - route_cost: std::ptr::null(), - route_cost_len: 0, - fee_units: FEE_UNITS.as_ptr(), - fee_units_len: FEE_UNITS.len(), - record_kinds: RECORD_KINDS.as_ptr(), - record_kinds_len: RECORD_KINDS.len(), - egress_targets: std::ptr::null(), - egress_targets_len: 0, - record_chains: RECORD_CHAINS.as_ptr(), - record_chains_len: RECORD_CHAINS.len(), - trust_keys: TRUST_KEYS.as_ptr(), - trust_keys_len: TRUST_KEYS.len(), - refusal_statuses: std::ptr::null(), - refusal_statuses_len: 0, - caller_credential_refusal: NONE, - admin_routes: ADMIN_ROUTES.as_ptr(), - admin_routes_len: ADMIN_ROUTES.len(), - admin_openapi: Blob { - ptr: ADMIN_OPENAPI.as_ptr(), - len: ADMIN_OPENAPI.len(), - fmt: busbar_contract::abi::mechanism::call::BLOB_JSON, - flags: 0, - }, - // No stream ceiling (ARCHITECT STREAM-CEILING, 2026-10-07): predev's mcp engine put no bound on - // a stalled consumer (its event and subscribe streams were pull-based bodies, `session_serve.rs`, - // `subscribe.rs`), so the mcp plane states none and keeps that behaviour. - stream_ceiling_secs: 0, -}; - -/// One path the plane answers on: the verb, the target, the transport claim it arrives over, and -/// whether the route admits a caller with no credential at all. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Route { - /// The verb. - pub verb: &'static str, - /// The target path. - pub target: &'static str, - /// The transport claim it arrives over. - pub carrier: &'static str, - /// No inbound credential: the discovery document. - pub open: bool, -} - -/// EVERY ROUTE THE PLANE SERVES, in the order the engine mounts them: the discovery document, the -/// endpoint's three verbs, and the event-framed answer on the same path. -pub const ROUTES: &[Route] = &[ - Route { - verb: "GET", - target: DEFAULT_METADATA, - carrier: CARRIER_HTTP, - open: true, - }, - Route { - verb: "POST", - target: DEFAULT_MOUNT, - carrier: CARRIER_HTTP, - open: false, - }, - Route { - verb: "GET", - target: DEFAULT_MOUNT, - carrier: CARRIER_HTTP, - open: false, - }, - Route { - verb: "DELETE", - target: DEFAULT_MOUNT, - carrier: CARRIER_HTTP, - open: false, - }, - Route { - verb: "POST", - target: DEFAULT_MOUNT, - carrier: CARRIER_SSE, - open: false, - }, - // THE TASK-RUN CLAIM (ARCHITECT round 5 Q-L3B-TASKS (b) → (A)): a task's continuation, nested - // by the unit that created the task. - Route { - verb: "POST", - target: TASK_RUN_MOUNT, - carrier: CARRIER_HTTP, - open: false, - }, - // THE LINE CARRIER (ARCHITECT Q1a, round 4 Q-L3B-STDIO-SHAPE (B)): the host holds the process's - // own stdin/stdout open as one carrier session and opens a unit per line. - Route { - verb: "POST", - target: DEFAULT_MOUNT, - carrier: CARRIER_STDIO, - open: false, - }, -]; - -/// The index of the task-run claim in [`ROUTES`]. -pub const TASK_RUN_ROUTE: usize = 5; - -/// The index of the line carrier's claim in [`ROUTES`]. -pub const LINE_ROUTE: usize = 6; - -/// The word each admin verb is audited under (`connect` reaches the operator's estate and can -/// quarantine a server, so it is audited; the two reads are not), in [`ADMIN_VERBS`] order. -pub const ADMIN_AUDIT: &[&str] = &["connect", "", ""]; - -/// THE TRUST VERBS' OpenAPI FRAGMENT, each path relative to the admin mount (the kernel keys it -/// under the mount): the served engine's text. Each verb's `200` names its typed body by `$ref`, -/// and the neutral `components.schemas` key carries those bodies (`McpTrustView`, the -/// `McpCapabilityView` it nests, `McpHealthView`) as data, since a door links no schema generator; -/// the kernel's fold inserts them into the admin document's `#/components/schemas` (ARCHITECT -/// Q2). They are the committed admin `openapi.json`'s entries verbatim, which a drift test holds. -pub const ADMIN_OPENAPI: &str = r##"{"/tools/{name}/connect":{"post":{"summary":"Fetch a registered MCP server's LIVE tool list, hash it, and record the observation. Approves nothing: adopting what was seen is a separate operator act","security":[{"adminToken":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK (the derived trust state and changes queue; a refresh that landed a quarantine is still a 200 — the drift is in the body)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpTrustView"}}}}}}},"/tools/{name}/changes":{"get":{"summary":"The changes queue for one MCP server, derived from the LAST observation. Contacts nothing","security":[{"adminToken":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpTrustView"}}}}}}},"/tools/{name}/health":{"get":{"summary":"Whether one MCP server currently serves, and why not when it does not","security":[{"adminToken":[]}],"parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpHealthView"}}}}}}},"components":{"schemas":{"McpTrustView":{"description":"THE TRUST VIEW of one registered MCP server — `POST /tools/{name}/connect` and\n`GET /tools/{name}/changes` answer with this.","properties":{"added":{"description":"Offered now, never ruled on.","items":{"type":"string"},"type":"array"},"capabilities":{"description":"Every capability's standing status, in name order.","items":{"$ref":"#/components/schemas/McpCapabilityView"},"type":"array"},"changed":{"description":"Approved, but offered at a DIFFERENT digest. THIS IS THE RUG-PULL ROW.","items":{"type":"string"},"type":"array"},"failure":{"description":"Why the last contact failed, when it did.","type":["string","null"]},"name":{"type":"string"},"observed_tools":{"description":"How many tools the last successful observation carried.","format":"uint","minimum":0,"type":"integer"},"pin_changed":{"description":"The presented identity is not the locked one. Its own axis: adopting a new identity is a\ndifferent act from adopting new content.","type":"boolean"},"pin_mechanism":{"description":"The operator's word for the authenticity root. Never interpreted by the machine.","type":"string"},"removed":{"description":"Approved, and no longer offered.","items":{"type":"string"},"type":"array"},"state":{"description":"`pending`, `approved`, `quarantined`, `suspended` or `error`. DERIVED from the approval and\nthe last sighting on every read, so there is no stored state to go stale.","type":"string"}},"required":["name","state","pin_mechanism","pin_changed","added","changed","removed","observed_tools","failure","capabilities"],"type":"object"},"McpCapabilityView":{"description":"ONE CAPABILITY'S STANDING STATUS, the three-way answer a trust view has to render.\n\nThree and not two: `approved at a digest`, `rejected`, and `allowed but never ruled on`. The\nthird is `pending` and it is the one a queue exists to drain, so collapsing it into \"not\napproved\" would hide the operator's whole worklist.","properties":{"approved_digest":{"description":"The digest the operator approved, when there is one. A rejected or pending capability has\nnone, and reporting the last OBSERVED digest here would read as an approval.","type":["string","null"]},"status":{"description":"`approved`, `rejected` or `pending`.","type":"string"},"tool":{"description":"The bare tool name, as the upstream spells it.","type":"string"}},"required":["tool","status","approved_digest"],"type":"object"},"McpHealthView":{"description":"THE HEALTH VIEW — `GET /tools/{name}/health`. Deliberately smaller than the trust view: health\nanswers \"can busbar use this server right now, and if not why\", and a dashboard polling it should\nnot be re-rendering the whole changes queue to find out.","properties":{"contacted":{"description":"`false` until a refresh has ever landed. A server that has never been contacted is not\nunhealthy, and reporting it as such would page somebody for a declarative deployment.","type":"boolean"},"failure":{"type":["string","null"]},"name":{"type":"string"},"observed_tools":{"format":"uint","minimum":0,"type":"integer"},"serving":{"description":"Whether this server currently SERVES: `state == approved`. Named separately from `state`\nbecause it is the one bit a caller usually wants and deriving it from a word is how a client\nends up hard-coding a spelling.","type":"boolean"},"state":{"type":"string"}},"required":["name","state","serving","contacted","failure","observed_tools"],"type":"object"}}}}"##; - -/// The trust verbs the plane serves on the kernel's admin mount, relative to it: `(verb, target)`. -pub const ADMIN_VERBS: &[(&str, &str)] = &[ - ("POST", "/tools/{name}/connect"), - ("GET", "/tools/{name}/changes"), - ("GET", "/tools/{name}/health"), -]; - -const fn admin_route(i: usize) -> AdminRoute { - AdminRoute { - verb: abi_str(ADMIN_VERBS[i].0), - target: abi_str(ADMIN_VERBS[i].1), - flags: 0, - _reserved: 0, - audit_verb: abi_str(ADMIN_AUDIT[i]), - } -} - -/// [`ADMIN_VERBS`] in the ABI's spelling. -pub const ADMIN_ROUTES: &[AdminRoute] = &[admin_route(0), admin_route(1), admin_route(2)]; - -/// `validate` and `open`: the settings blob read as the `tools:` section. An empty blob is the -/// empty section. The error is the grammar's own sentence. -/// -/// # Errors -/// -/// The first rule the section breaks, in the grammar's words. -pub fn read_tools_section(settings: &[u8]) -> Result { - if settings.is_empty() { - return Ok(ToolsCfg::default()); - } - let cfg = serde_json::from_slice::(settings).map_err(|e| e.to_string())?; - // The published names are judged over the whole section, as the kernel judges the effective - // registry (file and overlay): two servers publishing one name refuse it. - crate::tools_config::validate_published_names(&cfg)?; - Ok(cfg) -} - -/// ONE POOL of the section (the unified `pools:` its registrations resolved to, handed at the -/// section's reserved `pools` key): its interchangeable servers in the operator's order, the first -/// the primary, and the operations (bare tool names) it may perform twice. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct ToolPool { - /// The member servers, in order. - pub members: Vec, - /// The operations a member that answered with a failure may be repeated on another member for. - pub repeatable: Vec, -} - -/// `validate`: the blob stage 3g deals a plane (`{:
}` for each stated verb the -/// document writes; the kernel's own judges hand the same shape), read at the `tools:` section. An -/// empty blob, or one that writes no `tools:`, is the empty section; the `mcp:` endpoint block it may -/// carry is judged where it is lowered. -/// -/// # Errors -/// -/// A blob that is not a JSON object, or the `tools:` section's first broken rule. -pub fn read_dealt_tools(settings: &[u8]) -> Result { - if settings.is_empty() { - return Ok(ToolsCfg::default()); - } - let value: serde_json::Value = serde_json::from_slice(settings).map_err(|e| e.to_string())?; - let serde_json::Value::Object(mut verbs) = value else { - return Err("the dealt settings are not a map of the plane's sections".to_string()); - }; - match verbs.remove(crate::tools_config::SECTION) { - None | Some(serde_json::Value::Null) => Ok(ToolsCfg::default()), - Some(section) => { - read_tools_section(&serde_json::to_vec(§ion).map_err(|e| e.to_string())?) - } - } -} - -/// THE SETTINGS `open` and `refresh` are handed: the section ([`read_tools_section`]) and the pools the -/// kernel carries at its reserved `pools` key (ARCHITECT round 4 Q-L3B-SURFACES (h)). The operator's -/// own document never reaches this reader (`validate` reads the section as written). -/// -/// # Errors -/// -/// The section's refusal, or pools that are not `{name: {members: [..], repeatable: [..]}}`. -pub fn read_open( - settings: &[u8], -) -> Result<(ToolsCfg, std::collections::BTreeMap), String> { - if settings.is_empty() { - return Ok((ToolsCfg::default(), std::collections::BTreeMap::new())); - } - let mut value: serde_json::Value = - serde_json::from_slice(settings).map_err(|e| e.to_string())?; - let stated = value - .as_object_mut() - .and_then(|m| m.remove(busbar_contract::section::RESERVED_POOLS_KEY)); - let mut pools = std::collections::BTreeMap::new(); - if let Some(stated) = stated { - let map = stated - .as_object() - .ok_or("the section's pools are not a map")?; - let list = |v: Option<&serde_json::Value>| -> Result, String> { - v.map_or(Ok(Vec::new()), |v| { - serde_json::from_value::>(v.clone()).map_err(|e| e.to_string()) - }) - }; - for (name, pool) in map { - pools.insert( - name.clone(), - ToolPool { - members: list(pool.get(busbar_contract::section::POOL_MEMBERS_KEY))?, - repeatable: list(pool.get(busbar_contract::section::POOL_REPEATABLE_KEY))?, - }, - ); - } - } - let rest = serde_json::to_vec(&value).map_err(|e| e.to_string())?; - Ok((read_tools_section(&rest)?, pools)) -} - -/// ONE GENERATION'S SNAPSHOT, owned, for the SDK to publish until that generation's `retire`. -/// -/// With a readable `public_url` the plane is admitted: it states its audience (the endpoint's -/// absolute URI) and its protected-resource metadata document, and it claims its routes. Without -/// one it claims nothing: a plane with no public base has no audience a token could name. -#[must_use] -pub fn endpoint_snapshot_spec(public_url: Option<&str>) -> SnapshotSpec { - snapshot_spec_for(public_url.map(under_public_url)) -} - -/// The audience and metadata document of an endpoint mounted under the deployment's public base -/// URL: the fixed mount and its RFC 9728 document beneath that base. -#[must_use] -pub fn under_public_url(public_url: &str) -> (String, String) { - let base = public_url.trim_end_matches('/'); - ( - format!("{base}{DEFAULT_MOUNT}"), - format!("{base}{DEFAULT_METADATA}"), - ) -} - -/// THE ENDPOINT THE PLANE STATES ITS CLAIMS UNDER (ARCHITECT Q-L3B-AUD, predev's rule): its own -/// `mcp:` block's canonical URI when the document writes one (`owned`, the other sections it owns, -/// keyed by section name), else the deployment's public base URL; `None` when it states neither. -/// The block is judged by its grammar ([`crate::endpoint::McpResource::from_cfg`]): the audience is -/// the canonical URI verbatim and the metadata document the one it derives. -/// -/// # Errors -/// -/// The block, written and malformed, in its grammar's words. -pub fn admitted_endpoint( - public_url: Option<&str>, - owned: &[u8], -) -> Result, String> { - let block = if owned.is_empty() { - None - } else { - serde_json::from_slice::(owned) - .map_err(|e| e.to_string())? - .get(ENDPOINT_SECTION) - .cloned() - }; - match block { - Some(block) => { - let cfg: crate::endpoint::McpCfg = - serde_json::from_value(block).map_err(|e| e.to_string())?; - let resource = - crate::endpoint::McpResource::from_cfg(&cfg).map_err(|e| e.to_string())?; - Ok(Some(( - resource.canonical_uri().to_string(), - resource.metadata_url().to_string(), - ))) - } - None => Ok(public_url.map(under_public_url)), - } -} - -/// THE PROTECTED-RESOURCE FACTS its `mcp:` block states (ARCHITECT Q-L3B-RFC9728): its -/// authorization servers and scopes, as the JSON object `PlaneSnapshot::resource_facts` carries, from -/// which the kernel renders the RFC 9728 document; `None` when the document writes no block. -/// -/// # Errors -/// -/// The block, written and malformed, in its grammar's words. -pub fn resource_facts(owned: &[u8]) -> Result>, String> { - if owned.is_empty() { - return Ok(None); - } - let Some(block) = serde_json::from_slice::(owned) - .map_err(|e| e.to_string())? - .get(ENDPOINT_SECTION) - .cloned() - else { - return Ok(None); - }; - let cfg: crate::endpoint::McpCfg = serde_json::from_value(block).map_err(|e| e.to_string())?; - let resource = crate::endpoint::McpResource::from_cfg(&cfg).map_err(|e| e.to_string())?; - Ok(Some( - serde_json::json!({ - "authorization_servers": resource.authorization_servers(), - "scopes_supported": resource.scopes_supported(), - }) - .to_string() - .into_bytes(), - )) -} - -/// ONE GENERATION'S SNAPSHOT under `admitted` (the endpoint's audience and metadata document, -/// [`admitted`]); with none it claims nothing. -#[must_use] -pub fn snapshot_spec_for(admitted: Option<(String, String)>) -> SnapshotSpec { - snapshot_spec_with(admitted, None) -} - -/// [`snapshot_spec_for`], stating its protected-resource `facts` ([`resource_facts`]) beside. -#[must_use] -pub fn snapshot_spec_with( - admitted: Option<(String, String)>, - facts: Option>, -) -> SnapshotSpec { - let (audience, resource_metadata, claims) = match admitted { - Some((a, m)) => (Some(a), Some(m), ROUTES.iter().map(claim).collect()), - None => (None, None, Vec::new()), - }; - SnapshotSpec { - claims, - admin_routes: ADMIN_ROUTES - .iter() - .zip(ADMIN_VERBS) - .zip(ADMIN_AUDIT) - .map(|((r, (verb, target)), audit)| AdminRouteSpec { - audit_verb: (*audit).to_string(), - ..AdminRouteSpec::new(verb, target, r.flags) - }) - .collect(), - openapi: None, - resource_facts: facts.filter(|_| audience.is_some()), - audience, - resource_metadata, - listed: Vec::new(), - } -} - -/// A route as the snapshot's claim: an open route takes no inbound credential, and every target -/// matches exactly. -fn claim(r: &Route) -> ClaimSpec { - let flags = if r.open { - CLAIM_EXACT | CLAIM_OPEN - } else { - CLAIM_EXACT - }; - ClaimSpec::new(r.verb, r.target, r.carrier, flags) -} - -#[cfg(test)] -#[path = "tests/door.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/door_line.rs b/crates/busbar-plane-mcp/src/door_line.rs deleted file mode 100644 index 4939d66643..0000000000 --- a/crates/busbar-plane-mcp/src/door_line.rs +++ /dev/null @@ -1,442 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE LINE CARRIER'S UNITS (ARCHITECT Q1a, round 4 Q-L3B-STDIO-SHAPE (B)): the host holds the -//! process's own stdin/stdout open as ONE carrier session and opens a unit per line; what a line -//! MEANS is [`crate::line`]'s, and how a unit answers it is here. What the plane writes unsolicited -//! (busbar's own asks of its caller, a subscription's changes and keepalives) it writes with the -//! host's `session.emit` on the session every line names -//! ([`busbar_contract::abi::host::service::CARRIER_SESSION_FIELD`]). -//! -//! - A REQUEST goes on through the one dispatch under the fields its body implies; its answer is -//! one line. -//! - An `input_required` answer — busbar's OWN ask, or an UPSTREAM's ask relayed (Law 11) — is -//! LIVENED ([`liven`]): its first ask is emitted as busbar's own request (`busbar:`), the unit -//! answers nothing, and the caller's answers are lines of their own: each answer but the round's -//! last emits the next ask; the last is the RETRY, the request as the caller sent it with the -//! answers and the sealed state, dispatched as that unit (its own admission). An answer that is an -//! error hands the caller the `input_required` result itself. -//! - A SUBSCRIPTION (`subscriptions/listen`) is answered with its acknowledgement and kept on the -//! session ([`listen`]): the tick ([`tick`]) steps it, emitting what changed, a `ping` when it has -//! been quiet and its last frame at its bound. -//! - The STDIO-ERA VERBS are answered here, and a notification — or a line answering nothing busbar -//! asked — is answered with nothing at all. - -use std::collections::BTreeMap; - -use serde_json::Value; - -use super::{door_listen, CallUnit, McpDoor, Pending}; -use crate::line::{self, Era, LiveAsk, Reply}; -use crate::subscribe::{Listen, Standing, Step as ListenStep}; -use crate::tool_arrival::Disposition; -use busbar_contract::abi::mechanism::ticket::{CompletionHandle, Ticket}; - -/// What a line unit is to the carrier. -#[derive(Debug, Clone, Default)] -pub(super) struct LineUnit { - /// The carrier session it arrived over. - pub(super) session: u64, - /// The request this unit dispatches, as JSON: its own line, or the retry it became. - pub(super) original: Option, - /// What it answers without the dispatch: an era verb's answer, a failed ask's fallback, or - /// nothing at all (an empty answer is written as no line). - pub(super) preset: Option>, - /// The live round (by its ask number) whose next ask this unit emits. - pub(super) next_ask: Option, - /// The live round this unit's retry is: `0` for a request the caller sent itself. - pub(super) round: u32, - /// Its line was raised from the session revision its carrier session negotiated - /// ([`crate::adapt::raise`]): its answer is lowered back into it. - pub(super) raised: bool, -} - -/// One subscription kept on a carrier session. -#[derive(Debug, Clone)] -pub(super) struct LineListen { - listen: Box, - /// The entitlement answers it was opened under. - entitled: BTreeMap, -} - -/// One line as `arrive` read it: what the unit is to the carrier, and the bytes the one dispatch -/// decides on (`None` = the unit answers its preset, the dispatch decides nothing). -pub(super) struct Arrival { - pub(super) unit: LineUnit, - pub(super) dispatch: Option>, -} - -/// READ ONE LINE of carrier session `session`: an answer to one of busbar's own asks (its round -/// moved on, or its retry), a cancel, a stdio-era verb, or a request for the one dispatch. -pub(super) fn arrive(plane: &McpDoor, session: u64, body: &[u8]) -> Arrival { - let mut unit = LineUnit { - session, - ..LineUnit::default() - }; - let Ok(value) = serde_json::from_slice::(body) else { - return Arrival { - unit, - dispatch: Some(body.to_vec()), - }; - }; - if let Some(reply) = line::reply(&value) { - let n = match &reply { - Reply::Answered(n, _) | Reply::Failed(n) => *n, - }; - let Some(mut live) = plane.live_asks.remove(&(session, n)) else { - // An answer to nothing busbar asked on this session: nothing to say. - unit.preset = Some(Vec::new()); - return Arrival { - unit, - dispatch: None, - }; - }; - match reply { - Reply::Failed(_) => { - unit.preset = Some(live.fallback.clone()); - } - Reply::Answered(_, result) => { - live.answered(result); - if live.queued.is_empty() { - if let Some(retry) = live.retry() { - let dispatch = serde_json::to_vec(&retry).unwrap_or_default(); - unit.original = Some(retry); - unit.round = live.round.saturating_add(1); - return Arrival { - unit, - dispatch: Some(dispatch), - }; - } - unit.preset = Some(live.fallback.clone()); - } else { - plane.live_asks.insert((session, n), live); - unit.next_ask = Some(n); - unit.preset = Some(Vec::new()); - } - } - } - return Arrival { - unit, - dispatch: None, - }; - } - if let Some(id) = line::cancelled(&value) { - let key = line::id_key(&id); - plane.live_asks.with_all(|m| { - m.retain(|(s, _), live| { - *s != session || live.original.get("id").map(line::id_key) != Some(key.clone()) - }); - }); - } - match line::era(&value) { - Era::Dispatch => {} - Era::Opened(revision, answer) => { - // THE CARRIER SESSION NOW SPEAKS `revision` (revision by negotiation, THE DESIGN section 2). - // The table is bounded and evicts nothing (admission bounds live work): a full one - // answers the stateless revision, which needs no entry. - let kept = plane.line_revisions.with_all(|m| { - let full = m.len() >= super::MAX_UNITS && !m.contains_key(&session); - if !full { - m.insert(session, revision); - } - !full - }); - let answer = if kept { - answer - } else { - line::initialize_result(value.get("id").unwrap_or(&Value::Null)) - }; - unit.preset = Some(serde_json::to_vec(&answer).unwrap_or_default()); - return Arrival { - unit, - dispatch: None, - }; - } - Era::Answer(answer) => { - unit.preset = Some(serde_json::to_vec(&answer).unwrap_or_default()); - return Arrival { - unit, - dispatch: None, - }; - } - } - // A LINE OF A SESSION REVISION carries no stateless `_meta`: it is raised into the one - // dispatch's shape, and its answer lowered back ([`crate::adapt`]). - let stateless = value - .pointer("/params/_meta") - .and_then(|m| m.get(crate::codec::META_PROTOCOL_VERSION)) - .is_some(); - if !stateless && plane.line_revisions.get(&session).is_some() { - let mut raised = value.clone(); - if crate::adapt::raise(&mut raised).is_some() { - unit.raised = true; - let dispatch = serde_json::to_vec(&raised).unwrap_or_default(); - unit.original = Some(raised); - return Arrival { - unit, - dispatch: Some(dispatch), - }; - } - } - unit.original = Some(value); - Arrival { - unit, - dispatch: Some(body.to_vec()), - } -} - -/// Whether a line that the one dispatch refused is a notification: a notification is never -/// answered, refused or not. -pub(super) fn is_notification(body: &[u8]) -> bool { - serde_json::from_slice::(body) - .is_ok_and(|v| v.get("method").is_some_and(Value::is_string) && v.get("id").is_none()) -} - -/// The next number busbar spells one of its own requests in. -fn next_ask(plane: &McpDoor) -> u64 { - plane.ask_seq.with_all(|m| { - let n = m.entry(()).or_insert(0); - *n = n.saturating_add(1); - *n - }) -} - -/// `bytes` emitted on carrier session `session`, on a fresh handle of `ticket`: whether it went. -fn emit(plane: &McpDoor, ticket: Ticket, seq: u32, session: u64, bytes: &[u8]) -> bool { - let Some(services) = plane.services else { - return false; - }; - let handle = CompletionHandle { - ticket, - seq, - _reserved: 0, - }; - services.session_emit(handle, session, bytes).is_ok() -} - -/// THE PRESET ANSWER of a line unit, or the next ask of its round emitted (the unit then answers -/// nothing): `None` for a unit the one dispatch answers. -pub(super) fn preset(plane: &McpDoor, ticket: Ticket, unit: &mut CallUnit) -> Option { - let line_unit = unit.line.as_mut()?; - let session = line_unit.session; - if let Some(n) = line_unit.next_ask.take() { - let now = door_listen::mono_ns(plane.services, ticket, unit); - let line_unit = unit.line.as_mut()?; - if let Some(mut live) = plane.live_asks.remove(&(session, n)) { - let fresh = next_ask(plane); - if let Some(ask) = live.issue(fresh) { - live.arm(now); - // Kept before it is asked: its answer may arrive before the emit returns. - let fallback = live.fallback.clone(); - plane.live_asks.insert((session, fresh), live); - if !emit(plane, ticket, unit.issued, session, &ask) { - plane.live_asks.remove(&(session, fresh)); - line_unit.preset = Some(fallback); - } - } else { - // The next ask cannot be put to the caller: it is handed the result itself. - line_unit.preset = Some(live.fallback); - } - unit.issued += 1; - } - } - let line_unit = unit.line.as_mut()?; - let bytes = line_unit.preset.take()?; - Some(Pending::answer(200, bytes, None, &[])) -} - -/// LIVEN a line unit's answer: an `input_required` result (busbar's own ask, or an upstream's -/// relayed) is put to the caller as live requests on the session, its first ask emitted now, and -/// the unit answers nothing. Any other answer, or one that cannot be emitted, is left as it is. -pub(super) fn liven(plane: &McpDoor, ticket: Ticket, principal: &str, unit: &mut CallUnit) { - let Some(line_unit) = unit.line.as_ref() else { - return; - }; - let session = line_unit.session; - let round = line_unit.round; - // The round cap: past it the `input_required` result is handed to the caller as it is (its - // sealed `requestState` makes that a continuation), never put live again. - if !line::may_liven(round) { - return; - } - let Some(original) = line_unit.original.clone() else { - return; - }; - let Some(pending) = unit.pending.as_mut() else { - return; - }; - if pending.request.is_some() || pending.status != 200 || pending.bytes.is_empty() { - return; - } - let Ok(answer) = serde_json::from_slice::(&pending.bytes) else { - return; - }; - let Some(result) = answer.get("result") else { - return; - }; - if result.get("resultType").and_then(Value::as_str) - != Some(crate::jsonrpc::RESULT_TYPE_INPUT_REQUIRED) - { - return; - } - let Some(mut live) = LiveAsk::of(principal, original, pending.bytes.clone(), result, round) - else { - return; - }; - live.session = session; - live.arm(door_listen::mono_ns(plane.services, ticket, unit)); - let n = next_ask(plane); - let Some(ask) = live.issue(n) else { - return; - }; - let seq = unit.issued; - unit.issued += 1; - // KEPT BEFORE IT IS ASKED: the caller's answer is a line of its own, and may arrive before - // the emit returns. - plane.live_asks.insert((session, n), live); - if !emit(plane, ticket, seq, session, &ask) { - plane.live_asks.remove(&(session, n)); - return; - } - if let Some(pending) = unit.pending.as_mut() { - pending.bytes.clear(); - pending.fields.clear(); - } -} - -/// OPEN a subscription on the carrier session: refused in its own words, or answered with its -/// acknowledgement and kept on the session. `None` for a unit that is not a line's -/// `subscriptions/listen`. -pub(super) fn listen(plane: &McpDoor, ticket: Ticket, unit: &mut CallUnit) -> Option { - let session = unit.line.as_ref()?.session; - let Disposition::Request { row, id } = &unit.disposition else { - return None; - }; - if row.op != crate::tool_ops::OP_SUBSCRIPTIONS_LISTEN { - return None; - } - let id = id.clone(); - let held = plane.current().or_else(|| unit.held.clone())?; - door_listen::entitled_afresh(plane, ticket, unit, &held); - let now = door_listen::mono_ns(plane.services, ticket, unit); - let entitled = unit.entitled.clone(); - let admit = |kind: &str, name: &str| { - entitled - .get(&format!("{kind}:{name}")) - .copied() - .unwrap_or(false) - }; - let opened = Listen::open(unit.params.as_ref(), id.clone(), now, |uri| { - matches!( - held.catalogue.resource_by_uri(&admit, uri), - crate::catalogue::Lookup::One(_) - ) - }); - let mut listen = match opened { - Err(refusal) => { - let body = serde_json::to_vec(&refusal).unwrap_or_default(); - return Some(Pending::answer(400, body, None, &[])); - } - Ok(listen) => listen, - }; - let frames = match listen.step(&held.catalogue, &admit, &Standing::Live, &[], now) { - ListenStep::Frames(frames) | ListenStep::End(frames) => frames, - _ => Vec::new(), - }; - plane.line_listens.insert( - (session, line::id_key(&id)), - LineListen { - listen: Box::new(listen), - entitled, - }, - ); - Some(Pending::answer(200, lines(&frames), None, &[])) -} - -/// Frames as lines: one JSON-RPC message per line. -fn lines(frames: &[Value]) -> Vec { - let mut out = Vec::new(); - for frame in frames { - if !out.is_empty() { - out.push(b'\n'); - } - out.extend(serde_json::to_vec(frame).unwrap_or_default()); - } - out -} - -/// THE LAPSE of live asks: a round whose ask in flight went [`line::ASK_TIMEOUT_NS`] unanswered is -/// dropped and the caller is handed the `input_required` result itself, emitted on its session (the -/// unit that put the ask answered nothing, so the fallback is a line of its own). -fn lapse_asks(plane: &McpDoor, ticket: Ticket, now_ns: u64, seq: &mut u32) { - let lapsed: Vec<(u64, Vec)> = plane.live_asks.with_all(|m| { - let keys: Vec<(u64, u64)> = m - .iter() - .filter(|(_, live)| live.lapsed(now_ns)) - .map(|(key, _)| *key) - .collect(); - keys.iter() - .filter_map(|key| m.remove(key).map(|live| (key.0, live.fallback))) - .collect() - }); - for (session, mut fallback) in lapsed { - fallback.push(b'\n'); - emit(plane, ticket, *seq, session, &fallback); - *seq = seq.wrapping_add(1); - } -} - -/// THE TICK, for the subscriptions kept on carrier sessions: each stepped over the live catalogue, -/// what it says emitted on its session (a `ping` when it has been quiet), and a subscription whose -/// session can take nothing more, or that ended, dropped. -pub(super) fn tick(plane: &McpDoor, ticket: Ticket, now_ns: u64) { - let mut seq: u32 = 0; - lapse_asks(plane, ticket, now_ns, &mut seq); - let Some(held) = plane.current() else { - return; - }; - let gone = plane.line_listens.with_all(|m| { - let mut gone = Vec::new(); - for ((session, key), kept) in m.iter_mut() { - let entitled = &kept.entitled; - let admit = |kind: &str, name: &str| { - entitled - .get(&format!("{kind}:{name}")) - .copied() - .unwrap_or(false) - }; - let (bytes, ended) = - match kept - .listen - .step(&held.catalogue, &admit, &Standing::Live, &[], now_ns) - { - ListenStep::Frames(frames) => (lines(&frames), false), - ListenStep::Keepalive => { - let n = next_ask(plane); - ( - serde_json::to_vec(&serde_json::json!({ - "jsonrpc": "2.0", - "id": format!("{}{n}", line::ASK_ID_PREFIX), - "method": crate::adapt::METHOD_PING, - })) - .unwrap_or_default(), - false, - ) - } - ListenStep::Quiet => (Vec::new(), false), - ListenStep::End(frames) => (lines(&frames), true), - ListenStep::Closed => (Vec::new(), true), - }; - let went = bytes.is_empty() || emit(plane, ticket, seq, *session, &bytes); - seq = seq.wrapping_add(1); - if ended || !went { - gone.push((*session, key.clone())); - } - } - for key in &gone { - m.remove(key); - } - gone - }); - let _ = gone; -} diff --git a/crates/busbar-plane-mcp/src/door_listen.rs b/crates/busbar-plane-mcp/src/door_listen.rs deleted file mode 100644 index 580a675440..0000000000 --- a/crates/busbar-plane-mcp/src/door_listen.rs +++ /dev/null @@ -1,475 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! `subscriptions/listen` ON THE HTTP CARRIER: A K6 SESSION (ARCHITECT round 4 Q-L3B-SURFACES (a), -//! confirmed round 5 Q-L3B-K6-HTTP (a)). `arrive` states `ROUTE_SESSION` for it, so the driver -//! serves the unit as a duplex session whose caller leg is the long-lived response; every rule of -//! the subscription is [`crate::subscribe`]'s, the same state machine the line carrier drives. -//! -//! - THE CALLER'S PIECE (the request's body, read once) OPENS the subscription ([`Listen::open`]): -//! a request that can deliver nothing is refused `400` with its JSON-RPC error; one that opens is -//! answered `200` as an event stream (`content-type: text/event-stream`, -//! `cache-control: no-cache, no-store`) whose first event is the acknowledgement. -//! - THE TICK ([`tick`]) is the subscription's clock: every [`POLL_NS`] each held subscription is -//! named on the instance's driver ticket (`drive`, [`drive`]), and the session collects -//! (`FROM_KERNEL`): one step, its permission re-asked through the kernel's live re-resolution -//! ([`standing`]), a change frame when the catalogue its caller can see moved, a -//! keepalive comment (`: keepalive`) when it has been quiet for -//! [`crate::subscribe::KEEPALIVE_NS`], and its last frame at its bound or on a lapsed permission, -//! after which the session is done. A new generation (`refresh`) names them at once ([`wake_all`]). -//! - Every frame is one `message` event (`event: message`, `data: `), as predev wrote them. -//! - THE SESSION'S ONE LINE carries the request's fee unit, reported with the acknowledgement. - -use super::{ask_entitlements, write, CallUnit, Held, McpDoor, Pending, Written, MAX_UNITS}; -use crate::catalogue::Lookup; -use crate::framing::{events, CACHE_CONTROL, CONTENT_TYPE, EVENT_STREAM, NO_STORE}; -use crate::subscribe::{Listen, Standing, Step as ListenStep, KEEPALIVE_NS, MAX_LIFETIME_NS}; -use crate::tool_arrival::Disposition; -use busbar_contract::abi::mechanism::call::Outcome; -use busbar_contract::abi::mechanism::ticket::{CompletionHandle, Ticket}; -use busbar_contract::abi::plane::{ - OnPieceIn, OnPieceOut, PlaneDriveIn, PlaneDriveOut, UnitCount, FROM_CALLER, FROM_KERNEL, - PIECE_LAST, UNITS_REPORTED, -}; -use busbar_contract::abi::sdk::{Lent, Out, Services}; -use serde_json::Value; - -/// How often the instance's tick steps its held subscriptions (predev re-read the generation and -/// re-asked the permission every 250 ms of a stream's life). -pub(super) const POLL_NS: u64 = 250_000_000; - -/// The most subscriptions one caller holds open at once. A caller at it is refused its next one: -/// a quota is a refusal its caller can act on (close one, then open), where an eviction of another -/// caller's stream is not. -pub(super) const MAX_LISTENS_PER_OWNER: usize = 64; - -/// What an idle stream writes to say it is alive: an event-stream comment, which every reader -/// drops, so an intermediary does not reclaim a working connection. -const KEEPALIVE: &[u8] = b": keepalive\n\n"; - -/// The status an invalid `subscriptions/listen` is refused with. -const STATUS_INVALID: u32 = 400; - -/// The status a `subscriptions/listen` is refused with when its caller is at its quota. -const STATUS_QUOTA: u32 = 429; - -/// One held subscription, by its session's stream. -#[derive(Debug, Clone)] -pub(super) struct Listening { - /// The caller that opened it, by the reference the kernel lends every piece of its units (the - /// one the quota counts under). - owner: String, - /// The subscription. - listen: Box, - /// The session's caller-side ticket. - ticket: Ticket, - /// The tick clock's reading at its first tick (`0` before one): a subscription its session - /// never ended (its caller went away while it was idle, which nothing tells the door) is dropped - /// once it is past its bound. - first_tick: u64, - /// The tick asked for a step it has not collected. - due: bool, - /// The stream's head is written. - headed: bool, -} - -/// A frame of the stream: its head (status, fields and the request's fee unit) with the first. -pub(super) fn frame(bytes: Vec, done: bool, headed: bool) -> Pending { - let (fields, units) = if headed { - (Vec::new(), Vec::new()) - } else { - ( - vec![ - (CONTENT_TYPE.to_string(), EVENT_STREAM.to_string()), - (CACHE_CONTROL.to_string(), NO_STORE.to_string()), - ], - vec![UnitCount { - class: crate::door::CLASS_FEE_INDEX, - source: UNITS_REPORTED, - amount: 1, - }], - ) - }; - Pending { - status: 200, - fields, - request: None, - bytes, - sent: 0, - headed, - done, - records: Vec::new(), - audits: Vec::new(), - lane: None, - units, - } -} - -/// ONE PIECE OF A LISTEN SESSION (`stream != 0`): the caller's piece opens it, a collection steps -/// it, and the caller's side ending ends it. A re-call after `more = 1` writes what is pending. -pub(super) fn piece( - plane: &McpDoor, - input: Lent<'_, OnPieceIn>, - out: &mut Out<'_, OnPieceOut>, -) -> Outcome { - let given = input.get(); - let (key, ticket) = (given.unit, given.head.ticket); - let caller = input - .field(|i| &i.caller_ref) - .as_str() - .unwrap_or_default() - .to_string(); - let principal = if caller.is_empty() { - crate::ask::UNGOVERNED - } else { - caller.as_str() - }; - let step = plane.units.with(&key, |unit| { - let unit = unit?; - unit.ticket = Some(ticket); - if unit.pending.is_some() { - return Some(true); - } - match given.from { - // The caller went away: the session ends with nothing more to say. - FROM_CALLER if given.flags & PIECE_LAST != 0 => { - unit.pending = Some(frame(Vec::new(), true, true)); - Some(true) - } - FROM_CALLER => open(plane, ticket, principal, unit), - FROM_KERNEL if given.attempt_no == 0 => { - if plane.listens.with(&key, |l| l.is_none()) { - return Some(false); - } - step(plane, ticket, principal, unit) - } - // A listen session dials nothing: an ATTEMPT carries nothing. - FROM_KERNEL => Some(false), - _ => None, - } - }); - match step { - None => Outcome::Refused, - Some(false) => Outcome::Ready, - Some(true) => { - let written = plane.units.with(&key, |unit| { - let pending = unit?.pending.as_mut()?; - Some(write(&input, out, pending)) - }); - match written { - None | Some(Err(())) => Outcome::Failed, - Some(Ok(Written::More)) => Outcome::Ready, - Some(Ok(Written::Sent)) => { - plane.units.with(&key, |unit| { - if let Some(unit) = unit { - unit.pending = None; - } - }); - Outcome::Ready - } - Some(Ok(Written::Done)) => { - plane.units.remove(&key); - plane.listens.remove(&key); - Outcome::Ready - } - } - } - } -} - -/// OPEN the subscription the unit's request asks for: refused `400` in its own words, or held and -/// stepped once (the acknowledgement). `None` for a unit that is not a `subscriptions/listen`. -fn open(plane: &McpDoor, ticket: Ticket, principal: &str, unit: &mut CallUnit) -> Option { - let Disposition::Request { row, id } = &unit.disposition else { - return None; - }; - if row.op != crate::tool_ops::OP_SUBSCRIPTIONS_LISTEN { - return None; - } - if plane.listens.with(&unit.key, |l| l.is_some()) { - return Some(false); - } - let id = id.clone(); - let held = plane.current().or_else(|| unit.held.clone())?; - entitled_afresh(plane, ticket, unit, &held); - let now = mono_ns(plane.services, ticket, unit); - let opened = { - let entitled = &unit.entitled; - let admit = |kind: &str, name: &str| { - entitled - .get(&format!("{kind}:{name}")) - .copied() - .unwrap_or(false) - }; - Listen::open(unit.params.as_ref(), id, now, |uri| { - matches!(held.catalogue.resource_by_uri(&admit, uri), Lookup::One(_)) - }) - }; - match opened { - Err(refusal) => { - let body = serde_json::to_vec(&refusal).unwrap_or_default(); - unit.pending = Some(Pending::answer(STATUS_INVALID, body, None, &[])); - Some(true) - } - Ok(listen) => { - let id = listen.id.clone(); - let listening = Listening { - owner: principal.to_string(), - listen: Box::new(listen), - ticket, - first_tick: 0, - due: false, - headed: false, - }; - // THE QUOTA REFUSES THE NEW STREAM, never evicts a held one: this caller at its own - // bound, or the instance at its total, is told so. - let admitted = plane.listens.with_all(|held| { - let full = held.len() >= MAX_UNITS - || held.values().filter(|l| l.owner == principal).count() - >= MAX_LISTENS_PER_OWNER; - if !full { - held.insert(unit.key, listening); - } - !full - }); - if !admitted { - let refusal = crate::tool_arrival::Refusal { - status: STATUS_QUOTA, - id: Some(id), - code: crate::codec::CODE_REFUSED, - message: format!( - "this caller holds {MAX_LISTENS_PER_OWNER} open subscriptions, the most one \ - caller may hold; close one and open again" - ), - data: Some(serde_json::json!({ "reason": "subscription_quota" })), - }; - unit.pending = Some(Pending::answer(refusal.status, refusal.body(), None, &[])); - return Some(true); - } - step(plane, ticket, principal, unit) - } - } -} - -/// ONE STEP of the unit's subscription over the live catalogue, its permission re-asked: what it -/// writes, if anything. `Some(false)` when it has nothing to say. -fn step(plane: &McpDoor, ticket: Ticket, principal: &str, unit: &mut CallUnit) -> Option { - let held = plane.current().or_else(|| unit.held.clone())?; - entitled_afresh(plane, ticket, unit, &held); - let now = mono_ns(plane.services, ticket, unit); - let standing = standing(plane, ticket, principal, unit); - let entitled = &unit.entitled; - let admit = |kind: &str, name: &str| { - entitled - .get(&format!("{kind}:{name}")) - .copied() - .unwrap_or(false) - }; - let (stepped, headed) = plane.listens.with(&unit.key, |l| { - let l = l?; - l.due = false; - let stepped = l.listen.step(&held.catalogue, &admit, &standing, &[], now); - Some((stepped, l.headed)) - })?; - let (bytes, done) = match stepped { - ListenStep::Frames(frames) => (events(&frames), false), - ListenStep::Keepalive => (KEEPALIVE.to_vec(), false), - ListenStep::Quiet => return Some(false), - ListenStep::End(frames) => (events(&frames), true), - ListenStep::Closed => (Vec::new(), true), - }; - // The head goes with the stream's first frame (its acknowledgement, or the last frame of a - // stream whose permission lapsed before it). - plane.listens.with(&unit.key, |l| { - if let Some(l) = l { - l.headed = true; - } - }); - unit.pending = Some(frame(bytes, done, headed)); - Some(true) -} - -/// THE TICK: the instance's driver ticket and the tick clock noted, every held subscription asked -/// for a step (a subscription past its bound that never ended is dropped), and the driver ticket -/// woken when any is held. The next tick is [`POLL_NS`] on. -pub(super) fn tick(plane: &McpDoor, driver: Ticket, now_ns: u64) -> u64 { - plane.driver.insert((), (driver, now_ns)); - let stale = MAX_LIFETIME_NS.saturating_add(KEEPALIVE_NS); - let (gone, any) = plane.listens.with_all(|m| { - let mut gone = Vec::new(); - for (key, l) in m.iter_mut() { - if l.first_tick == 0 { - l.first_tick = now_ns.max(1); - } - if now_ns.saturating_sub(l.first_tick) > stale { - gone.push(*key); - } - l.due = true; - } - for key in &gone { - m.remove(key); - } - (gone, !m.is_empty()) - }); - for key in gone { - plane.units.remove(&key); - } - if any { - wake_driver(plane); - } - now_ns.saturating_add(POLL_NS) -} - -/// A new generation: every held subscription is asked for a step at once. -pub(super) fn wake_all(plane: &McpDoor) { - let any = plane.listens.with_all(|m| { - for l in m.values_mut() { - l.due = true; - } - !m.is_empty() - }); - if any { - wake_driver(plane); - } -} - -/// `drive`: the sessions whose step is due, by stream, as many as the host's buffer takes; each one -/// named collects on its caller-side ticket. -pub(super) fn drive( - plane: &McpDoor, - input: Lent<'_, PlaneDriveIn>, - out: &mut Out<'_, PlaneDriveOut>, -) -> Outcome { - let mut due: Vec = plane.listens.with_all(|m| { - m.iter() - .filter(|(_, l)| l.due) - .map(|(stream, _)| *stream) - .collect() - }); - // The session revisions' held streams owe their collections on the same driver ticket. - let streams = super::door_sessions::due_streams(plane); - due.extend(&streams); - let mut buf = input.sessions_buf(); - for stream in &due { - buf.push(*stream); - } - if !buf.fits() { - out.set(|o| &o.sessions_needed, buf.needed() as u32); - return Outcome::Failed; - } - out.set(|o| &o.sessions_written, buf.written() as u32); - plane.listens.with_all(|m| { - for stream in &due { - if let Some(l) = m.get_mut(stream) { - l.due = false; - } - } - }); - super::door_sessions::taken(plane, &streams); - Outcome::Ready -} - -/// The op on `ticket` was cancelled: the subscription it carried is dropped. -pub(super) fn cancelled(plane: &McpDoor, ticket: Ticket) { - plane - .listens - .with_all(|m| m.retain(|_, l| l.ticket != ticket)); -} - -/// Name the instance's driver ticket (as its last tick handed it): a held subscription owes a step. -pub(super) fn wake_driver(plane: &McpDoor) { - let driver = plane.driver.get(&()).map(|(ticket, _)| ticket); - if let (Some(wake), Some(ticket)) = (plane.wake, driver) { - if ticket != Ticket::NONE { - wake.wake(ticket); - } - } -} - -// ── what a held unit re-asks each frame, and the round busbar's own ask costs ────────────────── - -/// Whether a request asks busbar's own round before it is answered: a `prompts/get` of a prompt -/// whose operator asks its caller first, presenting no answers yet (the served engine charged that -/// round on the caller's budget). -pub(super) fn asks_a_round( - op: busbar_contract::ids::OpClassId, - held: &Held, - value: &Value, -) -> bool { - if op != crate::tool_ops::OP_PROMPT_GET { - return false; - } - let params = value.get("params"); - let retried = params - .is_some_and(|p| p.get("inputResponses").is_some() || p.get("requestState").is_some()); - let asks = params - .and_then(|p| p.get("name")) - .and_then(Value::as_str) - .and_then(|name| held.catalogue.prompt_for(&|_: &str, _: &str| true, name)) - .is_some_and(|p| !p.ask_caller.is_empty()); - asks && !retried -} - -/// The host's monotonic clock on a fresh handle of the unit's ticket; `0` with no clock. -pub(super) fn mono_ns(services: Option, ticket: Ticket, unit: &mut CallUnit) -> u64 { - let Some(services) = services else { - return 0; - }; - let handle = CompletionHandle { - ticket, - seq: unit.issued, - _reserved: 0, - }; - unit.issued += 1; - services.clock_now(handle).map_or(0, |r| r.mono_ns) -} - -/// The catalogue's every grant, entitled afresh for this frame: a held unit re-asks what its caller -/// may see each time it looks. -pub(super) fn entitled_afresh(plane: &McpDoor, ticket: Ticket, unit: &mut CallUnit, held: &Held) { - unit.entitled.clear(); - ask_entitlements(plane.services, ticket, unit, &held.catalogue.grants()); -} - -/// Whether the unit's principal still stands: the kernel's live re-resolution -/// (`entitlement.check` of [`busbar_contract::abi::host::service::ENTITLEMENT_STANDING`]). A host -/// that serves no entitlement stands nothing. -fn stands(plane: &McpDoor, ticket: Ticket, unit: &mut CallUnit) -> bool { - let Some(services) = plane.services else { - return false; - }; - let handle = CompletionHandle { - ticket, - seq: unit.issued, - _reserved: 0, - }; - unit.issued += 1; - services - .entitled( - handle, - busbar_contract::abi::host::service::ENTITLEMENT_STANDING, - ) - .unwrap_or(false) -} - -/// THE PERMISSION, RE-ASKED THIS FRAME: the kernel re-resolves the unit's principal live -/// ([`stands`]); one that no longer stands is lapsed, in predev's words, and ends the subscription on -/// this frame (on either carrier). -pub(super) fn standing( - plane: &McpDoor, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, -) -> Standing { - if stands(plane, ticket, unit) { - Standing::Live - } else { - Standing::Lapsed { - message: format!( - "`{principal}` is no longer live, so the standing decision it was admitted under \ - no longer applies" - ), - reason: busbar_contract::vocab::REASON_IDENTITY_NOT_LIVE.to_string(), - } - } -} diff --git a/crates/busbar-plane-mcp/src/door_program.rs b/crates/busbar-plane-mcp/src/door_program.rs deleted file mode 100644 index 0d69dee9bb..0000000000 --- a/crates/busbar-plane-mcp/src/door_program.rs +++ /dev/null @@ -1,609 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE STDIO SERVERS' LEG, in the door (ARCHITECT round 5 Q-L3B-STDIO-UPSTREAM (A)): a -//! `transport: stdio` registration is a member of the program need ([`door::NEED_PROGRAM`]); the -//! host keeps one long-lived child per member, and every exchange the door has with it is -//! correlated by id ([`crate::tool_program`]). -//! -//! * Before a call is relayed to a stdio member, its child's generation is GREETED once -//! ([`ready`]): an exchange that only opens a lease, reads the generation and, for one the door -//! has not greeted, runs `initialize` and its acknowledgement. -//! * The relayed `tools/call` goes over the kernel's walk on the member's route like any other -//! member's; its answer is the message carrying the unit's id among everything the child writes -//! ([`far`]), and a request of the child's own read on the way is answered on a lease of its own -//! (`ping`, an unknown method, an ungranted ask) or, a granted authority ask, handed up as busbar's -//! caller's to answer ([`Far::Asked`], Law 11) — the caller of the call FIRST IN THE CHILD'S -//! LINE ([`Relaying`]): calls to a member whose grants let its asks be relayed reach its child one -//! at a time, in arrival order, each holding its place until its answer is read (across a relayed -//! ask's round trip, until its retry or the ask's state lapses); a call behind it waits its turn -//! within its own deadline ([`in_line`]). An ask raised while no such call is open is refused. The -//! caller's answer comes back on a retry of the call ([`ProgramRelay::retrying`]): written to the -//! child under its own request id, and the call the child still owes read on. -//! * Verify-on-call's `tools/list` ([`tools_listed`]), an operator's `connect`, and a further round reach the -//! same child the same way. - -use super::*; -use crate::tool_program::{ - call_id, first_in_line, list_id, AskOwner, Correlator, Exchanged, Peer, ProgramExchange, -}; - -/// THE LINE OF CALLS RELAYED TO STDIO CHILDREN, by serial (arrival order) (finding 5): every -/// relayed call is entered before its request could reach the kernel's walk and leaves when its -/// relay is dropped (or, holding its place across a relayed ask's round trip, when that lapses). -/// For a member whose asks may be relayed, only the call first in its line is sent ([`in_line`]), -/// so the child serves one such call at a time and its ask is that call's ([`first_in_line`]). -pub(super) type Relaying = Arc>; - -/// One call in a stdio member's line. -#[derive(Debug, Clone)] -pub(super) struct InLine { - member: String, - /// The generation its lease reached; `0` before its head. - generation: u64, - /// The id the call carries on the child. - call: u64, - /// The ticket of its unit while it waits its turn: woken when the call ahead of it leaves. - ticket: Option, - /// Held across a relayed ask's round trip until this instant of the host's monotonic clock - /// (ms): the child still serves the call, so its place is kept for the retry. `0` = a live - /// relay. - held_until: u64, -} - -/// Whether member `def`'s calls wait their turn on its child: its grants let a child's ask be -/// relayed to a caller (a call whose asks are all refused may share the child freely). -pub(super) fn serialised(def: &crate::tools_config::McpServerDefCfg) -> bool { - let g = def.grants.as_ask_grants(); - g.sampling || g.elicitation || g.roots -} - -/// The sentence a call answers when its deadline passed while another call held the child. -pub(super) const BUSY: &str = "the stdio MCP child was serving another call whose asks are \ - relayed to its own caller, and this call's deadline passed while it \ - waited its turn; it was never sent"; - -/// Where the door's own program exchanges number their host services on a unit's ticket: the -/// greeting before each attempt, [`crate::tool_program::EXCHANGE_SERVICES`] apart per attempt. -const READY_SEQ: u32 = 1 << 31; - -/// Where the replies a relayed call owes its child number theirs: one exchange per reply batch, -/// [`crate::tool_program::EXCHANGE_SERVICES`] apart. -const REPLY_SEQ: u32 = (1 << 31) + (1 << 30); - -/// The most requests of a child's own the door remembers having answered or put to a call (per -/// member and generation, by id), so two exchanges reading one request decide it once. -const MAX_ANSWERED: usize = 4096; - -/// Whether `def` is a `transport: stdio` registration: a member of the program need. -pub(super) fn is_program(def: &crate::tools_config::McpServerDefCfg) -> bool { - def.transport - .is_some_and(crate::tools_config::ServerTransport::spawns_child) -} - -/// What a door exchange is doing with a unit's program lease. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum Purpose { - /// Greeting the member's child before the attempt's call. - Ready, - /// Verify-on-call's `tools/list`. - Verify, -} - -/// A relayed call to a stdio member, as its far pieces arrive. -pub(super) struct ProgramRelay { - /// The id the round in flight carries. - pub(super) wait: u64, - /// The generation of the child the walk's lease reached. - pub(super) generation: u64, - corr: Correlator, - /// The exchange writing the replies the child is owed, while it pends. - replying: Option, - /// How many reply exchanges this call made. - replies: u32, - /// The answer (or why there is none), held while the replies are written. - settled: Option, String>>, - /// A retry answering the child's own requests: the generation that asked, which is the only one - /// its answers and its wait mean anything to. - expect: Option, - /// Its place in the line ([`Relaying`]), left when it is dropped. - open: (Relaying, u64), - /// The host's wake, for the call behind it when it leaves the line. - wake: Option, - /// Its asks went to its caller: dropped, it holds its place until this instant (ms). - hold: u64, -} - -impl Drop for ProgramRelay { - fn drop(&mut self) { - let (line, serial) = &self.open; - let hold = self.hold; - let next = line.with_all(|m| { - if hold != 0 { - if let Some(e) = m.get_mut(serial) { - e.held_until = hold; - } - return None; - } - let gone = m.remove(serial)?; - if m.range(..*serial).any(|(_, e)| e.member == gone.member) { - return None; - } - m.values().find(|e| e.member == gone.member)?.ticket - }); - if let (Some(wake), Some(ticket)) = (self.wake, next) { - wake.wake(ticket); - } - } -} - -impl ProgramRelay { - /// The relay of a call to member `member` whose round carries `wait`: entered at the end of - /// the member's line. - pub(super) fn waiting(plane: &McpDoor, member: &str, wait: u64) -> Self { - let serial = plane.relaying.with_all(|line| { - let serial = line.last_key_value().map_or(1, |(k, _)| k.wrapping_add(1)); - line.insert( - serial, - InLine { - member: member.to_string(), - generation: 0, - call: wait, - ticket: None, - held_until: 0, - }, - ); - serial - }); - ProgramRelay::at(plane, wait, serial) - } - - fn at(plane: &McpDoor, wait: u64, serial: u64) -> Self { - ProgramRelay { - wait, - generation: 0, - corr: Correlator::relaying(), - replying: None, - replies: 0, - settled: None, - expect: None, - open: (Arc::clone(&plane.relaying), serial), - wake: plane.wake, - hold: 0, - } - } - - /// Its asks went to its caller: dropped, it keeps its place in the line until `until` (the - /// host's monotonic clock, ms), for the retry that answers them. - pub(super) fn hold(&mut self, until: u64) { - self.hold = until; - } - - /// THE RETRY OF A CHILD'S RELAYED ASK: the caller's answers after the first (which the walk's - /// own lease carries) owed on the child of `generation`, and the call `wait` it still owes read - /// on, on that generation only. It takes up the place in the line the call held while its - /// caller answered. - pub(super) fn retrying( - plane: &McpDoor, - member: &str, - wait: u64, - generation: u64, - rest: Vec>, - ) -> Self { - let held = plane.relaying.with_all(|line| { - let (serial, e) = line - .iter_mut() - .find(|(_, e)| e.held_until != 0 && e.member == member && e.call == wait)?; - e.held_until = 0; - Some(*serial) - }); - let mut relay = match held { - Some(serial) => ProgramRelay::at(plane, wait, serial), - None => ProgramRelay::waiting(plane, member, wait), - }; - relay.expect = Some(generation); - relay.corr.outbox.extend(rest); - relay - } -} - -/// The sentence a retry fails with when the child that asked is gone. -pub(super) const RESTARTED: &str = "the stdio MCP child that asked was restarted before the \ - caller's answer reached it, so the call it was serving is gone"; - -/// WHETHER `relay`'s CALL IS FIRST IN ITS MEMBER'S LINE, so may be sent: a place held across a -/// relayed ask's round trip that lapsed by `now` (the host's monotonic clock, ms) is given up first. -/// One that is not waits its turn, its unit's `ticket` woken when the call ahead of it leaves. -pub(super) fn in_line( - plane: &McpDoor, - relay: &ProgramRelay, - now: Option, - ticket: Ticket, -) -> bool { - let serial = relay.open.1; - plane.relaying.with_all(|line| { - let Some(member) = line.get(&serial).map(|e| e.member.clone()) else { - return true; - }; - if let Some(now) = now { - line.retain(|_, e| e.member != member || e.held_until == 0 || e.held_until > now); - } - let first = line - .iter() - .find(|(_, e)| e.member == member) - .is_none_or(|(s, _)| *s == serial); - if let Some(e) = line.get_mut(&serial).filter(|_| !first) { - e.ticket = Some(ticket); - } - first - }) -} - -/// The door, as an exchange with member `member`'s child needs it. -struct DoorPeer<'a> { - plane: &'a McpDoor, - member: &'a str, - grants: crate::client::jsonrpc::ServerRequestGrants, -} - -impl DoorPeer<'_> { - /// What became of the child's request `id` of `generation`: the call it was put to (`None` = - /// busbar answered it), decided by `first` when this exchange is the first to read it (`true`). - fn decide( - &self, - generation: u64, - id: &Value, - first: impl FnOnce() -> Option, - ) -> (Option, bool) { - let key = (self.member.to_string(), generation, id.to_string()); - self.plane.answered.with_all(|m| { - if let Some(owner) = m.get(&key) { - return (*owner, false); - } - while m.len() >= MAX_ANSWERED { - if m.pop_first().is_none() { - break; - } - } - let owner = first(); - m.insert(key, owner); - (owner, true) - }) - } -} - -impl Peer for DoorPeer<'_> { - fn grants(&self) -> crate::client::jsonrpc::ServerRequestGrants { - self.grants - } - - fn claim(&mut self, generation: u64, id: &Value) -> bool { - self.decide(generation, id, || None).1 - } - - fn owner(&mut self, generation: u64, id: &Value) -> AskOwner { - let member = self.member; - let line = &self.plane.relaying; - match self.decide(generation, id, || { - line.with_all(|l| { - first_in_line( - l.values() - .filter(|e| e.member == member) - .map(|e| (e.call, e.generation)), - generation, - ) - }) - }) { - (Some(call), _) => AskOwner::Call(call), - (None, true) => AskOwner::Refuse, - (None, false) => AskOwner::Refused, - } - } - - fn notice(&mut self) { - // The child's lists moved: the next call re-verifies (the timing, never the content). - self.plane.checked.remove(&self.member.to_string()); - } - - fn announced(&mut self, uri: &str) { - // The child's resource changed: every session watching it hears so. - super::door_sessions::announce(self.plane, self.member, uri); - } -} - -/// Drive `exchange` with member `member` on `ticket` from `base`: the door's greeting record kept. -fn exchange_child( - plane: &McpDoor, - host: &busbar_contract::abi::sdk::conn::Host, - ticket: Ticket, - base: u32, - member: &str, - def: &crate::tools_config::McpServerDefCfg, - exchange: &mut ProgramExchange, -) -> std::task::Poll> { - let greeted = |g: u64| plane.greeted.get(&member.to_string()) == Some(g); - let mut peer = DoorPeer { - plane, - member, - grants: def.grants.as_ask_grants(), - }; - let answer = exchange.drive(host, ticket, base, &greeted, &mut peer); - if let std::task::Poll::Ready(Ok(done)) = &answer { - if done.greeted { - plane.greeted.insert(member.to_string(), done.generation); - } - } - answer -} - -/// GREET the member's child before the attempt's call ([`Purpose::Ready`]): once per attempt, a -/// lease that reads the generation and runs the handshake on one the door has not greeted. A child -/// that cannot be reached is the walk's to find: the call goes on and its open answers. -pub(super) fn ready( - plane: &McpDoor, - ticket: Ticket, - unit: &mut CallUnit, - held: &Held, - member: &str, -) -> Looked { - let Some(def) = held.section.servers.get(member).filter(|d| is_program(d)) else { - return Looked::Fresh; - }; - let Some(host) = plane.host.as_ref().filter(|h| h.lends_connector()) else { - return Looked::Fresh; - }; - if unit.readied == Some(unit.attempt) { - return Looked::Fresh; - } - let mut exchange = match unit.program.take() { - Some((Purpose::Ready, parked)) => parked, - _ => ProgramExchange::new(member, door::NEED_PROGRAM, Vec::new(), true), - }; - let base = READY_SEQ.saturating_add( - unit.attempt - .saturating_mul(crate::tool_program::EXCHANGE_SERVICES), - ); - match exchange_child(plane, host, ticket, base, member, def, &mut exchange) { - std::task::Poll::Pending => { - unit.program = Some((Purpose::Ready, exchange)); - Looked::Pending - } - std::task::Poll::Ready(_) => { - unit.readied = Some(unit.attempt); - Looked::Fresh - } - } -} - -/// VERIFY-ON-CALL's fetch from a stdio member ([`Purpose::Verify`]): its `tools/list`, on a lease -/// of its own, answered by id, the child greeted first where its generation is new. The answer as -/// the exchange SDK spells one (a child's answer is a success), or why it failed. -pub(super) fn tools_listed( - plane: &McpDoor, - ticket: Ticket, - unit: &mut CallUnit, - def: &crate::tools_config::McpServerDefCfg, - server: &str, - base: u32, -) -> std::task::Poll> { - let Some(host) = plane.host.as_ref() else { - return std::task::Poll::Ready(Err( - busbar_contract::abi::sdk::conn::ConnFailure::Unarmed.to_string() - )); - }; - let id = list_id(unit.key, unit.attempt); - let mut exchange = match unit.program.take() { - Some((Purpose::Verify, parked)) => parked, - _ => ProgramExchange::new( - server, - door::NEED_PROGRAM, - vec![(list_request(id), Some(id))], - true, - ) - .timed(def.timeout_ms()), - }; - match exchange_child(plane, host, ticket, base, server, def, &mut exchange) { - std::task::Poll::Pending => { - unit.program = Some((Purpose::Verify, exchange)); - std::task::Poll::Pending - } - std::task::Poll::Ready(answer) => std::task::Poll::Ready(answer.map(|done| { - ( - busbar_contract::abi::sdk::exchange::ExchangeResponse { - status: 200, - reason: None, - fields: Vec::new(), - body: done.answer.unwrap_or_default(), - }, - id, - ) - })), - } -} - -/// An operator's `connect` to a stdio member: its `tools/list` on the verb's own ticket (the -/// exchange parked on the instance while it pends), correlated by `id`. -pub(super) fn connect_child( - instance: &Instance<'_, McpDoor>, - plane: &McpDoor, - def: &crate::tools_config::McpServerDefCfg, - server: &str, -) -> std::task::Poll<(Result, String>, u64)> { - let ticket = instance.ticket(); - let id = list_id( - (u64::from(ticket.slot) << 32) | u64::from(ticket.generation), - 0, - ); - let Some(host) = plane.host.as_ref() else { - return std::task::Poll::Ready(( - Err(busbar_contract::abi::sdk::conn::ConnFailure::Unarmed.to_string()), - id, - )); - }; - let mut exchange = match instance.resume::() { - Some(parked) => *parked, - None => ProgramExchange::new( - server, - door::NEED_PROGRAM, - vec![(list_request(id), Some(id))], - true, - ) - .timed(def.timeout_ms()), - }; - match exchange_child(plane, host, ticket, 0, server, def, &mut exchange) { - std::task::Poll::Pending => { - instance.park(exchange); - std::task::Poll::Pending - } - std::task::Poll::Ready(answer) => { - std::task::Poll::Ready((answer.map(|d| d.answer.unwrap_or_default()), id)) - } - } -} - -/// The `tools/list` a stdio member is sent, under `id`. -fn list_request(id: u64) -> Vec { - crate::client::jsonrpc::tools_list("", id, None).body -} - -/// What a far piece of a stdio member's answer came to. -pub(super) enum Far { - /// Read; nothing settled yet. - Taken, - /// A host service pended (a reply the child is owed): called again on its wake, the piece - /// already read. - Pending, - /// The answer to the call's id, or why there is none. - Settled(Result, String>), - /// The child asked for something busbar's caller answers (Law 11): its granted requests, in - /// order, unanswered. - Asked(Vec), -} - -/// ONE FAR PIECE of a stdio member's answer: the head names the child's generation; each body -/// piece is read for the message carrying the round's id, the child's own requests answered on a -/// lease of their own (only to the same generation) and its progress kept; the end without an -/// answer fails the call in the previous release's words. -#[allow(clippy::too_many_arguments)] // the piece's three facts beside the unit's -pub(super) fn far( - plane: &McpDoor, - ticket: Ticket, - member: &str, - def: &crate::tools_config::McpServerDefCfg, - relay: &mut ProgramRelay, - head: Option, - bytes: &[u8], - last: bool, -) -> Far { - if relay.replying.is_none() && relay.settled.is_none() { - if let Some(generation) = head { - relay.generation = generation; - plane.relaying.with(&relay.open.1, |e| { - if let Some(e) = e { - e.generation = generation; - } - }); - // A retry reaches only the child that asked: a restarted one owes nothing. - if relay.expect.is_some_and(|g| g != generation) { - relay.corr.outbox.clear(); - relay.settled = Some(Err(RESTARTED.to_string())); - return match relay.settled.take() { - Some(settled) => Far::Settled(settled), - None => Far::Taken, - }; - } - } - let mut peer = DoorPeer { - plane, - member, - grants: def.grants.as_ask_grants(), - }; - relay.hold = 0; - match relay - .corr - .take(bytes, relay.wait, member, relay.generation, &mut peer) - { - Ok(Some(answer)) => relay.settled = Some(Ok(answer)), - Ok(None) if last => relay.settled = Some(Err(crate::tool_program::CLOSED.to_string())), - Ok(None) => {} - Err(reason) => relay.settled = Some(Err(reason)), - } - owe(member, relay); - } - if replying(plane, ticket, member, def, relay) { - return Far::Pending; - } - match relay.settled.take() { - Some(settled) => Far::Settled(settled), - None if !relay.corr.asks.is_empty() => Far::Asked(std::mem::take(&mut relay.corr.asks)), - None => Far::Taken, - } -} - -/// The replies the child is owed, put on an exchange of their own (to its generation only). -fn owe(member: &str, relay: &mut ProgramRelay) { - if !relay.corr.outbox.is_empty() { - let owed: Vec<(Vec, Option)> = - relay.corr.outbox.drain(..).map(|r| (r, None)).collect(); - relay.replying = Some( - ProgramExchange::new(member, door::NEED_PROGRAM, owed, false).only_on(relay.generation), - ); - relay.replies += 1; - } -} - -/// The replies' exchange driven: whether it pends. -fn replying( - plane: &McpDoor, - ticket: Ticket, - member: &str, - def: &crate::tools_config::McpServerDefCfg, - relay: &mut ProgramRelay, -) -> bool { - if let Some(mut exchange) = relay.replying.take() { - if let Some(host) = plane.host.as_ref() { - let base = REPLY_SEQ.saturating_add( - relay - .replies - .saturating_mul(crate::tool_program::EXCHANGE_SERVICES), - ); - if exchange_child(plane, host, ticket, base, member, def, &mut exchange).is_pending() { - relay.replying = Some(exchange); - return true; - } - } - } - false -} - -/// ASKS BUSBAR COULD NOT RELAY, refused on the child's input (`replies`, one per ask: the round -/// cap, or a relay the deployment cannot carry) so the child is never left waiting on an answer -/// nobody will give; the call it serves is read on. `Pending` while the replies are written. -pub(super) fn refuse_asks( - plane: &McpDoor, - ticket: Ticket, - member: &str, - def: &crate::tools_config::McpServerDefCfg, - relay: &mut ProgramRelay, - replies: Vec>, -) -> Far { - relay.corr.outbox.extend(replies); - if relay.replying.is_none() { - owe(member, relay); - } - if replying(plane, ticket, member, def, relay) { - Far::Pending - } else { - Far::Taken - } -} - -/// The progress frames the relay's reading kept, taken. -pub(super) fn progress(relay: &mut ProgramRelay) -> Vec { - std::mem::take(&mut relay.corr.progress) -} - -/// The id unit `key`'s call carries on a child, round `round`. -pub(super) fn id_of(key: u64, round: u32) -> u64 { - call_id(key, round) -} diff --git a/crates/busbar-plane-mcp/src/door_sessions.rs b/crates/busbar-plane-mcp/src/door_sessions.rs deleted file mode 100644 index e8779d8cc2..0000000000 --- a/crates/busbar-plane-mcp/src/door_sessions.rs +++ /dev/null @@ -1,1651 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE SESSION REVISIONS AND THE LEGACY EVENT STREAM, SERVED, IN BOTH DIRECTIONS (THE DESIGN section 2, -//! the mcp bullet; `docs/design/BUSBAR-1.6.0.md` lines 379-390). -//! -//! INBOUND, on the endpoint's three verbs: -//! -//! - A POST that carries the stateless revision's own marker, or names neither a session nor -//! `initialize`, is the stateless path, untouched ([`crate::adapt::classify_post`]). -//! - `initialize` opens a session in the revision negotiation picks ([`crate::revision::negotiate`]; -//! no revision is configured): its id is 128 bits of the host's CSPRNG (`random.fill`), bound to -//! its OWNER, the unit's principal and credential (the kernel's opaque caller reference, minted -//! from the key id; the ungoverned chain's one constant), and named in `Mcp-Session-Id`. -//! - A message in a session is answered only for that owner: any other owner, an unknown, ended or -//! expired id, is `404` on every path. A dispatched method is RAISED into the stateless shape, sent -//! through the one dispatch, and its answer LOWERED ([`crate::adapt`]); `ping`, the session's own -//! verbs (`resources/subscribe`, `resources/unsubscribe`, `logging/setLevel`: subscribe stays for -//! the old revisions) and notifications are the session's. -//! - A GET naming a session opens that session's stream (resumed after `Last-Event-ID`); a GET -//! naming none and no `MCP-Protocol-Version` opens the `2024-11-05` stream, whose first event names -//! the message address its client POSTs to, every answer then arriving on the stream -//! ([`revision::sessionless_get`]); a GET naming a revision, a GET that accepts no event stream and -//! a DELETE naming no session are `405`. DELETE ends the owner's session. -//! - Sessions are this process's ([`crate::tool_sessions`]), bounded per owner in count and bytes. -//! On the ungoverned chain every caller is one owner, so the binding isolates nothing: the first -//! `2024-11-05` stream of an instance (whose session id rides a URL) says so once, as the -//! instance's diagnostic [`UNGOVERNED_LEGACY_STREAM`]. -//! -//! OUTBOUND ([`Negotiating`]): busbar sends an upstream the stateless request first, byte for byte -//! what it always sent; only when the upstream refuses it does the client ladder -//! ([`crate::client::negotiate`]) open a session with it (`initialize`, `notifications/initialized`, -//! the request lowered into the session), every further hop over the door's own connector. An -//! upstream answering in the stateless revision is remembered as such and never renegotiated. The -//! ladder's last rung, the `2024-11-05` event stream, is not carried as a client: an upstream that -//! refuses both the stateless request and `initialize` fails the call in words that name it. - -use std::collections::VecDeque; -use std::task::Poll; - -use serde_json::{json, Value}; - -use super::{ - ask_entitlements, door_listen, exchange_at, write, CallUnit, Held, McpDoor, Pending, Step, - Written, -}; -use crate::adapt::{self, SessionMethod}; -use crate::catalogue::Lookup; -use crate::client::jsonrpc::OutboundRequest; -use crate::client::negotiate::{Action, HopAnswer, Negotiator, Refusal as Refused, Verb}; -use crate::codec::{ - CODE_INTERNAL, CODE_INVALID_PARAMS, CODE_INVALID_REQUEST, CODE_METHOD_NOT_FOUND, CODE_REFUSED, - H_MCP_METHOD, H_MCP_NAME, H_PROTOCOL_VERSION, PROTOCOL_VERSION, -}; -use crate::framing::{CONTENT_LENGTH, CONTENT_TYPE, EVENT_STREAM}; -use crate::revision::{self, HeaderCheck, Revision, SessionlessGet}; -use crate::tool_arrival::{Disposition, Refusal}; -use crate::tool_sessions::{Carriage, OpenRefused, Owner, Remembered, SessionTable}; -use busbar_contract::abi::mechanism::call::{Outcome, SEVERITY_WARN}; -use busbar_contract::abi::mechanism::ticket::{CompletionHandle, Ticket}; -use busbar_contract::abi::plane::{OnPieceIn, OnPieceOut, FROM_CALLER, FROM_KERNEL, PIECE_LAST}; -use busbar_contract::abi::sdk::{Instance, Lent, Out, Services}; - -/// THE INSTANCE'S DIAGNOSTIC that an ungoverned chain's sessions are unisolated, latched on its -/// first `2024-11-05` stream (the index of its id in the Statement's diagnostic ids). -pub(super) const UNGOVERNED_LEGACY_STREAM: u32 = 0; - -/// The Statement's diagnostic ids, in index order. -pub(super) const DIAG_IDS: &[busbar_contract::abi::mechanism::call::AbiStr] = - &[busbar_contract::abi::sdk::door::abi_str( - "mcp-legacy-stream-ungoverned", - )]; - -/// The words of [`UNGOVERNED_LEGACY_STREAM`]. -const UNGOVERNED_WORDS: &str = - "an MCP 2024-11-05 event stream opened on the ungoverned chain: every \ - caller there is one owner, so a session id (which this revision carries in a URL) isolates \ - nothing between callers; govern the deployment to bind each session to its key"; - -/// The status a session that is not the caller's (unknown, ended, expired, or another owner's) is -/// answered with, on every path. -const STATUS_NOT_FOUND: u32 = 404; -/// The status of a session request whose version header disagrees with its session. -const STATUS_BAD_REQUEST: u32 = 400; -/// The status a session that cannot be opened is answered with. -const STATUS_UNAVAILABLE: u32 = 503; -/// The status of a message accepted and answered elsewhere (a notification, or a `2024-11-05` -/// message whose answer goes on its stream). -const STATUS_ACCEPTED: u32 = 202; - -/// The most resource subscriptions one owner holds across its sessions. -const MAX_OWNER_SUBSCRIPTIONS: usize = 256; -/// The ceiling on one retained subscription uri, in bytes. -const MAX_RESOURCE_SUB_URI_BYTES: usize = 2048; -/// The most announced updates a session holds until its stream collects them; past it, the oldest -/// gives way. -const MAX_PENDING_UPDATES: usize = 64; - -/// The RFC 5424 severities `logging/setLevel` names, least severe first. -const LEVELS: &[&str] = &[ - "debug", - "info", - "notice", - "warning", - "error", - "critical", - "alert", - "emergency", -]; - -/// What a unit is to the session machinery. -#[derive(Debug, Clone)] -pub(super) enum SessionUnit { - /// `initialize` POSTed to the endpoint: it opens a session. `id` is the request's id. - Open { - id: Value, - requested: Option, - }, - /// A message in `session`, carried as `carriage`, with the version header it carried. - Message { - session: String, - carriage: Carriage, - header: Option, - kind: Kind, - /// The session check passed: its answer is lowered (and, on the `2024-11-05` stream, - /// delivered there). - checked: bool, - }, - /// A GET stream: a session's (`Some`), or the `2024-11-05` stream opening (`None`). `mount` - /// is the path the arrival named, the message address's own. - Stream { - session: Option, - last_event_id: Option, - mount: String, - }, - /// DELETE of `session`. - Delete { session: String }, - /// A line of a carrier session that negotiated a session revision: its answer is lowered. - Line, -} - -/// What a session message asks. -#[derive(Debug, Clone)] -pub(super) enum Kind { - /// A method of the one dispatch, raised. - Dispatch, - /// Answered here: `ping`, the session's own verbs, `initialize` inside a `2024-11-05` session, - /// or a method no session revision has. - Here(Value), - /// A notification or a client's response: accepted, unanswered. - Accept(Value), - /// Refused as it arrived, answered only once the session is the caller's. - Refused(Box), -} - -/// One arrival the session machinery takes. -pub(super) struct Arrival { - pub(super) unit: SessionUnit, - /// The raised body the one dispatch decides on; `None` = the unit is answered here. - pub(super) dispatch: Option>, - /// The head fields the raised body implies: read ahead of the caller's own. - pub(super) mirror: Vec<(String, String)>, -} - -/// What `arrive` makes of an arrival on the endpoint. -pub(super) enum SessionArrived { - /// The stateless path, untouched. - Stateless, - /// `405`. - NotAllowed, - /// The session machinery's. - Session(Box), -} - -/// THE OWNER a unit's session is bound to: the kernel's caller reference (derived from the -/// principal's key id), or the ungoverned chain's one constant. -pub(super) fn owner_of(caller: &str) -> Owner { - let who = if caller.is_empty() { - crate::ask::UNGOVERNED - } else { - caller - }; - Owner { - principal: who.to_string(), - credential: who.to_string(), - } -} - -/// READ ONE ARRIVAL on the endpoint (`verb`, `target`, `body`, and a reader over its head fields). -pub(super) fn arrive( - held: Option<&Held>, - verb: &str, - target: &str, - body: &[u8], - field: &dyn Fn(&str) -> Option, -) -> SessionArrived { - let session = field(adapt::H_SESSION_ID).filter(|s| !s.is_empty()); - let (mount, query) = target - .split_once('?') - .map_or((target, None), |(p, q)| (p, Some(q))); - match verb { - "GET" => { - let streams = field("accept").is_some_and(|a| a.contains(EVENT_STREAM)); - if !streams { - return SessionArrived::NotAllowed; - } - let opens = session.is_some() - || revision::sessionless_get(field(H_PROTOCOL_VERSION).as_deref()) - == SessionlessGet::EventStream; - if !opens { - return SessionArrived::NotAllowed; - } - SessionArrived::Session(Box::new(Arrival { - unit: SessionUnit::Stream { - session, - last_event_id: field(adapt::H_LAST_EVENT_ID), - mount: mount.to_string(), - }, - dispatch: None, - mirror: Vec::new(), - })) - } - "DELETE" => match session { - Some(session) => SessionArrived::Session(Box::new(Arrival { - unit: SessionUnit::Delete { session }, - dispatch: None, - mirror: Vec::new(), - })), - None => SessionArrived::NotAllowed, - }, - "POST" => { - let value = serde_json::from_slice::(body).ok(); - let message_session = adapt::message_session_of(query).map(str::to_string); - let kind = match &value { - Some(v) => adapt::classify_post(v, session.as_deref(), message_session.as_deref()), - None if message_session.is_some() => adapt::PostKind::EventStreamMessage, - None if session.is_some() => adapt::PostKind::InSession, - None => adapt::PostKind::Stateless, - }; - match (kind, value) { - (adapt::PostKind::Initialize, Some(v)) => { - SessionArrived::Session(Box::new(Arrival { - unit: SessionUnit::Open { - id: v.get("id").cloned().unwrap_or(Value::Null), - requested: v - .pointer("/params/protocolVersion") - .and_then(Value::as_str) - .map(str::to_string), - }, - dispatch: None, - mirror: Vec::new(), - })) - } - (adapt::PostKind::InSession, value) => SessionArrived::Session(message( - held, - session.unwrap_or_default(), - Carriage::Endpoint, - field(H_PROTOCOL_VERSION), - value, - )), - (adapt::PostKind::EventStreamMessage, value) => SessionArrived::Session(message( - held, - message_session.unwrap_or_default(), - Carriage::EventStream, - None, - value, - )), - _ => SessionArrived::Stateless, - } - } - _ => SessionArrived::NotAllowed, - } -} - -/// One message in a session: what it asks, and the raised body when the one dispatch answers it. -fn message( - held: Option<&Held>, - session: String, - carriage: Carriage, - header: Option, - value: Option, -) -> Box { - let unit = |kind| SessionUnit::Message { - session, - carriage, - header, - kind, - checked: false, - }; - let Some(mut value) = value else { - let refusal = Refusal { - status: STATUS_BAD_REQUEST, - id: None, - code: busbar_contract::jsonrpc::PARSE_ERROR, - message: crate::tool_arrival::NOT_JSON.to_string(), - data: None, - }; - return Box::new(Arrival { - unit: unit(Kind::Refused(Box::new(refusal))), - dispatch: None, - mirror: Vec::new(), - }); - }; - let method = value - .get("method") - .and_then(Value::as_str) - .map(str::to_string); - let has_id = value.get("id").is_some_and(|i| !i.is_null()); - let (kind, dispatch, mirror) = match adapt::session_method(method.as_deref(), has_id) { - SessionMethod::Accept => (Kind::Accept(value), None, Vec::new()), - SessionMethod::Ping | SessionMethod::Session | SessionMethod::NotFound => { - (Kind::Here(value), None, Vec::new()) - } - SessionMethod::Dispatch => match adapt::raise(&mut value) { - None => (Kind::Here(value), None, Vec::new()), - Some(raised) => { - let mut mirror = vec![ - (H_PROTOCOL_VERSION.to_string(), raised.version.to_string()), - (H_MCP_METHOD.to_string(), raised.method.clone()), - ]; - if let Some(name) = &raised.name { - mirror.push(( - H_MCP_NAME.to_string(), - crate::client::jsonrpc::encode_sentinel(name), - )); - } - // A `tools/call` mirrors its annotated arguments, as its caller's revision - // cannot. - if raised.method == crate::codec::METHOD_TOOLS_CALL { - let schema = raised - .name - .as_deref() - .and_then(|n| held?.catalogue.tool(n)?.input_schema.clone()); - let arguments = value.pointer("/params/arguments"); - for (name, v) in adapt::param_mirror(schema.as_ref(), arguments) { - mirror.push((name.to_ascii_lowercase(), v)); - } - } - let dispatch = serde_json::to_vec(&value).unwrap_or_default(); - (Kind::Dispatch, Some(dispatch), mirror) - } - }, - }; - Box::new(Arrival { - unit: unit(kind), - dispatch, - mirror, - }) -} - -/// The disposition a session unit answered here is decided under, and counted as: an `initialize` -/// or a message answered here is a discovery, a stream holds open as a subscription does, and a -/// DELETE or an accepted message is a notification. -pub(super) fn disposition(unit: &SessionUnit) -> Disposition { - let request = |method: &str, id: Value| match crate::tool_ops::method_row_for(method) { - Some(row) => Disposition::Request { row, id }, - None => Disposition::Notice { - method: String::new(), - }, - }; - match unit { - SessionUnit::Open { id, .. } => request("server/discover", id.clone()), - SessionUnit::Stream { .. } => request("subscriptions/listen", Value::Null), - SessionUnit::Message { kind, .. } => match kind { - Kind::Here(v) => request( - "server/discover", - v.get("id").cloned().unwrap_or(Value::Null), - ), - Kind::Refused(r) => request("server/discover", r.id.clone().unwrap_or(Value::Null)), - Kind::Accept(v) => Disposition::Notice { - method: v - .get("method") - .and_then(Value::as_str) - .unwrap_or_default() - .to_string(), - }, - Kind::Dispatch => Disposition::Notice { - method: String::new(), - }, - }, - SessionUnit::Delete { .. } | SessionUnit::Line => Disposition::Notice { - method: String::new(), - }, - } -} - -/// A session unit's arrival refused by the one dispatch: answered once the session is the -/// caller's, never before (a mismatch answers `404` on every path). -pub(super) fn refused_in_session(unit: &mut SessionUnit, refusal: &Refusal) -> bool { - match unit { - SessionUnit::Message { kind, .. } => { - *kind = Kind::Refused(Box::new(refusal.clone())); - true - } - _ => false, - } -} - -// ── the host's services, on a unit's ticket ───────────────────────────────────────────────────── - -/// A fresh completion handle on `ticket`, counted in `issued`. -fn handle(ticket: Ticket, issued: &mut u32) -> CompletionHandle { - let h = CompletionHandle { - ticket, - seq: *issued, - _reserved: 0, - }; - *issued += 1; - h -} - -/// The kernel's wall clock in milliseconds; `0` with no clock. -pub(super) fn wall_ms(services: Option, ticket: Ticket, issued: &mut u32) -> u64 { - services.map_or(0, |s| { - s.clock_now(handle(ticket, issued)) - .map_or(0, |r| r.wall_ns / 1_000_000) - }) -} - -/// The session table. -fn with_sessions(plane: &McpDoor, f: impl FnOnce(&mut SessionTable) -> R) -> Option { - plane.sessions.with(&(), |t| t.map(f)) -} - -/// OPENS a session for `owner`: 128 bits from the host's CSPRNG (a collision draws again once). -fn open_session( - plane: &McpDoor, - ticket: Ticket, - unit: &mut CallUnit, - owner: &Owner, - revision: Revision, - carriage: Carriage, - now: u64, -) -> Result { - for _ in 0..2 { - let mut entropy = [0_u8; 16]; - let drawn = plane.services.is_some_and(|s| { - s.random_fill(handle(ticket, &mut unit.issued), &mut entropy) - .is_ok() - }); - if !drawn { - return Err("the host's random source did not answer, so no session id can be minted"); - } - let opened = with_sessions(plane, |t| { - t.open(entropy, owner.clone(), revision, carriage, now) - }); - match opened { - Some(Ok(id)) => return Ok(id.as_str().to_string()), - Some(Err(OpenRefused::Collision)) => {} - Some(Err(OpenRefused::NoEntropy)) => { - return Err("the host's random source answered no entropy") - } - Some(Err(OpenRefused::Full)) | None => { - return Err("this caller holds as many MCP sessions as this node keeps for it") - } - } - } - Err("two fresh session ids collided") -} - -/// A JSON-RPC error envelope. -fn rpc_error(id: &Value, code: i64, message: &str) -> Vec { - serde_json::to_vec(&busbar_contract::jsonrpc::error_body( - id.clone(), - code, - message, - None, - )) - .unwrap_or_default() -} - -/// A whole JSON answer. -fn answer_of(status: u32, body: Vec) -> Pending { - Pending::answer(status, body, None, &[]) -} - -/// The answer to a session that is not the caller's. -fn not_found(id: &Value) -> Pending { - answer_of( - STATUS_NOT_FOUND, - rpc_error( - id, - CODE_REFUSED, - "Session not found: it ended, expired, or is not this caller's. Send `initialize` \ - without Mcp-Session-Id to open a new one.", - ), - ) -} - -/// The id a session message carries. -fn id_of(kind: &Kind) -> Value { - match kind { - Kind::Here(v) | Kind::Accept(v) => v.get("id").cloned().unwrap_or(Value::Null), - Kind::Refused(r) => r.id.clone().unwrap_or(Value::Null), - Kind::Dispatch => Value::Null, - } -} - -// ── the answers ───────────────────────────────────────────────────────────────────────────────── - -/// ANSWER A SESSION UNIT on its caller's piece, the session first checked against its owner: -/// `None` for a unit the one dispatch answers (its session checked). -pub(super) fn answer( - plane: &McpDoor, - ticket: Ticket, - caller: &str, - unit: &mut CallUnit, -) -> Option { - let session_unit = unit.session.clone()?; - let owner = owner_of(caller); - let now = wall_ms(plane.services, ticket, &mut unit.issued); - let pending = match session_unit { - SessionUnit::Line | SessionUnit::Stream { .. } => return None, - SessionUnit::Open { id, requested } => { - open_endpoint(plane, ticket, unit, &owner, &id, requested.as_deref(), now) - } - SessionUnit::Delete { session } => { - if with_sessions(plane, |t| t.close(&session, &owner, now)) == Some(true) { - plane.session_state.remove(&session); - // Each of its streams ends at its next collection. - due(plane, &session); - answer_of(200, Vec::new()) - } else { - not_found(&Value::Null) - } - } - SessionUnit::Message { - session, - carriage, - header, - kind, - .. - } => { - let held = with_sessions(plane, |t| { - let revision = t.revision(&session, &owner, now)?; - (t.carriage(&session, &owner, now)? == carriage).then_some(revision) - }) - .flatten(); - let Some(revision) = held else { - return Some(written(unit, not_found(&id_of(&kind)))); - }; - if carriage == Carriage::Endpoint - && revision::check_header(revision, header.as_deref()) == HeaderCheck::Disagrees - { - let body = rpc_error( - &id_of(&kind), - CODE_INVALID_REQUEST, - "The MCP-Protocol-Version header names another revision than this session's.", - ); - return Some(written(unit, answer_of(STATUS_BAD_REQUEST, body))); - } - if let Some(SessionUnit::Message { checked, .. }) = unit.session.as_mut() { - *checked = true; - } - match kind { - Kind::Dispatch => return None, - Kind::Accept(v) => { - if v.get("method").and_then(Value::as_str) == Some(adapt::METHOD_INITIALIZED) { - with_sessions(plane, |t| t.mark_initialized(&session, &owner, now)); - } - answer_of(STATUS_ACCEPTED, Vec::new()) - } - Kind::Refused(r) => answer_of(r.status, r.body()), - Kind::Here(v) => { - let body = here( - plane, - ticket, - unit, - (session.as_str(), &owner), - revision, - carriage, - &v, - ); - answer_of(200, body) - } - } - } - }; - Some(written(unit, pending)) -} - -/// `pending` kept as the unit's answer. -fn written(unit: &mut CallUnit, pending: Pending) -> Step { - unit.pending = Some(pending); - Step::Write -} - -/// The discovery document this caller is entitled to, as `initialize` answers from it. -fn discovery(plane: &McpDoor, ticket: Ticket, unit: &mut CallUnit, id: &Value) -> Option { - let held = unit.held.clone().or_else(|| plane.current())?; - ask_entitlements(plane.services, ticket, unit, &held.catalogue.grants()); - let entitled = &unit.entitled; - let admit = |kind: &str, name: &str| { - entitled - .get(&format!("{kind}:{name}")) - .copied() - .unwrap_or(false) - }; - let document = crate::answer::discover(&held.catalogue, id, &admit); - serde_json::from_slice::(&document) - .ok()? - .get("result") - .cloned() -} - -/// `initialize` on the endpoint: the revision negotiation picks, a session opened in it. -fn open_endpoint( - plane: &McpDoor, - ticket: Ticket, - unit: &mut CallUnit, - owner: &Owner, - id: &Value, - requested: Option<&str>, - now: u64, -) -> Pending { - // The event-stream revision is opened by its GET, never by a POSTed `initialize`. - let revision = match revision::negotiate(requested) { - r if r.is_event_stream_revision() => revision::SESSION_REVISIONS[0], - r => r, - }; - let Some(discovered) = discovery(plane, ticket, unit, id) else { - return answer_of( - STATUS_UNAVAILABLE, - rpc_error( - id, - CODE_INTERNAL, - "this node publishes no MCP catalogue yet", - ), - ); - }; - let session = match open_session( - plane, - ticket, - unit, - owner, - revision, - Carriage::Endpoint, - now, - ) { - Ok(session) => session, - Err(why) => return answer_of(STATUS_UNAVAILABLE, rpc_error(id, CODE_INTERNAL, why)), - }; - let result = adapt::initialize_result(&discovered, revision, false); - let body = serde_json::to_vec(&crate::line::result(id, result)).unwrap_or_default(); - let mut pending = answer_of(200, body); - pending - .fields - .push((adapt::H_SESSION_ID.to_string(), session)); - pending -} - -/// A message the session answers itself. -fn here( - plane: &McpDoor, - ticket: Ticket, - unit: &mut CallUnit, - (session, owner): (&str, &Owner), - revision: Revision, - carriage: Carriage, - value: &Value, -) -> Vec { - let id = value.get("id").cloned().unwrap_or(Value::Null); - let method = value.get("method").and_then(Value::as_str).unwrap_or(""); - let ok = - |result: Value| serde_json::to_vec(&crate::line::result(&id, result)).unwrap_or_default(); - let invalid = |message: &str| rpc_error(&id, CODE_INVALID_PARAMS, message); - let uri = value.pointer("/params/uri").and_then(Value::as_str); - match method { - adapt::METHOD_PING => ok(json!({})), - // The `2024-11-05` session opened with its stream; its `initialize` arrives inside it. - adapt::METHOD_INITIALIZE if carriage == Carriage::EventStream => { - match discovery(plane, ticket, unit, &id) { - Some(d) => ok(adapt::initialize_result(&d, revision, false)), - None => rpc_error( - &id, - CODE_INTERNAL, - "this node publishes no MCP catalogue yet", - ), - } - } - adapt::METHOD_INITIALIZE => rpc_error( - &id, - CODE_INVALID_REQUEST, - "this session is already open: send `initialize` without Mcp-Session-Id to open \ - another", - ), - "resources/subscribe" => { - let Some(uri) = uri else { - return invalid("`params.uri` is required: the resource to watch."); - }; - if uri.len() > MAX_RESOURCE_SUB_URI_BYTES { - return invalid( - "`params.uri` is longer than a session retains: a subscription is held for \ - the session, so it is bounded.", - ); - } - let Some(held) = unit.held.clone().or_else(|| plane.current()) else { - return invalid("this node publishes no MCP catalogue yet"); - }; - ask_entitlements(plane.services, ticket, unit, &held.catalogue.grants()); - let entitled = &unit.entitled; - let admit = |kind: &str, name: &str| { - entitled - .get(&format!("{kind}:{name}")) - .copied() - .unwrap_or(false) - }; - if !matches!(held.catalogue.resource_by_uri(&admit, uri), Lookup::One(_)) { - return invalid("`params.uri` names no resource this caller may read."); - } - if subscribe(plane, session, owner, uri) { - ok(json!({})) - } else { - invalid( - "this caller already holds as many resource subscriptions as this node \ - watches for it: unsubscribe from one before subscribing to another.", - ) - } - } - "resources/unsubscribe" => { - let Some(uri) = uri else { - return invalid("`params.uri` is required: the resource to stop watching."); - }; - plane.session_state.with(&session.to_string(), |s| { - if let Some(s) = s { - s.uris.retain(|u| u != uri); - } - }); - ok(json!({})) - } - "logging/setLevel" => { - let level = value.pointer("/params/level").and_then(Value::as_str); - match level.filter(|l| LEVELS.contains(l)) { - Some(level) => { - state_of(plane, session, owner, |s| s.level = Some(level.to_string())); - ok(json!({})) - } - None => invalid( - "`params.level` must name an RFC 5424 severity: the floor this session's \ - `notifications/message` records are filtered at.", - ), - } - } - _ => rpc_error( - &id, - CODE_METHOD_NOT_FOUND, - &format!("Method `{method}` is not implemented by this server for this revision."), - ), - } -} - -// ── what a session holds beside its table row ─────────────────────────────────────────────────── - -/// A session's own state: its owner, the resources it watches, its log floor, and the updates -/// announced for it its stream has not collected. -#[derive(Debug, Clone, Default)] -pub(super) struct SessionState { - owner: Option, - uris: Vec, - level: Option, - updates: VecDeque<(String, String)>, -} - -/// Runs `f` on `session`'s state, made for `owner` when it has none. -fn state_of(plane: &McpDoor, session: &str, owner: &Owner, f: impl FnOnce(&mut SessionState)) { - plane.session_state.with_all(|m| { - let s = m.entry(session.to_string()).or_default(); - s.owner.get_or_insert_with(|| owner.clone()); - f(s); - }); -} - -/// Subscribes `session` to `uri`: `false` past the owner's subscription quota. -fn subscribe(plane: &McpDoor, session: &str, owner: &Owner, uri: &str) -> bool { - plane.session_state.with_all(|m| { - if m.get(session) - .is_some_and(|s| s.uris.iter().any(|u| u == uri)) - { - return true; - } - let held: usize = m - .values() - .filter(|s| s.owner.as_ref() == Some(owner)) - .map(|s| s.uris.len()) - .sum(); - if held >= MAX_OWNER_SUBSCRIPTIONS { - return false; - } - let s = m.entry(session.to_string()).or_default(); - s.owner.get_or_insert_with(|| owner.clone()); - s.uris.push(uri.to_string()); - true - }) -} - -/// AN UPSTREAM ANNOUNCED that `uri` of its registration `server` changed: every session watching -/// that uri holds it until its stream collects it, where the subscriber's entitlement is asked -/// again (Listen's relay rule: the uri resolves under its live grant to the announcing server's -/// declared resource). -pub(super) fn announce(plane: &McpDoor, server: &str, uri: &str) { - let sessions: Vec = plane.session_state.with_all(|m| { - let mut hit = Vec::new(); - for (session, s) in m.iter_mut() { - if s.uris.iter().any(|u| u == uri) { - if s.updates.len() >= MAX_PENDING_UPDATES { - s.updates.pop_front(); - } - s.updates.push_back((server.to_string(), uri.to_string())); - hit.push(session.clone()); - } - } - hit - }); - for session in sessions { - due(plane, &session); - } -} - -/// WHAT AN UPSTREAM SAID ON ITS EVENT STREAM beside a relayed call's answer: its resource updates -/// are announced to every watching session, and, for a caller in a session, its log records past -/// the session's floor are delivered on that session's stream. -pub(super) fn heard( - plane: &McpDoor, - server: &str, - raw: &[u8], - session: Option<(&str, &Owner, u64)>, -) { - for line in String::from_utf8_lossy(raw).lines() { - let Some(data) = line.strip_prefix("data:") else { - continue; - }; - let Ok(frame) = serde_json::from_str::(data.trim()) else { - continue; - }; - match frame.get("method").and_then(Value::as_str) { - Some("notifications/resources/updated") => { - if let Some(uri) = frame.pointer("/params/uri").and_then(Value::as_str) { - announce(plane, server, uri); - } - } - Some("notifications/message") => { - let Some((id, owner, now)) = session else { - continue; - }; - let floor = plane - .session_state - .get(&id.to_string()) - .and_then(|s| s.level); - let level = frame.pointer("/params/level").and_then(Value::as_str); - let rank = |l: &str| LEVELS.iter().position(|x| *x == l); - let passes = match (floor.as_deref().and_then(rank), level.and_then(rank)) { - (Some(floor), Some(level)) => level >= floor, - (Some(_), None) => false, - (None, _) => true, - }; - if passes { - deliver_frame(plane, id, owner, &frame.to_string(), now); - } - } - _ => {} - } - } -} - -/// The session a unit's caller is in, when it is in one whose check passed. -pub(super) fn session_of(unit: &Option) -> Option { - match unit { - Some(SessionUnit::Message { - session, - checked: true, - .. - }) => Some(session.clone()), - _ => None, - } -} - -// ── the answer, lowered ───────────────────────────────────────────────────────────────────────── - -/// LOWERS the answer a session unit's dispatch wrote, before its first byte: the session -/// revision's shape (a result no session revision can carry becomes an error), every JSON-RPC -/// answer at `200`; on the `2024-11-05` stream, the answer goes on the stream and the POST is -/// accepted (`202`). -pub(super) fn lower(plane: &McpDoor, ticket: Ticket, caller: &str, unit: &mut CallUnit) { - let (line, legacy) = match &unit.session { - Some(SessionUnit::Message { - checked: true, - carriage, - session, - .. - }) => ( - false, - (*carriage == Carriage::EventStream).then(|| session.clone()), - ), - Some(SessionUnit::Line) => (true, None), - _ => return, - }; - let lowerable = unit - .pending - .as_ref() - .is_some_and(|p| !p.headed && p.request.is_none() && !p.bytes.is_empty()); - if !lowerable { - return; - } - let now = legacy - .as_ref() - .map_or(0, |_| wall_ms(plane.services, ticket, &mut unit.issued)); - let Some(pending) = unit.pending.as_mut() else { - return; - }; - let Ok(mut v) = serde_json::from_slice::(&pending.bytes) else { - return; - }; - let inexpressible = v - .get_mut("result") - .is_some_and(|result| adapt::lower_result(result).is_err()); - if inexpressible { - let id = v.get("id").cloned().unwrap_or(Value::Null); - v = busbar_contract::jsonrpc::error_body( - id, - CODE_REFUSED, - "this answer asks for input or names a task, which this session's MCP revision \ - cannot carry", - None, - ); - } - let rpc = v.get("jsonrpc").is_some() && (v.get("result").is_some() || v.get("error").is_some()); - let bytes = serde_json::to_vec(&v).unwrap_or_default(); - if line { - pending.bytes = bytes; - return; - } - match legacy { - Some(session) => { - deliver_frame( - plane, - &session, - &owner_of(caller), - &String::from_utf8_lossy(&bytes), - now, - ); - pending.bytes.clear(); - pending.fields.clear(); - pending.status = STATUS_ACCEPTED; - } - None => { - if rpc { - pending.status = 200; - } - for (name, value) in &mut pending.fields { - if name.as_str() == CONTENT_LENGTH { - *value = bytes.len().to_string(); - } - } - pending.bytes = bytes; - } - } -} - -// ── the streams ───────────────────────────────────────────────────────────────────────────────── - -/// One held GET stream, by its unit. -#[derive(Debug, Clone)] -pub(super) struct Stream { - session: String, - owner: Owner, - /// The session table's stream the events are buffered on. - stream: u32, - /// The last event seq written to the caller. - delivered: u64, - /// The `2024-11-05` stream (no event ids). - legacy: bool, - /// The session's caller-side ticket. - ticket: Ticket, - /// The stream owes a collection. - due: bool, - /// When it last wrote (the host's monotonic clock). - last_write_ns: u64, -} - -/// Whether `unit` is a held session stream (its pieces are this module's). -pub(super) fn holds(plane: &McpDoor, unit: u64) -> bool { - plane.units.with(&unit, |u| { - u.is_some_and(|u| matches!(u.session, Some(SessionUnit::Stream { .. }))) - }) -} - -/// `data` buffered on `session`'s stream, and the stream named due. -fn deliver_frame(plane: &McpDoor, session: &str, owner: &Owner, data: &str, now: u64) { - let target = plane - .streams - .with_all(|m| m.values().find(|s| s.session == session).map(|s| s.stream)); - if let Some(stream) = target { - with_sessions(plane, |t| t.push(session, owner, stream, data, now)); - due(plane, session); - } -} - -/// Every stream of `session` named due, and the driver woken. -fn due(plane: &McpDoor, session: &str) { - let any = plane.streams.with_all(|m| { - let mut any = false; - for s in m.values_mut().filter(|s| s.session == session) { - s.due = true; - any = true; - } - any - }); - if any { - door_listen::wake_driver(plane); - } -} - -/// The streams owing a collection. -pub(super) fn due_streams(plane: &McpDoor) -> Vec { - plane - .streams - .with_all(|m| m.iter().filter(|(_, s)| s.due).map(|(k, _)| *k).collect()) -} - -/// The streams named to the host: no longer owing. -pub(super) fn taken(plane: &McpDoor, named: &[u64]) { - plane.streams.with_all(|m| { - for key in named { - if let Some(s) = m.get_mut(key) { - s.due = false; - } - } - }); -} - -/// THE TICK: every held stream owes a collection (its keepalive, or its end once its session is -/// gone), and the state of every session the table no longer holds is dropped. -pub(super) fn tick(plane: &McpDoor) { - let any = plane.streams.with_all(|m| { - for s in m.values_mut() { - s.due = true; - } - !m.is_empty() - }); - let gone: Vec = plane - .session_state - .with_all(|m| m.keys().cloned().collect()); - let gone: Vec = gone - .into_iter() - .filter(|id| with_sessions(plane, |t| !t.holds(id)).unwrap_or(true)) - .collect(); - plane.session_state.with_all(|m| { - for id in &gone { - m.remove(id); - } - }); - if any { - door_listen::wake_driver(plane); - } -} - -/// The op on `ticket` was cancelled: the stream it carried is dropped. -pub(super) fn cancelled(plane: &McpDoor, ticket: Ticket) { - plane - .streams - .with_all(|m| m.retain(|_, s| s.ticket != ticket)); -} - -/// What an idle stream writes to say it is alive. -const KEEPALIVE: &[u8] = b": keepalive\n\n"; - -/// ONE PIECE OF A HELD SESSION STREAM: the caller's piece opens it, a collection writes what its -/// session buffered (or a keepalive, or its end), and the caller's side ending ends it. -pub(super) fn piece( - plane: &McpDoor, - input: Lent<'_, OnPieceIn>, - out: &mut Out<'_, OnPieceOut>, -) -> Outcome { - let given = input.get(); - let (key, ticket) = (given.unit, given.head.ticket); - let caller = input - .field(|i| &i.caller_ref) - .as_str() - .unwrap_or_default() - .to_string(); - let owner = owner_of(&caller); - let mut say_ungoverned = false; - let step = plane.units.with(&key, |unit| { - let unit = unit?; - unit.ticket = Some(ticket); - if unit.pending.is_some() { - return Some(true); - } - match given.from { - FROM_CALLER if given.flags & PIECE_LAST != 0 => { - plane.streams.remove(&key); - unit.pending = Some(door_listen::frame(Vec::new(), true, true)); - Some(true) - } - FROM_CALLER => { - let (wrote, legacy_ungoverned) = open_stream(plane, ticket, &owner, unit)?; - say_ungoverned = legacy_ungoverned; - Some(wrote) - } - FROM_KERNEL if given.attempt_no == 0 => step_stream(plane, ticket, unit), - FROM_KERNEL => Some(false), - _ => None, - } - }); - // THE UNGOVERNED CHAIN SAYS SO, once per instance, on its first `2024-11-05` stream. - if say_ungoverned && plane.said_ungoverned.insert((), ()).is_none() { - let _reported = out.diag(UNGOVERNED_LEGACY_STREAM, SEVERITY_WARN, UNGOVERNED_WORDS); - } - match step { - None => Outcome::Refused, - Some(false) => Outcome::Ready, - Some(true) => { - let written = plane.units.with(&key, |unit| { - let pending = unit?.pending.as_mut()?; - Some(write(&input, out, pending)) - }); - match written { - None | Some(Err(())) => Outcome::Failed, - Some(Ok(Written::More)) => Outcome::Ready, - Some(Ok(Written::Sent)) => { - plane.units.with(&key, |unit| { - if let Some(unit) = unit { - unit.pending = None; - } - }); - Outcome::Ready - } - Some(Ok(Written::Done)) => { - plane.units.remove(&key); - plane.streams.remove(&key); - Outcome::Ready - } - } - } - } -} - -/// The events after a stream's cursor, framed: `(bytes, the last seq)`. -fn frames(events: &[(String, String)], legacy: bool) -> (Vec, Option) { - let mut bytes = Vec::new(); - let mut last = None; - for (id, data) in events { - let id_word = (!legacy).then_some(id.as_str()); - bytes.extend_from_slice(adapt::frame(id_word, Some(adapt::EVENT_MESSAGE), data).as_bytes()); - last = id.split_once('-').and_then(|(_, q)| q.parse().ok()); - } - (bytes, last) -} - -/// OPENS the stream the unit's GET asks for: `(wrote, an ungoverned 2024-11-05 stream)`. -fn open_stream( - plane: &McpDoor, - ticket: Ticket, - owner: &Owner, - unit: &mut CallUnit, -) -> Option<(bool, bool)> { - let Some(SessionUnit::Stream { - session, - last_event_id, - mount, - }) = unit.session.clone() - else { - return None; - }; - if plane.streams.with(&unit.key, |s| s.is_some()) { - return Some((false, false)); - } - let now = wall_ms(plane.services, ticket, &mut unit.issued); - let mono = door_listen::mono_ns(plane.services, ticket, unit); - let ungoverned = owner.principal == crate::ask::UNGOVERNED; - let (session, stream, delivered, legacy, bytes) = match session { - None => { - let opened = open_session( - plane, - ticket, - unit, - owner, - Revision::R2024_11_05, - Carriage::EventStream, - now, - ); - let session = match opened { - Ok(session) => session, - Err(why) => { - let body = rpc_error(&Value::Null, CODE_INTERNAL, why); - unit.pending = Some(answer_of(STATUS_UNAVAILABLE, body)); - return Some((true, false)); - } - }; - let stream = with_sessions(plane, |t| t.open_stream(&session, owner, now)).flatten()?; - let endpoint = adapt::endpoint_event(&mount, &session); - (session, stream, 0, true, endpoint.into_bytes()) - } - Some(session) => { - let carriage = with_sessions(plane, |t| t.carriage(&session, owner, now)).flatten(); - if carriage != Some(Carriage::Endpoint) { - unit.pending = Some(not_found(&Value::Null)); - return Some((true, false)); - } - let resumed = last_event_id - .and_then(|cursor| { - with_sessions(plane, |t| t.replay(&session, owner, &cursor, now)) - }) - .flatten(); - match resumed { - Some(replay) => { - let (bytes, last) = frames(&replay.events, false); - let delivered = last.unwrap_or(0); - let bytes = if bytes.is_empty() { - KEEPALIVE.to_vec() - } else { - bytes - }; - (session, replay.stream, delivered, false, bytes) - } - None => { - let stream = - with_sessions(plane, |t| t.open_stream(&session, owner, now)).flatten()?; - (session, stream, 0, false, KEEPALIVE.to_vec()) - } - } - } - }; - // One stream per live unit, so the unit table's admission bound (it refuses past `MAX_UNITS`, - // and evicts nothing) bounds this table too. - plane.streams.insert( - unit.key, - Stream { - session, - owner: owner.clone(), - stream, - delivered, - legacy, - ticket, - due: false, - last_write_ns: mono, - }, - ); - unit.pending = Some(door_listen::frame(bytes, false, false)); - Some((true, legacy && ungoverned)) -} - -/// ONE COLLECTION of a held stream: the updates announced for its session (each re-judged under -/// the caller's live grants), then every event buffered after its cursor; a keepalive when quiet; -/// its end once its session is gone. -fn step_stream(plane: &McpDoor, ticket: Ticket, unit: &mut CallUnit) -> Option { - let held_stream = plane.streams.get(&unit.key)?; - let now = wall_ms(plane.services, ticket, &mut unit.issued); - let mono = door_listen::mono_ns(plane.services, ticket, unit); - let updates: Vec<(String, String)> = plane - .session_state - .with(&held_stream.session, |s| { - s.map(|s| s.updates.drain(..).collect::>()) - }) - .unwrap_or_default(); - if !updates.is_empty() { - if let Some(held) = plane.current().or_else(|| unit.held.clone()) { - door_listen::entitled_afresh(plane, ticket, unit, &held); - let entitled = &unit.entitled; - let admit = |kind: &str, name: &str| { - entitled - .get(&format!("{kind}:{name}")) - .copied() - .unwrap_or(false) - }; - for (server, uri) in updates { - let theirs = matches!( - held.catalogue.resource_by_uri(&admit, &uri), - Lookup::One(entry) if entry.server == server - ); - if theirs { - let note = json!({ - "jsonrpc": "2.0", - "method": "notifications/resources/updated", - "params": { "uri": uri }, - }); - with_sessions(plane, |t| { - t.push( - &held_stream.session, - &held_stream.owner, - held_stream.stream, - ¬e.to_string(), - now, - ) - }); - } - } - } - } - let cursor = format!("{}-{}", held_stream.stream, held_stream.delivered); - let replay = with_sessions(plane, |t| { - t.replay(&held_stream.session, &held_stream.owner, &cursor, now) - }) - .flatten(); - let Some(replay) = replay else { - // The session is gone (ended, expired, evicted): so is its stream. - plane.streams.remove(&unit.key); - unit.pending = Some(door_listen::frame(Vec::new(), true, true)); - return Some(true); - }; - let (bytes, last) = frames(&replay.events, held_stream.legacy); - let bytes = if !bytes.is_empty() { - bytes - } else if mono.saturating_sub(held_stream.last_write_ns) >= crate::subscribe::KEEPALIVE_NS { - KEEPALIVE.to_vec() - } else { - return Some(false); - }; - plane.streams.with(&unit.key, |s| { - if let Some(s) = s { - if let Some(last) = last { - s.delivered = last; - } - s.last_write_ns = mono; - } - }); - unit.pending = Some(door_listen::frame(bytes, false, true)); - Some(true) -} - -// ── busbar as a client: the negotiated upstream conversation ──────────────────────────────────── - -/// How many connector handles one hop of a negotiation may number. -pub(super) const HOP_SPAN: u32 = 1 << 9; - -/// What busbar remembers about `member`. -fn remembered(plane: &McpDoor, member: &str) -> Option { - plane.upstreams.with(&(), |t| t.and_then(|t| t.get(member))) -} - -/// One hop's answer as the connector read it: its status, its head fields (lower-case names) and -/// its body. -type Hop = (u16, Vec<(String, String)>, Vec); - -/// What a negotiation came to. -pub(super) enum Negotiated { - /// The answer to hand on: the status, the body as the upstream sent it, and whether it is an - /// event stream. - Answer(u16, Vec, bool), - /// No answer: why, in words, for the caller's upstream-failure answer. - Failed(String), -} - -/// ONE UPSTREAM CONVERSATION, NEGOTIATED ([`Negotiator`]), parked on its unit across PENDING -/// entries: the action under way, the hops sent, and what the answers so far said. -#[derive(Debug)] -pub(super) struct Negotiating { - negotiator: Negotiator, - next: Option, - hops: u32, - /// The first hop is the stateless request, not yet answered. - probing: bool, - /// The upstream answered the stateless revision before: it is not renegotiated. - stateless_known: bool, - /// The walk's answer was fed (a resumed FAR-END piece carries it again). - pub(super) fed: bool, - /// The session the walk's answer named (its head crosses with its first piece). - pub(super) walk_session: Option, - /// The last answer's body as sent, and whether it is an event stream. - raw: (Vec, bool), - /// The statuses of the hops that refused, for the words of a failure. - refused: Vec, - /// Why the last hop had no answer at all. - failure: Option, -} - -impl Negotiating { - /// A conversation carrying `original` (a stateless request) to `member`, and the request its - /// first hop is: `original` itself, or lowered into the session busbar remembers. - pub(super) fn begin(plane: &McpDoor, member: &str, original: OutboundRequest) -> Self { - let remembered = remembered(plane, member); - let stateless_known = remembered - .as_ref() - .is_some_and(|r| r.revision == Revision::R2026_07_28); - let probing = !remembered - .as_ref() - .is_some_and(|r| r.revision.has_sessions()); - let mut negotiator = Negotiator::new(original, remembered, super::VERSION); - let first = negotiator.start(); - Negotiating { - negotiator, - next: Some(first), - hops: 0, - probing, - stateless_known, - fed: false, - walk_session: None, - raw: (Vec::new(), false), - refused: Vec::new(), - failure: None, - } - } - - /// The request the first hop sends, when it is one. - pub(super) fn first_request(&self) -> Option<&OutboundRequest> { - match &self.next { - Some(Action::Send { request, .. }) => Some(request), - _ => None, - } - } - - /// Feeds one hop's answer (`None`: none at all) and answers the next action. - fn feed(&mut self, answer: Option) -> Action { - let probing = std::mem::replace(&mut self.probing, false); - let Some((status, fields, body)) = answer else { - return self.negotiator.on_answer(None); - }; - let sse = fields - .iter() - .any(|(n, v)| n.eq_ignore_ascii_case(CONTENT_TYPE) && v.starts_with(EVENT_STREAM)); - let json = if sse { - crate::call::last_sse_data(&body) - } else { - body.clone() - }; - self.raw = (body, sse); - let hop = HopAnswer { - status, - fields, - body: json, - }; - // A KNOWN STATELESS UPSTREAM, or a refusal that says nothing about the revision (the - // credential, or a rate): its answer is the stateless path's, unrenegotiated. - if probing && (self.stateless_known || matches!(status, 401 | 403 | 407 | 429)) { - return Action::Finish(hop); - } - if !(200..300).contains(&status) { - self.refused.push(status); - } - self.negotiator.on_answer(Some(hop)) - } - - /// DRIVES the conversation over the door's connector, hop by hop, from `base`: PENDING while a - /// hop pends (the action kept), READY with what it came to. What busbar learnt is kept for the - /// next call to `member`. - fn drive( - &mut self, - instance: &Instance<'_, McpDoor>, - plane: &McpDoor, - base: u32, - member: &str, - timeout_ms: u64, - ) -> Poll { - loop { - match self.next.take() { - Some(Action::Send { verb, request }) => { - let at = base.saturating_add(self.hops.saturating_mul(HOP_SPAN)); - let polled = exchange_at( - instance, - plane.host.as_ref(), - at, - &request.url, - member, - || exchange_request(verb, &request, timeout_ms), - ); - let Poll::Ready(answered) = polled else { - self.next = Some(Action::Send { verb, request }); - return Poll::Pending; - }; - self.hops += 1; - let answered = match answered { - Ok(r) => Some(( - r.status, - r.fields - .iter() - .map(|(n, v)| { - ( - String::from_utf8_lossy(n).to_ascii_lowercase(), - String::from_utf8_lossy(v).into_owned(), - ) - }) - .collect(), - r.body, - )), - Err(e) => { - self.failure = Some(e.to_string()); - None - } - }; - self.next = Some(self.feed(answered)); - } - Some(Action::Finish(answer)) => { - self.keep(plane, member); - let (raw, sse) = std::mem::take(&mut self.raw); - let raw = if raw.is_empty() { answer.body } else { raw }; - return Poll::Ready(Negotiated::Answer(answer.status, raw, sse)); - } - Some(Action::Fail(refusal)) => { - self.keep(plane, member); - return Poll::Ready(self.failed(&refusal, member)); - } - // THE LAST RUNG, NOT CARRIED: the `2024-11-05` event stream is read only as a held - // stream, which the exchange cannot hold. Said, never papered over. - Some(Action::OpenStream { .. } | Action::AwaitEvent) | None => { - self.keep(plane, member); - return Poll::Ready(Negotiated::Failed(self.legacy_only(member))); - } - } - } - } - - /// What busbar learnt, kept (or forgotten) for the next call to `member`. - fn keep(&self, plane: &McpDoor, member: &str) { - plane.upstreams.with(&(), |t| { - if let Some(t) = t { - match self.negotiator.remembered() { - Some(r) => t.put(member, r), - None => t.forget(member), - } - } - }); - } - - /// The words of an upstream that refused every revision busbar carries as a client. - fn legacy_only(&self, member: &str) -> String { - let statuses: Vec = self.refused.iter().map(u16::to_string).collect(); - format!( - "MCP server `{member}` refused the stateless {PROTOCOL_VERSION} request and refused \ - `initialize` (HTTP {}); the one revision left is 2024-11-05 (HTTP+SSE), which busbar \ - does not speak as a client", - statuses.join(", ") - ) - } - - /// A failed conversation, in words; an answer it did get is handed on as it came. - fn failed(&mut self, refusal: &Refused, member: &str) -> Negotiated { - match refusal { - Refused::Unreachable(Some(answer)) => { - let (raw, sse) = std::mem::take(&mut self.raw); - let raw = if raw.is_empty() { - answer.body.clone() - } else { - raw - }; - Negotiated::Answer(answer.status, raw, sse) - } - Refused::Unreachable(None) => Negotiated::Failed( - self.failure - .clone() - .unwrap_or_else(|| format!("MCP server `{member}` could not be reached")), - ), - Refused::OfferedUnsupported => Negotiated::Failed(format!( - "MCP server `{member}` answered `initialize` with an MCP revision busbar does not \ - carry as a client (it carries 2025-11-25 and 2025-06-18 on a session)" - )), - Refused::NoCommonRevision | Refused::AddressRefused | Refused::StreamLost => { - Negotiated::Failed(self.legacy_only(member)) - } - } - } -} - -/// One hop as the connector's exchange sends it. -fn exchange_request( - verb: Verb, - request: &OutboundRequest, - timeout_ms: u64, -) -> busbar_contract::abi::sdk::exchange::Request { - busbar_contract::abi::sdk::exchange::Request { - method: match verb { - Verb::Post => b"POST".to_vec(), - Verb::Delete => b"DELETE".to_vec(), - }, - target: crate::call::path_of(&request.url).into_bytes(), - fields: request - .headers - .iter() - .map(|(n, v)| (n.as_bytes().to_vec(), v.as_bytes().to_vec())) - .collect(), - body: request.body.clone(), - timeout_ms, - } -} - -/// A FETCH OF THE DOOR'S OWN (verify-on-call's `tools/list`), negotiated: every hop over the -/// connector from `base`, the conversation parked on the unit while one pends. -pub(super) fn fetch( - instance: &Instance<'_, McpDoor>, - plane: &McpDoor, - unit: &mut CallUnit, - (base, member, timeout_ms): (u32, &str, u64), - original: OutboundRequest, -) -> Poll> { - let negotiating = unit - .negotiating - .get_or_insert_with(|| Box::new(Negotiating::begin(plane, member, original))); - let Poll::Ready(done) = negotiating.drive(instance, plane, base, member, timeout_ms) else { - return Poll::Pending; - }; - unit.negotiating = None; - Poll::Ready(match done { - Negotiated::Answer(status, raw, sse) => { - Ok(busbar_contract::abi::sdk::exchange::ExchangeResponse { - status, - body: if sse { - crate::call::last_sse_data(&raw) - } else { - raw - }, - ..Default::default() - }) - } - Negotiated::Failed(reason) => Err(reason), - }) -} - -/// THE WALK'S ANSWER to a relayed call's first hop (`status`, event stream or not, `far`), and the -/// conversation from there: READY with what it came to, `None` when the answer is handed on as it -/// came (the stateless path, or no connector to negotiate over). -pub(super) fn far_answer( - instance: &Instance<'_, McpDoor>, - plane: &McpDoor, - negotiating: &mut Negotiating, - (base, member, timeout_ms): (u32, &str, u64), - (status, sse, far): (u32, bool, &[u8]), -) -> Poll> { - if !negotiating.fed { - negotiating.fed = true; - let mut fields = Vec::new(); - if sse { - fields.push((CONTENT_TYPE.to_string(), EVENT_STREAM.to_string())); - } - if let Some(session) = negotiating.walk_session.clone() { - fields.push((adapt::H_SESSION_ID.to_string(), session)); - } - let status = u16::try_from(status).unwrap_or(0); - let next = negotiating.feed(Some((status, fields, far.to_vec()))); - // The walk's own answer, or a refusal with no connector to negotiate over: handed on. - let lends = plane.host.is_some_and(|h| h.lends_connector()); - match next { - Action::Finish(_) => { - negotiating.keep(plane, member); - return Poll::Ready(None); - } - Action::Send { .. } if !lends => return Poll::Ready(None), - next => negotiating.next = Some(next), - } - } - negotiating - .drive(instance, plane, base, member, timeout_ms) - .map(Some) -} - -/// The stateless request a relayed call's outbound is, for its negotiation. -pub(super) fn original(url: &str, outbound: &crate::call::OutboundCall) -> OutboundRequest { - OutboundRequest { - url: url.to_string(), - headers: outbound.fields.clone(), - body: outbound.body.clone(), - } -} diff --git a/crates/busbar-plane-mcp/src/door_tasks.rs b/crates/busbar-plane-mcp/src/door_tasks.rs deleted file mode 100644 index 4237233b8e..0000000000 --- a/crates/busbar-plane-mcp/src/door_tasks.rs +++ /dev/null @@ -1,1953 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE TASKS EXTENSION ON THE DOOR (ARCHITECT round 5 Q-L3B-TASKS (b) → (A)): who creates a task, -//! who runs it, and how the three `tasks/*` verbs read and move it. The task itself — its state, its -//! shapes, its sweep and its durable rows — is [`crate::tool_tasks`]. -//! -//! * THE CREATING UNIT (a `tools/call` admitted on a tool that creates a task for this caller) opens -//! a durable work handle (`work.open`; its reference is the `taskId`), holds the task, nests the -//! CONTINUATION on the plane's own task-run claim ([`crate::tool_claims::TASK_RUN_MOUNT`], -//! `unit.nest`) carrying the reference and the call it runs, and answers `CreateTaskResult`. -//! The continuation runs without any poll, as the served engine's runner did. -//! * THE CONTINUATION is a NEW unit with its own arrival, admission and window (THE DESIGN), under -//! the principal its handle recorded: it finds the handle (`work.find`, scoped to the instance and -//! the principal) and checks the call it carries is the one that was admitted (the digest in the -//! handle's row), takes the run ONCE (the instance's own ledger, then the host's one-time -//! `records.claim`), binds the handle (`work.resume`, principal-checked), asks its caller the -//! task's own rounds (`task_ask_caller`, answered through `tasks/update`), sends the call to the -//! member its walk picked, and settles the handle with the task's terminal state (`work.settle`). -//! A settle the handle refuses means `tasks/cancel` got there first. PARKED ON ITS CALLER (its -//! own round unanswered), it ends with the handle live; the `tasks/update` that answers the round, -//! on whichever node, nests the resume ([`Retry::Asked`]). -//! * AN UPSTREAM'S ASK, RELAYED (Law 11: busbar answers nothing on the caller's behalf; ARCHITECT -//! Q6): when the member answers the continuation's call with an `input_required` result, the task -//! parks `input_required` with the upstream's `inputRequests` verbatim, under busbar's sealed -//! state (bound to the principal, the tool and the member, nesting the upstream's own), and the -//! continuation ends with the handle live. The caller answers through `tasks/update`; once every -//! key is answered the update nests a NEW continuation ([`continuation_asked`], the retry): it -//! presents the state, which is opened, matched and spent once, binds the handle and sends the -//! call back to the SAME member with the caller's answers and the upstream's own state. -//! * THE TASK STORE IS HOST RECORDS (THE DESIGN, the mcp bullet): `tasks/get` is `work.find` plus -//! the task's live state, or its result, in the plane's records — the same answer on every -//! node and across a restart; `tasks/update` delivers input, writes the state it leaves, and nests -//! the run that continues; `tasks/cancel` settles the handle `cancelled`, which a running -//! continuation observes when it settles. -//! * A SETTLE THAT DOES NOT LAND is owed: its task is held unsettled and the next create's sweep -//! settles it again, until it lands. The same sweep settles the live tasks of its caller a process -//! that is gone left behind ([`tasks::TaskLease`]), so they never exhaust the bound -//! of live work (THE DESIGN: admission bounds live work; nothing evicts it). - -use std::task::Poll; - -use busbar_contract::abi::host::service::{ItemSpan, WORK_LIVE, WORK_SETTLED}; -use busbar_contract::abi::mechanism::call::Outcome as AbiOutcome; -use busbar_contract::abi::mechanism::call::Span; -use busbar_contract::abi::mechanism::check::SPAN_ABSENT; -use serde_json::{json, Map, Value}; - -use super::{ - clock_s, twin_of, CallUnit, CompletionHandle, DoorSeal, Held, McpDoor, Pending, Relay, - Services, Step, Ticket, Twin, -}; -use crate::call::{AdmittedCall, Leg, RelayedRetry}; -use crate::door::RECORD_TASK; -use crate::tool_arrival::Refusal; -use crate::tool_tasks::{ - self as tasks, run_digest, Status, Task, WorkRow, RUN_CLAIM_TTL_MS, TASK_PROTOCOL_ERROR_CODE, -}; - -/// What a unit is to the tasks extension. -pub(super) enum TaskUnit { - /// The unit that creates a task, part way through. - Create(Create), - /// A task's continuation. - Run(Box), - /// A `tasks/*` verb, part way through. - Verb(Verb), -} - -/// What a continuation that is not the task's first run carries. -#[derive(Clone)] -pub(super) enum Retry { - /// The caller's answer to a relayed ask: busbar's sealed state and the caller's - /// `inputResponses`. - Relayed { - /// Busbar's sealed state. - state: String, - /// The caller's answers. - responses: Value, - }, - /// The round of the task's own asks it was parked on, answered: the run resumes there. - Asked(usize), -} - -impl TaskUnit { - /// A continuation of task `reference`, running `params`; `retry` = the caller's answer to the - /// upstream's ask it carries back (the retry of a relayed ask). - pub(super) fn run(reference: String, params: Value, retry: Option) -> Self { - let digest = run_digest(¶ms); - TaskUnit::Run(Box::new(Run { - reference, - params, - digest, - retry, - leg: None, - begun: false, - phase: Phase::Bind, - find: None, - claim: None, - resume: None, - settle: None, - local: false, - handle: 0, - round: 0, - end: None, - at: None, - answered: false, - far_bytes: 0, - })) - } -} - -/// The creating unit's progress: the handle numbers its host calls were issued under (a call that -/// pends is re-issued under its first number), the handle it opened, and whether the continuation -/// was nested; this caller's index rows read, the tasks left behind it settles, and the records the -/// settles leave to strike. -#[derive(Default)] -pub(super) struct Create { - open: Option, - nest: Option, - opened: Option<(u64, String)>, - nested: bool, - swept: bool, - at: Option, - index: Listing, - indexed: bool, - left: Vec, - struck: Vec<(Vec, Vec)>, -} - -/// A task left behind, being settled: its reference, the handle number its find was issued under, -/// and whether it is done. -pub(super) struct Left { - reference: String, - find: Option, - done: bool, -} - -/// A verb's progress. -#[derive(Default)] -pub(super) struct Verb { - find: Option, - settle: Option, - result: Listing, - live: Listing, - at: Option, -} - -/// A listing of the task kind's records under a prefix, page by page: the handle number the page in -/// flight was issued under, where the next page starts, the records read, and whether it is done. -#[derive(Default)] -pub(super) struct Listing { - page: Option, - after: Option>, - rows: Vec<(Vec, Vec)>, - done: bool, -} - -/// A continuation. -pub(super) struct Run { - reference: String, - /// The call it runs: the published name, the arguments as admitted, the caller's `_meta`. - params: Value, - digest: String, - /// The retry of a relayed ask: the state and the caller's answers it carries. - retry: Option, - /// The upstream leg the retry's state opened to: the member it goes back to and its own state. - leg: Option, - /// Its caller's body has arrived: its phases run. - begun: bool, - phase: Phase, - find: Option, - claim: Option, - resume: Option, - settle: Option, - /// It holds the instance's own half of the one-time run. - local: bool, - handle: u64, - /// The task ask round it is on. - round: usize, - /// The terminal state it settles, and when it reached it. - end: Option, - at: Option, - /// A far end answered one of its rounds: the tool call it reports. - answered: bool, - /// The bytes of the documents its far ends answered with: the byte count it reports. - far_bytes: usize, -} - -/// Where a continuation is. -#[derive(Clone, Copy, PartialEq, Eq)] -enum Phase { - /// Finding, claiming and binding its handle. - Bind, - /// Asking its caller the task's own rounds: parked, it ends. - Ask, - /// Sending the call. - Call, - /// The call is with the far end. - Far, - /// Settling its handle. - Settle, - /// Its reply is written. - Done, -} - -/// The terminal state a continuation reached. -#[derive(Clone)] -enum End { - /// The tool ran: its result, as it came. - Completed(Value), - /// A protocol-level failure, in words. - Failed(String), -} - -/// The words of a task this caller has no task under: the one answer for an id that does not -/// exist and one that is another caller's (anti-enumeration). -const UNKNOWN_TASK: &str = "No task with that `taskId` exists for this caller."; - -/// The words of a verb with no `taskId`. -const NO_TASK_ID: &str = "`params.taskId` is required and must be a string."; - -/// The status of a `400` answer. -const STATUS_BAD_REQUEST: u32 = 400; - -/// The status a task that could not be started is refused with. -const STATUS_UNAVAILABLE: u32 = 503; - -/// The call-log reason of a call whose task could not be started. -const REASON_TASK_UNAVAILABLE: &str = "task_unavailable"; - -/// The bytes a work handle's answer is read into: its state byte and its record. -const WORK_BYTES: usize = 512; - -/// The most result chunks one page of a listing reads. -const PAGE: u32 = 64; - -/// The most stale result chunks one creating unit strikes as it writes. -const MAX_STRIKES: usize = 64; - -/// One span, blank. -pub(super) fn blank() -> ItemSpan { - let absent = Span { - offset: SPAN_ABSENT, - len: 0, - }; - ItemSpan { - key: absent, - value: absent, - } -} - -/// The handle numbered `slot` on `ticket`: the number it was first issued under, or the unit's next. -pub(super) fn handle(ticket: Ticket, issued: &mut u32, slot: &mut Option) -> CompletionHandle { - let seq = *slot.get_or_insert_with(|| { - let seq = *issued; - *issued += 1; - seq - }); - CompletionHandle { - ticket, - seq, - _reserved: 0, - } -} - -/// The kernel's wall clock in Unix milliseconds, on a fresh handle of `ticket`. -fn task_clock_ms(services: Services, ticket: Ticket, issued: &mut u32) -> u64 { - let mut fresh = None; - let h = handle(ticket, issued, &mut fresh); - services.clock_now(h).map_or(0, |r| r.wall_ns / 1_000_000) -} - -/// The caller's declared capabilities in `params`. -fn capabilities(params: Option<&Value>) -> Value { - params - .and_then(|p| p.get("_meta")) - .and_then(|m| m.get(crate::codec::META_CLIENT_CAPABILITIES)) - .cloned() - .unwrap_or(Value::Null) -} - -/// Whether `entry` answers this caller's call (`params`) with a task: the operator's declaration -/// crossed with the caller's. -pub(super) fn creates(entry: &crate::catalogue::ToolEntry, params: Option<&Value>) -> bool { - entry - .task_support - .creates_task(crate::call::client_declares_tasks(&capabilities(params))) -} - -/// Whether `unit`'s arrival is a `tools/call` that creates a task (its refusal by the caller's -/// budget is said as the served engine's task path said it). -pub(super) fn creates_task(unit: &CallUnit) -> bool { - let (Some(held), crate::tool_arrival::Disposition::Request { row, .. }) = - (unit.held.as_ref(), &unit.disposition) - else { - return false; - }; - row.op == crate::tool_ops::OP_TOOL_CALL - && unit.task.is_none() - && unit - .params - .as_ref() - .and_then(|p| p.get("name")) - .and_then(Value::as_str) - .and_then(|name| held.catalogue.tool(name)) - .is_some_and(|entry| creates(entry, unit.params.as_ref())) -} - -/// The refusal of a task-creating call by the caller's budget, in the served engine's words. -pub(super) fn budget_refused(id: Option, message: &str) -> Refusal { - Refusal { - status: crate::tool_arrival::STATUS_NOT_FOUND, - id, - code: crate::codec::CODE_REFUSED, - message: format!("this task was refused by your budget: {message}"), - data: Some(json!({ "reason": busbar_contract::vocab::REASON_NOT_GRANTED })), - } -} - -/// What a continuation's arrival carries: its reference, the call it runs, that call as the -/// `tools/call` body the one dispatch decides, and the caller's answer to a relayed ask it carries -/// back (`None` for the task's first run). -pub(super) type RunArrival = (String, Value, Vec, Option); - -/// THE CONTINUATION'S ARRIVAL on the task-run claim: its reference, the call it runs, and that call -/// as the `tools/call` body the one dispatch decides (its head fields mirrored from it), and — the -/// retry of a relayed ask — `relay: {requestState, inputResponses}`, or — the resume of an answered -/// round of the task's own asks — `resume: `. `None` for a body that is not one. -pub(super) fn run_arrival(body: &[u8]) -> Option { - let value: Value = serde_json::from_slice(body).ok()?; - let reference = value.get("taskId")?.as_str()?.to_string(); - let params = value.get("params")?.clone(); - params.get("name")?.as_str()?; - let retry = match (value.get("relay"), value.get("resume")) { - (None, None) => None, - (Some(relay), _) => Some(Retry::Relayed { - state: relay.get("requestState")?.as_str()?.to_string(), - responses: relay.get("inputResponses")?.clone(), - }), - (None, Some(round)) => Some(Retry::Asked(usize::try_from(round.as_u64()?).ok()?)), - }; - let call = json!({ - "jsonrpc": "2.0", - "id": 0, - "method": crate::codec::METHOD_TOOLS_CALL, - "params": params, - }); - Some((reference, params, serde_json::to_vec(&call).ok()?, retry)) -} - -/// The refused arrival of a continuation whose body names no task. -pub(super) fn unknown_arrival() -> Refusal { - Refusal { - status: STATUS_BAD_REQUEST, - id: None, - code: crate::codec::CODE_INVALID_PARAMS, - message: UNKNOWN_TASK.to_string(), - data: None, - } -} - -/// The call a continuation runs: the published name, the arguments as admitted (the answers to -/// busbar's own asks merged in), and the caller's `_meta` (its capabilities), its progress token cut -/// (nobody is listening for the continuation's progress). -fn run_params(admitted: &AdmittedCall, params: Option<&Value>) -> Value { - let mut out = Map::new(); - out.insert("name".into(), admitted.entry.namespaced.clone().into()); - out.insert("arguments".into(), admitted.arguments.clone()); - if let Some(mut meta) = params.and_then(|p| p.get("_meta")).cloned() { - if let Some(m) = meta.as_object_mut() { - m.remove("progressToken"); - } - out.insert("_meta".into(), meta); - } - Value::Object(out) -} - -/// `unit`'s answer `status` `body`, framed as its arrival asked, its reply done. -fn answer(unit: &mut CallUnit, status: u32, body: Vec) -> Step { - unit.pending = Some(Pending::answer(status, body, unit.framing.as_ref(), &[])); - Step::Write -} - -/// `unit`'s refusal. -fn refuse(unit: &mut CallUnit, refusal: &Refusal) -> Step { - answer(unit, refusal.status, refusal.body()) -} - -/// A JSON-RPC `-32602` answer in `message`. -fn invalid(id: &Value, message: &str) -> Refusal { - Refusal { - status: STATUS_BAD_REQUEST, - id: Some(id.clone()), - code: crate::codec::CODE_INVALID_PARAMS, - message: message.to_string(), - data: None, - } -} - -/// SETTLE handle `work` with `row`, on a fresh handle of `ticket`: whether it LANDED (settled now, or -/// settled first by another unit). One that pends or fails is owed ([`owe`]): its task is held -/// unsettled and the next create's sweep settles it again, until it lands. -fn settled(services: Services, ticket: Ticket, issued: &mut u32, work: u64, row: &[u8]) -> bool { - use busbar_contract::abi::sdk::services::ServiceError; - let mut fresh = None; - let h = handle(ticket, issued, &mut fresh); - matches!( - services.work_settle(h, work, row), - Poll::Ready(Ok(()) | Err(ServiceError::Declined(AbiOutcome::Refused))) - ) -} - -/// A settle of `task` that did not land, OWED: the instance holds the task unsettled, for the next -/// create's sweep. -fn owe(plane: &McpDoor, task: &Task) { - plane.tasks.with_all(|m| { - m.entry(task.id.clone()) - .or_insert_with(|| task.clone()) - .unsettled = true; - }); -} - -/// THE TASK'S LIVE STATE AS ITS RECORDS (THE DESIGN, the mcp bullet: the task store is -/// host records): its live chunks, any this instance wrote past them struck, and — `until` given — its caller's index row, -/// its run's lease `until` (`0`: no run holds it). -fn live_records(plane: &McpDoor, task: &Task, until: Option) -> Vec<(Vec, Vec)> { - let mut out = tasks::live_parts(&task.id, &task.live()); - let wrote = u32::try_from(out.len()).unwrap_or(u32::MAX); - let before = plane.tasks.with(&task.id, |t| { - t.map_or(0, |t| std::mem::replace(&mut t.live_chunks, wrote)) - }); - out.extend((wrote..before).map(|n| (tasks::live_key(&task.id, n), Vec::new()))); - if let Some(until) = until { - let lease = tasks::TaskLease { - until_ms: until, - updated_ms: task.stamps().1, - }; - out.push((task.index_key(), lease.bytes())); - } - out -} - -/// THE RECORDS A SETTLED TASK LEAVES, struck: its live chunks and its caller's index row. -fn struck_records(task: &Task) -> Vec<(Vec, Vec)> { - let mut out: Vec<(Vec, Vec)> = (0..task.live_chunks.max(tasks::LIVE_STRIKES)) - .map(|n| (tasks::live_key(&task.id, n), Vec::new())) - .collect(); - out.push((task.index_key(), Vec::new())); - out -} - -/// `records` of the task kind ride the unit's pending write. -fn ride(unit: &mut CallUnit, records: Vec<(Vec, Vec)>) { - if let Some(p) = unit.pending.as_mut() { - p.records - .extend(records.into_iter().map(|(k, v)| (RECORD_TASK, k, v))); - } -} - -/// THE TASK KIND'S RECORDS under `prefix`, read page by page into `l.rows` in key order (one page -/// only when `once`). `Ready(false)`: the host could not list them. -fn list_records( - services: Services, - ticket: Ticket, - issued: &mut u32, - prefix: &[u8], - once: bool, - l: &mut Listing, -) -> Poll { - use busbar_contract::abi::sdk::services::ServiceError; - while !l.done { - let h = handle(ticket, issued, &mut l.page); - let mut sizes = (64 * 1024, PAGE as usize); - let mut page: Option<(usize, Option>)> = None; - for _ in 0..2 { - let mut buf = vec![0u8; sizes.0]; - let mut spans = vec![blank(); sizes.1]; - match services.records_list( - h, - crate::door::KIND_TASK, - prefix, - l.after.as_deref(), - PAGE, - (&mut buf, &mut spans), - ) { - Poll::Pending => return Poll::Pending, - Poll::Ready(Ok(records)) => { - let mut n = 0; - for (key, value) in records.records() { - l.rows.push((key.to_vec(), value.to_vec())); - n += 1; - } - page = Some((n, records.last_key().map(<[u8]>::to_vec))); - break; - } - Poll::Ready(Err(ServiceError::Short { bytes, items })) => { - sizes = ( - usize::try_from(bytes).unwrap_or(usize::MAX).max(sizes.0), - usize::try_from(items).unwrap_or(usize::MAX).max(sizes.1), - ); - } - Poll::Ready(Err(_)) => return Poll::Ready(false), - } - } - let Some((n, last)) = page else { - return Poll::Ready(false); - }; - l.page = None; - if once || n < PAGE as usize || last.is_none() { - l.done = true; - } else { - l.after = last; - } - } - Poll::Ready(true) -} - -/// A TASK LEFT BEHIND, settled `cancelled`: found (scoped to this caller), its own row cancelled and -/// settled. The records its settle leaves to strike; none while it is owed. -fn settle_left( - plane: &McpDoor, - services: Services, - ticket: Ticket, - issued: &mut u32, - principal: &str, - left: &mut Left, - now: u64, -) -> Poll, Vec)>> { - if left.done { - return Poll::Ready(Vec::new()); - } - let h = handle(ticket, issued, &mut left.find); - let (mut buf, mut spans) = ([0u8; WORK_BYTES], [blank(); 1]); - let found = match services.work_find(h, &left.reference, &mut buf, &mut spans) { - Poll::Pending => return Poll::Pending, - Poll::Ready(found) => found, - }; - left.done = true; - let mut task = match found { - Ok(Some(f)) if f.state == WORK_LIVE => match WorkRow::read(f.record) { - Some(row) => Task::from_row(&left.reference, principal, f.handle, &row, None), - None => return Poll::Ready(Vec::new()), - }, - // Settled, or past its retention: only its records are left. - Ok(_) => { - let gone = Task::new(&left.reference, principal, 0, "", now); - return Poll::Ready(struck_records(&gone)); - } - // The host could not say: the next create reads it again. - Err(_) => return Poll::Ready(Vec::new()), - }; - task.cancel(now); - if settled(services, ticket, issued, task.handle, &task.row()) { - Poll::Ready(struck_records(&task)) - } else { - owe(plane, &task); - Poll::Ready(Vec::new()) - } -} - -// ── the creating unit ───────────────────────────────────────────────────────────────────────── - -/// THE CREATING UNIT: the sweep, the handle opened, the task held, the continuation nested, and the -/// `CreateTaskResult` written (with the call-log line `refused`/`task_created`, as the served engine -/// recorded it: at the moment the caller is answered nothing has gone out). -pub(super) fn create( - plane: &McpDoor, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, - admitted: &AdmittedCall, - params: Option<&Value>, -) -> Step { - let mut st = match unit.task.take() { - Some(TaskUnit::Create(st)) => st, - _ => Create::default(), - }; - let step = creating(plane, ticket, principal, unit, admitted, params, &mut st); - if unit.task.is_none() { - unit.task = Some(TaskUnit::Create(st)); - } - step -} - -fn creating( - plane: &McpDoor, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, - admitted: &AdmittedCall, - params: Option<&Value>, - st: &mut Create, -) -> Step { - let generation = unit.held.as_ref().map_or(0, |h| h.catalogue.generation()); - let unavailable = |unit: &mut CallUnit, why: &str| { - let refusal = Refusal { - status: STATUS_UNAVAILABLE, - id: Some(admitted.id.clone()), - code: crate::codec::CODE_REFUSED, - message: format!("this task could not be started: {why}"), - data: Some(json!({ "reason": REASON_TASK_UNAVAILABLE })), - }; - let ts = clock_s(plane.services, ticket, unit); - unit.pending = Some( - Pending::answer(refusal.status, refusal.body(), unit.framing.as_ref(), &[]).logged( - Some(&crate::call::task_line( - &admitted.entry, - REASON_TASK_UNAVAILABLE, - )), - principal, - generation, - ts, - ), - ); - Step::Write - }; - let Some(services) = plane.services else { - return unavailable(unit, "the host keeps no work handles"); - }; - let run = run_params(admitted, params); - let digest = run_digest(&run); - let now = match st.at { - Some(at) => at, - None => { - let at = task_clock_ms(services, ticket, &mut unit.issued); - st.at = Some(at); - at - } - }; - // THE SWEEP, as the served engine ran it, on a create: abandoned tasks cancelled (their - // continuations woken to see it), expired ones dropped. A task that reached its terminal state - // in hand with its handle unsettled is settled now; one whose settle does not land is owed - // again, until it lands. - if !st.swept { - st.swept = true; - let swept = plane.tasks.with_all(|m| tasks::sweep(m, now)); - if let Some(wake) = plane.wake { - swept.wake.iter().for_each(|t| wake.wake(*t)); - } - for (work, task) in &swept.settle { - if settled(services, ticket, &mut unit.issued, *work, &task.row()) { - st.struck.extend(struck_records(task)); - } else { - owe(plane, task); - } - } - plane.strikes.with_all(|m| { - for (id, chunks) in swept.strike { - m.insert(id, chunks); - } - }); - } - // THE TASKS LEFT BEHIND (THE DESIGN: admission bounds live work; nothing evicts it): this - // caller's live tasks no unit here runs whose run's lease lapsed — the process running it is gone — or - // that nothing moved past the abandonment ceiling, read from the plane's records and settled - // `cancelled` before the handle is opened, so the handles an earlier process left never exhaust - // `work.open`. From a submit, never a read or a timer. - if !st.indexed { - let prefix = tasks::index_prefix(principal); - if list_records( - services, - ticket, - &mut unit.issued, - &prefix, - true, - &mut st.index, - ) - .is_pending() - { - return Step::Pending; - } - st.indexed = true; - for (key, value) in std::mem::take(&mut st.index.rows) { - let Some(id) = key - .strip_prefix(prefix.as_slice()) - .and_then(|k| std::str::from_utf8(k).ok()) - else { - continue; - }; - let left = tasks::TaskLease::read(&value).is_some_and(|l| l.left_behind(now)); - let running = plane - .tasks - .get(&id.to_string()) - .is_some_and(|t| t.runner.is_some() || t.unsettled); - if left && !running { - st.left.push(Left { - reference: id.to_string(), - find: None, - done: false, - }); - } - } - } - for left in &mut st.left { - match settle_left( - plane, - services, - ticket, - &mut unit.issued, - principal, - left, - now, - ) { - Poll::Pending => return Step::Pending, - Poll::Ready(struck) => st.struck.extend(struck), - } - } - if st.opened.is_none() { - let row = WorkRow { - status: Status::Working, - created_ms: now, - updated_ms: now, - digest: digest.clone(), - }; - let h = handle(ticket, &mut unit.issued, &mut st.open); - let mut buf = [0u8; 64]; - let mut spans = [blank(); 2]; - match services.work_open( - h, - crate::door::KIND_TASK, - &row.bytes(), - &mut buf, - &mut spans, - ) { - Poll::Pending => return Step::Pending, - Poll::Ready(Ok(opened)) => { - let reference = opened.reference.to_string(); - plane.tasks.insert( - reference.clone(), - Task::new(&reference, principal, opened.handle, &digest, now), - ); - st.opened = Some((opened.handle, reference)); - } - Poll::Ready(Err(_)) => { - let step = unavailable( - unit, - "the deployment holds as many live tasks as it keeps, or keeps no store for them", - ); - ride(unit, std::mem::take(&mut st.struck)); - return step; - } - } - } - let Some((work, reference)) = st.opened.clone() else { - return Step::Declined; - }; - if !st.nested { - let body = - serde_json::to_vec(&json!({ "taskId": reference, "params": run })).unwrap_or_default(); - let h = handle(ticket, &mut unit.issued, &mut st.nest); - let mut buf = vec![0u8; 1024]; - let mut spans = vec![blank(); 8]; - match services.unit_nest( - h, - "POST", - crate::tool_claims::TASK_RUN_MOUNT, - &body, - &mut buf, - &mut spans, - ) { - // The continuation runs: its answer is its own, and nobody waits for it here. - Poll::Pending - | Poll::Ready(Ok(_)) - | Poll::Ready(Err(busbar_contract::abi::sdk::services::ServiceError::Short { - .. - })) => st.nested = true, - // The host runs no continuation for it (no nesting, too deep, too many at once): the - // task is settled failed — owed until the settle lands — and the call refused. - Poll::Ready(Err(_)) => { - if let Some(mut task) = plane.tasks.remove(&reference) { - task.fail( - TASK_PROTOCOL_ERROR_CODE, - "the host ran no continuation for this task".to_string(), - now, - ); - if !settled(services, ticket, &mut unit.issued, work, &task.row()) { - owe(plane, &task); - } - } - let step = unavailable(unit, "the host runs no continuation for it"); - ride(unit, std::mem::take(&mut st.struck)); - return step; - } - } - } - let Some(created) = plane.tasks.get(&reference).map(|t| t.created()) else { - return Step::Declined; - }; - // STALE RESULT CHUNKS, struck as this unit writes (a bounded few each time): the highest - // first, so what is left of a task's chunks is still keyed `0..left`. - let strikes: Vec<(u32, Vec, Vec)> = plane.strikes.with_all(|m| { - let mut out = Vec::new(); - while out.len() < MAX_STRIKES { - let Some((id, chunks)) = m.pop_first() else { - break; - }; - let room = u32::try_from(MAX_STRIKES - out.len()).unwrap_or(0); - let take = chunks.min(room); - for n in (chunks - take)..chunks { - out.push((RECORD_TASK, tasks::chunk_key(&id, n), Vec::new())); - } - if take < chunks { - m.insert(id, chunks - take); - } - } - out - }); - let ts = clock_s(plane.services, ticket, unit); - let mut pending = Pending::answer( - 200, - tasks::task_result(&admitted.id, created), - unit.framing.as_ref(), - &[], - ) - .logged( - Some(&crate::call::task_line( - &admitted.entry, - busbar_contract::vocab::REASON_TASK_CREATED, - )), - principal, - generation, - ts, - ); - pending.records.extend(strikes); - // ITS INDEX ROW, the run's lease taken: the continuation is nested at once. - let lease = tasks::TaskLease { - until_ms: now.saturating_add(tasks::RUN_LEASE_MS), - updated_ms: now, - }; - pending.records.push(( - RECORD_TASK, - tasks::index_key(principal, &reference), - lease.bytes(), - )); - unit.pending = Some(pending); - ride(unit, std::mem::take(&mut st.struck)); - Step::Write -} - -// ── the continuation ────────────────────────────────────────────────────────────────────────── - -/// Whether `unit` is a continuation. -pub(super) fn is_run(unit: &CallUnit) -> bool { - matches!(unit.task, Some(TaskUnit::Run(_))) -} - -/// THE CONTINUATION'S CALLER BODY has arrived (or arrived again, for another member of its walk): -/// its phases run from where they are. -pub(super) fn begin(plane: &McpDoor, ticket: Ticket, principal: &str, unit: &mut CallUnit) -> Step { - if let Some(TaskUnit::Run(run)) = unit.task.as_mut() { - run.begun = true; - // Another member of the walk: the call is sent again, to it. - if run.phase == Phase::Far { - run.phase = Phase::Call; - } - } - advance(plane, ticket, principal, unit) -} - -/// A continuation called again part way through a phase that waits on the host (a pend's wake): the -/// phase goes on. `None` for any other unit. -pub(super) fn resume( - plane: &McpDoor, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, -) -> Option { - match unit.task.as_ref() { - Some(TaskUnit::Run(run)) - if run.begun && matches!(run.phase, Phase::Bind | Phase::Settle) => {} - _ => return None, - } - Some(advance(plane, ticket, principal, unit)) -} - -/// THE FAR END ANSWERED THE CONTINUATION'S CALL (its rounds settled): the terminal state the served -/// engine's runner settled — the tool's result as it came (`completed`, `isError` and all); an -/// upstream failure or a refusal of an upstream's ask, `failed` — then the handle settled. -/// `answered` is the length of the document a far end answered with (`None`: no far end answered), -/// the tool call and the byte count the run reports. -/// THE FAR END ASKED THE CONTINUATION'S CALLER (Law 11, ARCHITECT Q6), the ask granted -/// ([`crate::call::settle_call`]): the task parks `input_required` with the upstream's `result`'s -/// `inputRequests` verbatim under busbar's sealed state, bound to the principal, the tool (`digest`: -/// the arguments as sent) and the member that asked, nesting the upstream's own state; the -/// continuation ends with the handle live, and `tasks/update` answers it. A deployment that cannot -/// seal the state fails the task in the refusal's words. `server` is the asking registration's, -/// `answered` the length of the document it answered with. -#[allow(clippy::too_many_arguments)] -pub(super) fn continuation_asked( - plane: &McpDoor, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, - result: &Value, - round: u32, - child: Option, - asked: (&str, &str, Option), -) -> Step { - let Some(TaskUnit::Run(mut run)) = unit.task.take() else { - return Step::Declined; - }; - let step = asking( - plane, ticket, principal, unit, &mut run, result, round, child, asked, - ); - unit.task = Some(TaskUnit::Run(run)); - step -} - -#[allow(clippy::too_many_arguments)] -fn asking( - plane: &McpDoor, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, - run: &mut Run, - result: &Value, - round: u32, - child: Option, - (server, digest, answered): (&str, &str, Option), -) -> Step { - if let Some(far) = answered { - run.answered = true; - run.far_bytes = run.far_bytes.saturating_add(far); - } - let Some(services) = plane.services else { - return not_run(unit, run); - }; - let leg = crate::ask::UpstreamLeg { - member: unit.member.clone().unwrap_or_default(), - state: result.get("requestState").cloned(), - round: round.saturating_add(1), - child, - }; - let name = run - .params - .get("name") - .and_then(Value::as_str) - .unwrap_or("") - .to_string(); - let sealed = { - let mut seal = DoorSeal { - services, - ticket, - issued: &mut unit.issued, - claim: &mut unit.claim, - spent: &plane.spent, - pending: false, - }; - let bind = |now| relay_bind(principal, &name, now); - crate::tool_door::seal_relayed( - &mut seal, - server, - bind, - digest, - leg, - tasks::TASK_RELAY_TTL_SECS, - ) - }; - let state = match sealed { - Ok(state) => state, - Err(refusal) => { - run.end = Some(End::Failed(refusal.to_string())); - run.phase = Phase::Settle; - return running(plane, ticket, principal, unit, run); - } - }; - let requests = result - .get("inputRequests") - .and_then(Value::as_object) - .cloned() - .unwrap_or_default(); - let now = task_clock_ms(services, ticket, &mut unit.issued); - let params = run.params.clone(); - let parked = plane.tasks.with(&run.reference, |t| match t { - Some(t) if !t.status().is_terminal() => { - t.park_relay(&requests, state, params, now); - t.runner = None; - Ok(t.clone()) - } - Some(t) => Err(t.status()), - None => Err(Status::Cancelled), - }); - // THE CONTINUATION ENDS with the handle live, its live state in the plane's records: the - // caller's answer, on whichever node, runs the next one. - match parked { - Ok(task) => { - let records = live_records(plane, &task, Some(0)); - reply(unit, run, Status::InputRequired, records) - } - Err(status) => reply(unit, run, status, Vec::new()), - } -} - -/// What a task's relayed-ask state is bound to: the principal, the tool (`name`, as published) and, -/// in the state's leg, the member; not the catalogue generation, because a task parked on a human -/// outlives a configuration move. -fn relay_bind<'a>(principal: &'a str, name: &'a str, now: u64) -> crate::ask::Bind<'a> { - crate::ask::Bind { - principal, - method: crate::codec::METHOD_TOOLS_CALL, - capability: name, - generation: 0, - now, - roots_epoch: 0, - } -} - -pub(super) fn continuation_answered( - plane: &McpDoor, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, - leg: Leg, - answered: Option, - body: &[u8], -) -> Step { - if let Some(TaskUnit::Run(run)) = unit.task.as_mut() { - if let Some(far) = answered { - run.answered = true; - run.far_bytes = run.far_bytes.saturating_add(far); - } - run.end = Some(match leg { - Leg::Done(value) => End::Completed(value), - Leg::Failed(reason) => End::Failed(format!("the MCP upstream call failed: {reason}")), - Leg::Asked => End::Failed( - serde_json::from_slice::(body) - .ok() - .and_then(|v| { - v.pointer("/error/message") - .and_then(Value::as_str) - .map(str::to_string) - }) - .unwrap_or_default(), - ), - }); - run.phase = Phase::Settle; - } - advance(plane, ticket, principal, unit) -} - -fn advance(plane: &McpDoor, ticket: Ticket, principal: &str, unit: &mut CallUnit) -> Step { - let Some(TaskUnit::Run(mut run)) = unit.task.take() else { - return Step::Declined; - }; - let step = running(plane, ticket, principal, unit, &mut run); - unit.task = Some(TaskUnit::Run(run)); - step -} - -/// The continuation's reply: `{taskId, status}`, the tool call it made reported (never a fee: the -/// unit that drew the fee is the one that created the task), and the result's chunks. -fn reply( - unit: &mut CallUnit, - run: &mut Run, - status: Status, - chunks: Vec<(Vec, Vec)>, -) -> Step { - run.phase = Phase::Done; - let body = serde_json::to_vec(&json!({ "taskId": run.reference, "status": status.token() })) - .unwrap_or_default(); - let mut pending = Pending::answer(200, body, None, &[]); - pending.units.clear(); - if run.answered { - pending = pending.counted(run.far_bytes); - } - pending.records = chunks - .into_iter() - .map(|(k, v)| (RECORD_TASK, k, v)) - .collect(); - unit.pending = Some(pending); - Step::Write -} - -/// A continuation that may not run (no such task for its principal, not the admitted call, or run -/// already): the unknown-task answer, nothing moved. -fn not_run(unit: &mut CallUnit, run: &mut Run) -> Step { - run.phase = Phase::Done; - let refusal = unknown_arrival(); - let mut pending = Pending::answer(refusal.status, refusal.body(), None, &[]); - pending.units.clear(); - unit.pending = Some(pending); - Step::Write -} - -/// THE RETRY'S STATE, opened ([`crate::ask::open_relayed`]): busbar's own, sealed for this -/// principal and this task's tool over the arguments it sends (the admitted ones with busbar's own -/// answers merged), nesting an upstream leg, and spent once. `None` for a state that is forged, -/// spent, another caller's or another call's, or for a task that is not this principal's or has -/// ended. -fn relayed_leg( - plane: &McpDoor, - services: Services, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, - run: &Run, - state: &str, -) -> Poll> { - let held = plane.tasks.get(&run.reference); - let Some(task) = held.filter(|t| t.owned_by(principal) && !t.status().is_terminal()) else { - return Poll::Ready(None); - }; - let arguments = tasks::merge_answers( - &run.params - .get("arguments") - .cloned() - .unwrap_or_else(|| json!({})), - task.answers(), - ); - let digest = crate::ask::digest_arguments(&arguments); - let name = run.params.get("name").and_then(Value::as_str).unwrap_or(""); - let mut seal = DoorSeal { - services, - ticket, - issued: &mut unit.issued, - claim: &mut unit.claim, - spent: &plane.spent, - pending: false, - }; - // A clock that cannot be read opens nothing: the state's window is not judged at the epoch. - let opened = seal.now().map(|now| { - crate::ask::open_relayed(state, relay_bind(principal, name, now), &digest, &mut seal) - }); - if seal.pending { - return Poll::Pending; - } - Poll::Ready(opened.and_then(Result::ok)) -} - -#[allow(clippy::too_many_lines)] -fn running( - plane: &McpDoor, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, - run: &mut Run, -) -> Step { - use busbar_contract::abi::sdk::services::ServiceError; - let Some(services) = plane.services else { - return not_run(unit, run); - }; - let Some(held) = unit.held.clone() else { - return not_run(unit, run); - }; - loop { - match run.phase { - Phase::Done | Phase::Far => return Step::Taken, - Phase::Bind => { - if run.handle == 0 { - let h = handle(ticket, &mut unit.issued, &mut run.find); - let (mut buf, mut spans) = ([0u8; WORK_BYTES], [blank(); 1]); - let found = match services.work_find(h, &run.reference, &mut buf, &mut spans) { - Poll::Pending => return Step::Pending, - Poll::Ready(Ok(Some(found))) => found, - Poll::Ready(_) => return not_run(unit, run), - }; - let Some(row) = WorkRow::read(found.record) else { - return not_run(unit, run); - }; - // THE CALL IT CARRIES IS THE CALL THAT WAS ADMITTED. - if row.digest != run.digest { - return not_run(unit, run); - } - if found.state == WORK_SETTLED { - // Cancelled before it started: nothing to run. - return reply(unit, run, row.status, Vec::new()); - } - run.handle = found.handle; - let (reference, work) = (run.reference.clone(), found.handle); - plane.tasks.with_all(|m| { - m.entry(reference.clone()).or_insert_with(|| { - let mut t = Task::from_row(&reference, principal, work, &row, None); - t.started = false; - t - }); - }); - } - // THE RUN, TAKEN ONCE: the instance's own half, then the host's one-time claim. The - // retry of a relayed ask is taken by its state instead, spent once. - if !run.local { - match run.retry.clone() { - Some(Retry::Relayed { state, .. }) => { - match relayed_leg(plane, services, ticket, principal, unit, run, &state) - { - Poll::Pending => return Step::Pending, - Poll::Ready(Some(leg)) => run.leg = Some(leg), - Poll::Ready(None) => return not_run(unit, run), - } - } - // THE RESUME of an answered round of the task's own asks: the update that - // answered it nested it once, and the host's one-time claim of that round - // takes it; it asks from that round on. - Some(Retry::Asked(round)) => { - let working = plane.tasks.get(&run.reference).is_some_and(|t| { - t.owned_by(principal) && t.status() == Status::Working - }); - if !working { - return not_run(unit, run); - } - run.round = round; - } - None => { - let took = plane.tasks.with(&run.reference, |t| match t { - Some(t) if !t.started && t.owned_by(principal) => { - t.started = true; - true - } - _ => false, - }); - if !took { - return not_run(unit, run); - } - } - } - run.local = true; - } - if run.leg.is_none() { - let h = handle(ticket, &mut unit.issued, &mut run.claim); - let key = match run.retry { - Some(Retry::Asked(round)) => format!("task-run:{}/{round}", run.reference), - _ => format!("task-run:{}", run.reference), - }; - match services.records_claim( - h, - crate::door::KIND_APPROVAL, - key.as_bytes(), - RUN_CLAIM_TTL_MS, - ) { - Poll::Pending => return Step::Pending, - // The host's ledger said this unit takes the run; or no store is bound, and - // the instance's own half was the whole gate. - Poll::Ready( - Ok(true) - | Err( - ServiceError::Declined(AbiOutcome::Refused) - | ServiceError::Unserved, - ), - ) => {} - Poll::Ready(_) => return not_run(unit, run), - } - } - // THE HANDLE, BOUND to this unit (principal-checked). - let h = handle(ticket, &mut unit.issued, &mut run.resume); - let (mut buf, mut spans) = ([0u8; WORK_BYTES], [blank(); 1]); - match services.work_resume(h, run.handle, &mut buf, &mut spans) { - Poll::Pending => return Step::Pending, - Poll::Ready(Ok(_)) => {} - Poll::Ready(Err(_)) => return not_run(unit, run), - } - let runner = (unit.key, ticket); - plane.tasks.with(&run.reference, |t| { - if let Some(t) = t { - t.runner = Some(runner); - } - }); - // The retry's caller already answered busbar's own rounds before the first call. - run.phase = if run.leg.is_some() { - Phase::Call - } else { - Phase::Ask - }; - } - Phase::Ask => { - let name = run.params.get("name").and_then(Value::as_str).unwrap_or(""); - let rounds = held.catalogue.tool(name).map_or_else(Vec::new, |entry| { - tasks::task_ask_rounds(&entry.task_ask_caller, &capabilities(Some(&run.params))) - }); - let now = task_clock_ms(services, ticket, &mut unit.issued); - while let Some(asks) = rounds.get(run.round) { - enum Then { - Next, - Park(Box), - Gone(Status), - } - let asked = (run.round, run.params.clone()); - let then = plane.tasks.with(&run.reference, |t| { - let Some(t) = t else { - return Then::Gone(Status::Cancelled); - }; - if t.status().is_terminal() { - return Then::Gone(t.status()); - } - t.park(asks.clone(), now); - if t.answered() { - return Then::Next; - } - t.asked = Some(asked); - t.runner = None; - Then::Park(Box::new(t.clone())) - }); - match then { - Then::Gone(status) => return reply(unit, run, status, Vec::new()), - // PARKED ON ITS CALLER: the continuation ends with the handle live and the - // task's live state in the plane's records; the `tasks/update` that answers - // the round, on whichever node, nests the resume. - Then::Park(task) => { - let records = live_records(plane, &task, Some(0)); - return reply(unit, run, Status::InputRequired, records); - } - Then::Next => run.round += 1, - } - } - plane.tasks.with(&run.reference, |t| { - if let Some(t) = t { - t.set_working(now); - } - }); - run.phase = Phase::Call; - } - Phase::Call => { - // A cancel that landed while it waited: observed here, before anything goes out. - if let Some(status) = plane - .tasks - .get(&run.reference) - .map(|t| t.status()) - .filter(|s| s.is_terminal()) - { - return reply(unit, run, status, Vec::new()); - } - let answers = plane - .tasks - .get(&run.reference) - .map(|t| t.answers().clone()) - .unwrap_or_default(); - match call(services, ticket, &held, unit, run, &answers) { - // THE RUN'S LEASE, renewed as its call goes out: the task's live state and its - // index row ride the request. - Ok(Step::Write) => { - if let Some(task) = plane.tasks.get(&run.reference) { - let timeout = unit - .member - .as_ref() - .and_then(|m| held.section.servers.get(m)) - .map_or(0, crate::tools_config::McpServerDefCfg::timeout_ms); - let now = task_clock_ms(services, ticket, &mut unit.issued); - let until = now - .saturating_add(timeout) - .saturating_add(tasks::RUN_LEASE_MS); - let records = live_records(plane, &task, Some(until)); - ride(unit, records); - } - return Step::Write; - } - Ok(step) => return step, - Err(end) => { - run.end = Some(end); - run.phase = Phase::Settle; - } - } - } - Phase::Settle => { - let Some(end) = run.end.clone() else { - return not_run(unit, run); - }; - let at = match run.at { - Some(at) => at, - None => { - let at = task_clock_ms(services, ticket, &mut unit.issued); - run.at = Some(at); - at - } - }; - let apply = |t: &mut Task| match &end { - End::Completed(value) => t.complete(value.clone(), at), - End::Failed(message) => t.fail(TASK_PROTOCOL_ERROR_CODE, message.clone(), at), - }; - let principal_task = plane.tasks.get(&run.reference).unwrap_or_else(|| { - Task::new(&run.reference, principal, run.handle, &run.digest, at) - }); - let mut settled = principal_task.clone(); - apply(&mut settled); - let h = handle(ticket, &mut unit.issued, &mut run.settle); - let won = match services.work_settle(h, run.handle, &settled.row()) { - Poll::Pending => return Step::Pending, - Poll::Ready(Ok(())) => Some(true), - // The handle was settled first: `tasks/cancel` won. - Poll::Ready(Err(ServiceError::Declined(AbiOutcome::Refused))) => Some(false), - // The host could not say: the instance's word stands, and the settle is owed to - // the next create's sweep. - Poll::Ready(Err(_)) => None, - }; - let reference = run.reference.clone(); - let (status, chunks) = plane.tasks.with_all(|m| { - let t = m.entry(reference.clone()).or_insert(principal_task); - t.runner = None; - match won { - Some(false) => { - t.cancel(at); - (t.status(), Vec::new()) - } - won => { - apply(t); - t.unsettled = won.is_none(); - let chunks = match (won, t.terminal()) { - (Some(true), Some(terminal)) => { - tasks::result_chunks(&reference, &terminal) - } - _ => Vec::new(), - }; - t.chunks = u32::try_from(chunks.len()).unwrap_or(0); - (t.status(), chunks) - } - } - }); - // A settle that landed leaves the task's live state and index row to strike. - let mut records = chunks; - if won.is_some() { - if let Some(task) = plane.tasks.get(&reference) { - records.extend(struck_records(&task)); - } - } - return reply(unit, run, status, records); - } - } - } -} - -/// THE CALL, sent to the member the continuation's walk picked: the arguments as admitted with the -/// task's answers merged in (`answers`), judged again by the argument guard on what is actually -/// about to be dispatched (the answers were never screened), a pool's twin taken as the synchronous -/// path takes it. Every host the arguments name is asked of `dest.judge` on `ticket`. `Err` = the -/// task ends here, in the engine's words. -fn call( - services: Services, - ticket: Ticket, - held: &Held, - unit: &mut CallUnit, - run: &mut Run, - answers: &Map, -) -> Result { - let name = run.params.get("name").and_then(Value::as_str).unwrap_or(""); - let Some(entry) = held.catalogue.tool(name).cloned() else { - return Err(End::Failed(format!( - "`{name}` is not a tool this server exposes" - ))); - }; - let arguments = tasks::merge_answers( - &run.params - .get("arguments") - .cloned() - .unwrap_or_else(|| json!({})), - answers, - ); - let schema = entry - .input_schema - .clone() - .unwrap_or_else(|| json!({ "type": "object" })); - let judged = crate::argguard::guard(&schema, &arguments, |dest| { - let h = handle(ticket, &mut unit.issued, &mut None); - super::dest_verdict(services, h, held, &entry, dest) - }); - if let Err(refused) = judged { - return Err(End::Failed(refused.to_string())); - } - let Some(member) = unit.member.clone() else { - // No member to send it to: the walk's terminal is the kernel's to render. - return Ok(Step::Taken); - }; - // A RELAYED ASK'S RETRY goes back to the member that asked, and no other. - if run.leg.as_ref().is_some_and(|leg| leg.member != member) { - return Ok(Step::Decline); - } - let entry = match twin_of(held, Some(&run.params), &member) { - Twin::Same => entry, - Twin::Declined => return Ok(Step::Decline), - Twin::Call(published) => match held.catalogue.tool(&published) { - Some(twin) => twin.clone(), - None => return Ok(Step::Decline), - }, - }; - let Some(def) = held.section.servers.get(&member) else { - return Ok(Step::Decline); - }; - // THE RETRY: the caller's answers and the upstream's own state, verbatim, on the next round. - let relay = run.leg.as_ref().map(|leg| { - let mut continuation = Map::new(); - if let Some(Retry::Relayed { responses, .. }) = &run.retry { - continuation.insert("inputResponses".into(), responses.clone()); - } - if let Some(state) = &leg.state { - continuation.insert("requestState".into(), state.clone()); - } - Box::new(RelayedRetry { - member: leg.member.clone(), - round: leg.round, - continuation: Value::Object(continuation), - child: leg.child.clone(), - }) - }); - let round = relay.as_ref().map_or(0, |r| r.round); - let admitted = AdmittedCall { - sent_digest: crate::ask::digest_arguments(&arguments), - entry, - arguments, - id: json!(0), - progress_token: None, - // The caller's declared capabilities: what an upstream's ask may be relayed to it for. - capabilities: capabilities(Some(&run.params)), - relay, - }; - let continuation = admitted.relay.as_ref().map(|r| r.continuation.clone()); - let Some(outbound) = - crate::call::outbound(&admitted, &member, def, round, continuation.as_ref()) - else { - return Err(End::Failed(format!( - "server `{member}` registers no `url:` this door can reach it at" - ))); - }; - unit.pending = Some(Pending::far(outbound).laned(&admitted.entry.namespaced)); - let mut relay = Relay::of(admitted); - relay.round = round; - unit.relay = Some(relay); - run.phase = Phase::Far; - Ok(Step::Write) -} - -// ── the verbs ───────────────────────────────────────────────────────────────────────────────── - -/// THE RUN THAT CONTINUES A TASK, nested by the `tasks/update` that answered it (under the updating -/// caller's principal, the task's own) on the task-run claim: `body` names the task, its call and -/// what continues it. The records the update leaves: the task's live state, its run's lease taken; -/// a host that runs no continuation for it fails the task, its settle owed until it lands. -fn nest_run( - plane: &McpDoor, - services: Services, - ticket: Ticket, - unit: &mut CallUnit, - task: &Task, - body: &Value, - at: u64, -) -> Vec<(Vec, Vec)> { - let body = serde_json::to_vec(body).unwrap_or_default(); - let mut fresh = None; - let h = handle(ticket, &mut unit.issued, &mut fresh); - let mut buf = vec![0u8; 1024]; - let mut spans = vec![blank(); 8]; - match services.unit_nest( - h, - "POST", - crate::tool_claims::TASK_RUN_MOUNT, - &body, - &mut buf, - &mut spans, - ) { - // The run continues: its answer is its own, and nobody waits for it here. - Poll::Pending - | Poll::Ready(Ok(_)) - | Poll::Ready(Err(busbar_contract::abi::sdk::services::ServiceError::Short { .. })) => { - live_records(plane, task, Some(at.saturating_add(tasks::RUN_LEASE_MS))) - } - Poll::Ready(Err(_)) => { - let failed = plane.tasks.with(&task.id, |t| { - t.and_then(|t| { - t.fail( - TASK_PROTOCOL_ERROR_CODE, - "the host ran no continuation for the caller's answer".to_string(), - at, - ) - .then(|| t.clone()) - }) - }); - match failed { - Some(t) if settled(services, ticket, &mut unit.issued, t.handle, &t.row()) => { - struck_records(&t) - } - Some(t) => { - owe(plane, &t); - Vec::new() - } - None => Vec::new(), - } - } - } -} - -/// What resolving a verb's `taskId` came to. -enum Resolved { - /// A host call pended. - Pending, - /// No task of this caller's. - Unknown, - /// The task. - Task(Box), -} - -/// A `tasks/*` VERB (`op`) under the caller's request id `id`: behind the `-32021` gate, then -/// `params.taskId`, then the task — `tasks/get` the detailed task with its result INLINED when -/// terminal, `tasks/update` the input delivered (an empty ack), `tasks/cancel` the handle settled -/// `cancelled` (an empty ack, idempotent on a settled task). -pub(super) fn verb( - plane: &McpDoor, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, - op: busbar_contract::ids::OpClassId, - id: &Value, -) -> Step { - let params = unit.params.clone(); - if !crate::call::client_declares_tasks(&capabilities(params.as_ref())) { - return refuse(unit, &crate::call::missing_tasks_capability(id)); - } - let Some(task_id) = params - .as_ref() - .and_then(|p| p.get("taskId")) - .and_then(Value::as_str) - .map(str::to_string) - else { - return refuse(unit, &invalid(id, NO_TASK_ID)); - }; - let Some(services) = plane.services else { - return refuse(unit, &invalid(id, UNKNOWN_TASK)); - }; - let mut st = match unit.task.take() { - Some(TaskUnit::Verb(st)) => st, - _ => Verb::default(), - }; - let step = verbing( - plane, - services, - ticket, - principal, - unit, - op, - id, - &task_id, - params.as_ref(), - &mut st, - ); - if unit.task.is_none() { - unit.task = Some(TaskUnit::Verb(st)); - } - step -} - -#[allow(clippy::too_many_arguments)] -fn verbing( - plane: &McpDoor, - services: Services, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, - op: busbar_contract::ids::OpClassId, - id: &Value, - task_id: &str, - params: Option<&Value>, - st: &mut Verb, -) -> Step { - use busbar_contract::abi::sdk::services::ServiceError; - let task = match resolve(plane, services, ticket, principal, unit, task_id, st) { - Resolved::Pending => return Step::Pending, - Resolved::Unknown => return refuse(unit, &invalid(id, UNKNOWN_TASK)), - Resolved::Task(task) => *task, - }; - let at = match st.at { - Some(at) => at, - None => { - let at = task_clock_ms(services, ticket, &mut unit.issued); - st.at = Some(at); - at - } - }; - let ack = |unit: &mut CallUnit| answer(unit, 200, crate::call::result(id, json!({}))); - if op == crate::tool_ops::OP_TASK_GET { - return answer(unit, 200, crate::call::result(id, task.detailed())); - } - if op == crate::tool_ops::OP_TASK_UPDATE { - // ABSENT is treated as empty rather than refused. - let responses = params - .and_then(|p| p.get("inputResponses")) - .and_then(Value::as_object) - .cloned() - .unwrap_or_default(); - let delivered = plane.tasks.with(&task.id, |t| { - t.map(|t| { - let was = t.status(); - let took = t.deliver(&responses, at); - let park = t.take_relay(); - let resume = if took - && park.is_none() - && was == Status::InputRequired - && t.status() == Status::Working - { - t.asked.take() - } else { - None - }; - (took, park, resume, t.clone()) - }) - }); - let (park, resume, t) = match delivered { - Some((false, ..)) => { - return refuse( - unit, - &invalid( - id, - &format!( - "this task already holds {} answer keys, the most one task retains. \ - `inputResponses` may still update any of its existing keys, but \ - adding a new one past that ceiling is refused.", - tasks::MAX_TASK_ANSWERS - ), - ), - ) - } - Some((true, park, resume, t)) => (park, resume, t), - None => return ack(unit), - }; - let records = if t.status().is_terminal() { - // A terminal task takes no input: nothing moved. - Vec::new() - } else if let Some(park) = park { - // THE CALLER'S ANSWER TO THE UPSTREAM'S ASK continues the task as a NEW unit: the - // retry, back to the member that asked. - let body = json!({ - "taskId": t.id, - "params": park.params, - "relay": { - "requestState": park.state, - "inputResponses": Value::Object(park.responses), - }, - }); - nest_run(plane, services, ticket, unit, &t, &body, at) - } else if let Some((round, call)) = resume { - // THE ROUND OF ITS OWN ASKS, ANSWERED: the run resumes there, as a NEW unit. - let body = json!({ "taskId": t.id, "params": call, "resume": round }); - nest_run(plane, services, ticket, unit, &t, &body, at) - } else if t.status() == Status::InputRequired { - live_records(plane, &t, Some(0)) - } else { - // A run holds it: its state is written, its lease left as the run took it. - live_records(plane, &t, None) - }; - let step = ack(unit); - ride(unit, records); - return step; - } - // `tasks/cancel`: idempotent on a settled task, and audited either way (the served engine's - // `mcp_task.cancel` row on every cancel of a task the caller holds). - let cancel_ack = |unit: &mut CallUnit| { - let step = ack(unit); - if let Some(p) = unit.pending.take() { - unit.pending = Some(p.audited(crate::call::AuditRow::task_cancel(&task.id))); - } - step - }; - if task.status().is_terminal() { - return cancel_ack(unit); - } - let mut cancelled = task.clone(); - cancelled.cancel(at); - let h = handle(ticket, &mut unit.issued, &mut st.settle); - let landed = match services.work_settle(h, task.handle, &cancelled.row()) { - Poll::Pending => return Step::Pending, - Poll::Ready(Ok(())) => Some(true), - // The continuation settled it first: its terminal state stands. - Poll::Ready(Err(ServiceError::Declined(AbiOutcome::Refused))) => Some(false), - // Owed to the next create's sweep, until it lands. - Poll::Ready(Err(_)) => None, - }; - if landed != Some(false) { - plane.tasks.with(&task.id, |t| { - if let Some(t) = t { - if t.cancel(at) { - t.unsettled = landed.is_none(); - } - } - }); - } - let step = cancel_ack(unit); - if landed == Some(true) { - ride(unit, struck_records(&cancelled)); - } - step -} - -/// The task `task_id` names FOR THIS CALLER, from the host's rows (THE DESIGN, the mcp bullet: the -/// task store is host records): `work.find` (scoped to the instance and the principal; every denial alike); a -/// live handle's live state in the plane's records; a settled one's row and the result written -/// there (or the result this instance holds, when it holds one the records could not). The -/// instance's own halves of the task are kept beside what the host says. -fn resolve( - plane: &McpDoor, - services: Services, - ticket: Ticket, - principal: &str, - unit: &mut CallUnit, - task_id: &str, - st: &mut Verb, -) -> Resolved { - let h = handle(ticket, &mut unit.issued, &mut st.find); - let (mut buf, mut spans) = ([0u8; WORK_BYTES], [blank(); 1]); - let (state, work, row) = match services.work_find(h, task_id, &mut buf, &mut spans) { - Poll::Pending => return Resolved::Pending, - Poll::Ready(Ok(Some(found))) => (found.state, found.handle, WorkRow::read(found.record)), - Poll::Ready(_) => return Resolved::Unknown, - }; - let held = plane.tasks.get(&task_id.to_string()); - if held.as_ref().is_some_and(|t| !t.owned_by(principal)) { - return Resolved::Unknown; - } - if state != WORK_LIVE { - if let Some(task) = held.as_ref().filter(|t| t.status().is_terminal()) { - return Resolved::Task(Box::new(task.clone())); - } - } - // A TERMINAL STATE THIS INSTANCE HOLDS AND STILL OWES THE HOST (a continuation the kernel - // refused, its settle owed to the next create's sweep) is newer than the live row the host - // keeps until then: the instance's own word answers, as the served engine's runner answered a - // call it could not carry at once. The host's live row would otherwise overwrite it. - if let Some(task) = held - .as_ref() - .filter(|t| t.unsettled && t.status().is_terminal()) - { - return Resolved::Task(Box::new(task.clone())); - } - // A host that lists none of the plane's records: what the instance holds answers. - let unlisted = - |held: Option| held.map_or(Resolved::Unknown, |t| Resolved::Task(Box::new(t))); - let Some(row) = row else { - return Resolved::Unknown; - }; - let mut task = if state == WORK_LIVE { - // ITS LIVE STATE, read back from its chunks. - let prefix = tasks::live_prefix(task_id); - match list_records( - services, - ticket, - &mut unit.issued, - &prefix, - false, - &mut st.live, - ) { - Poll::Pending => return Resolved::Pending, - Poll::Ready(false) => return unlisted(held), - Poll::Ready(true) => {} - } - let mut task = Task::from_row(task_id, principal, work, &row, None); - let bytes: Vec = st.live.rows.iter().flat_map(|(_, v)| v.clone()).collect(); - if let Some(live) = tasks::read_live(&bytes) { - task.take_live(&live); - } - task - } else { - let terminal = match row.status { - Status::Completed | Status::Failed => { - // THE RESULT, read back from its chunks. - let prefix = tasks::chunk_prefix(task_id); - match list_records( - services, - ticket, - &mut unit.issued, - &prefix, - false, - &mut st.result, - ) { - Poll::Pending => return Resolved::Pending, - Poll::Ready(false) => return unlisted(held), - Poll::Ready(true) => {} - } - match tasks::read_chunks(st.result.rows.iter().map(|(_, v)| v.as_slice())) { - Some(terminal) => Some(terminal), - None => return Resolved::Unknown, - } - } - _ => None, - }; - Task::from_row(task_id, principal, work, &row, terminal.as_ref()) - }; - let host = task.clone(); - plane.tasks.with_all(|m| match m.get_mut(task_id) { - Some(t) => { - t.hosted(host); - task = t.clone(); - } - None => { - m.insert(task_id.to_string(), host); - } - }); - Resolved::Task(Box::new(task)) -} - -// ── the kernel's own ends of a continuation ─────────────────────────────────────────────────── - -/// The kernel cancelled the unit on `ticket` (its caller went, its deadline passed, the node -/// drains): a continuation running on it leaves its task `cancelled` — the transition a caller's -/// `tasks/cancel` makes, as the served engine's runner settled a task its shutdown stopped. The -/// handle is settled by the next create's sweep. -/// -/// Answers the ids of the tasks it moved to `cancelled`: each is audited as the served engine's -/// runner audited a task its shutdown stopped (`mcp_task.cancel`). -pub(super) fn cancelled(plane: &McpDoor, ticket: Ticket) -> Vec { - let now = clock_now(plane); - let mut moved = Vec::new(); - plane.tasks.with_all(|m| { - for (id, t) in m.iter_mut() { - if t.runner.is_some_and(|(_, tk)| tk == ticket) { - let at = now.unwrap_or(t.stamps().1); - if t.cancel(at) { - t.unsettled = true; - moved.push(id.clone()); - } - t.runner = None; - } - } - }); - moved -} - -/// The kernel refused the continuation `unit` (`text`: its walk was exhausted, its budget said no) -/// before it settled its task: the task fails, as the served engine's runner failed a call that -/// could not be carried. A task another continuation took is not touched. -pub(super) fn continuation_refused(plane: &McpDoor, unit: u64, reference: &str, text: &str) { - let now = clock_now(plane); - plane.tasks.with(&reference.to_string(), |t| { - let Some(t) = t else { - return; - }; - let ran_here = t.runner.is_some_and(|(u, _)| u == unit); - if !ran_here && t.started { - return; - } - let at = now.unwrap_or(t.stamps().1); - if t.fail( - TASK_PROTOCOL_ERROR_CODE, - format!("the MCP upstream call failed: {text}"), - at, - ) { - t.unsettled = true; - } - t.started = true; - t.runner = None; - }); -} - -/// The reference of the continuation `unit`, when it is one. -pub(super) fn run_reference(unit: &CallUnit) -> Option { - match unit.task.as_ref() { - Some(TaskUnit::Run(run)) if run.phase != Phase::Done => Some(run.reference.clone()), - _ => None, - } -} - -/// The kernel's clock read off-unit (a call that may not pend), in Unix milliseconds. -fn clock_now(plane: &McpDoor) -> Option { - let services = plane.services?; - let h = CompletionHandle { - ticket: Ticket::NONE, - seq: 0, - _reserved: 0, - }; - services.clock_now(h).ok().map(|r| r.wall_ns / 1_000_000) -} diff --git a/crates/busbar-plane-mcp/src/endpoint.rs b/crates/busbar-plane-mcp/src/endpoint.rs deleted file mode 100644 index 12806eda39..0000000000 --- a/crates/busbar-plane-mcp/src/endpoint.rs +++ /dev/null @@ -1,299 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE `mcp:` ENDPOINT BLOCK: its grammar ([`McpCfg`]), its boot refusals ([`McpCfgError`]) and the -//! validated resource ([`McpResource`]) derived from it. Pure data and string rules, MOVED here from -//! the retired host crate with the `tools:` grammar beside it ([`crate::config`]), so the endpoint's -//! judging survives that crate's deletion unchanged: the same fields, the same refusals, the same -//! words. The composition root registers the block on the plane axis and lowers it through -//! [`McpResource::from_cfg`] at boot. - -use serde::{Deserialize, Serialize}; - -/// The top-level `mcp:` config block. Its mere PRESENCE mounts the MCP plane; its absence means the -/// deployment carries no MCP surface at all — no ingress, no metadata document, nothing added to the -/// route table. A gateway that is not an MCP server should not answer as one. -#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -#[serde(deny_unknown_fields)] -pub struct McpCfg { - /// The RFC 8707 resource indicator for this deployment: the canonical, absolute URI that names - /// busbar's MCP endpoint, and therefore the exact `aud` value every inbound token must carry. - /// - /// It is OPERATOR-CONFIGURED rather than derived from the request's `Host`, and that is the - /// whole point: deriving it from the request would let a caller choose its own audience by - /// sending a `Host` header, which turns the confused-deputy defence into a formality. It is also - /// what closes the multi-tenant gap — one deployment, one canonical audience, stated once. - /// - /// Its PATH must be the one path the claim table names: the mount is fixed, not configured, and - /// a URI naming any other path is refused at boot (CG-17). The scheme, host and port are the - /// operator's; the path is not. - pub canonical_uri: String, - /// RFC 9728 `authorization_servers`: the issuer identifiers of the authorization servers that - /// may mint tokens for this resource — in practice, the operator's IdP. At least one is - /// REQUIRED, because this list is the entire content of the answer a credential-less client came - /// for; an empty one advertises a resource nobody can obtain a token for. - #[serde(default)] - pub authorization_servers: Vec, - /// RFC 9728 `scopes_supported`: the scope values this resource understands. Advisory metadata — - /// authorization is decided by the caller's grant, never by what this list says. - #[serde(default)] - pub scopes_supported: Vec, - /// Browser origins accepted on the MCP ingress, for the `2026-07-28` `Origin` MUST. - /// - /// EMPTY IS THE DEFAULT AND IT MEANS "no browser origin is accepted", which is the safe posture - /// for a server whose clients are agents rather than pages: a request carrying NO `Origin` (every - /// non-browser client) is unaffected, and a request carrying one is refused unless the operator - /// listed it. The threat is DNS rebinding — a page on an attacker's origin resolving a name to - /// busbar's loopback address and driving the tool plane with the user's ambient credentials. - #[serde(default)] - pub allowed_origins: Vec, -} - -/// The VALIDATED MCP resource: `McpCfg` after every derivation and refusal has already happened, so -/// nothing downstream re-parses a URI or re-decides a path. -/// -/// Built once at boot. A config that cannot produce one does not boot — an MCP plane that is -/// half-configured is worse than one that is absent, because it answers. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct McpResource { - /// The canonical URI verbatim, as configured. THE audience. - canonical_uri: String, - /// The path component of `canonical_uri`, normalised to a leading and no trailing slash. The - /// ingress mount. - mount_path: String, - /// The RFC 9728 section 3.1 metadata path: `/.well-known/oauth-protected-resource` with the resource's - /// path INSERTED AFTER it, not before. This is the one detail of RFC 9728 that is easy to get - /// backwards, and getting it backwards means every compliant client's discovery 404s. - metadata_path: String, - /// The absolute form of `metadata_path`, which is what goes in the challenge — a client that has - /// no credential also has no reason to trust its own reconstruction of our origin. - metadata_url: String, - authorization_servers: Vec, - scopes_supported: Vec, - allowed_origins: Vec, -} - -/// Why an `mcp:` block was refused at boot. Every arm names the field and what a correct value looks -/// like: a boot refusal that does not say what to type is a boot refusal the operator works around. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum McpCfgError { - /// `canonical_uri` was empty or absent. - MissingCanonicalUri, - /// `canonical_uri` was not an absolute `http`/`https` URI. - CanonicalUriNotAbsolute(String), - /// `canonical_uri` carried a query or a fragment. RFC 8707 section 2 forbids a fragment outright, and a - /// query would make the identifier depend on parameter ordering — two spellings of one resource - /// is one spelling too many when the value is compared for equality. - CanonicalUriHasQueryOrFragment(String), - /// `canonical_uri` had no path, or only `/`. The resource must be distinguishable from the - /// deployment's root: mounting the MCP plane at `/` would put it in front of the LLM residual - /// and claim every path in the process. - CanonicalUriHasNoPath(String), - /// `canonical_uri` named a path other than the one the claim table names. Inbound paths are - /// compile-time claims in the Statement; a configured address naming another path would - /// advertise, and bind every token's audience to, a path nothing is served at. The mount is - /// fixed rather than configurable (CG-17). Carries the URI as configured and the path it named, - /// normalised. - CanonicalUriPathNotClaimed { - /// The URI as configured. - uri: String, - /// The path it named, normalised to a leading and no trailing slash. - path: String, - }, - /// `authorization_servers` was empty. - NoAuthorizationServers, - /// An `authorization_servers` entry was not an absolute `http`/`https` URI. - AuthorizationServerNotAbsolute(String), -} - -impl std::fmt::Display for McpCfgError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - McpCfgError::MissingCanonicalUri => write!( - f, - "mcp.canonical_uri is required: it is the audience every inbound token must carry \ - (RFC 8707) and the path the endpoint mounts at. Example: \ - `canonical_uri: https://gateway.example.com/mcp`" - ), - McpCfgError::CanonicalUriNotAbsolute(v) => write!( - f, - "mcp.canonical_uri `{v}` is not an absolute http(s) URI. It is compared for exact \ - equality against a token's `aud`, so it must be the same absolute string the \ - authorization server was asked to mint for. Example: \ - `https://gateway.example.com/mcp`" - ), - McpCfgError::CanonicalUriHasQueryOrFragment(v) => write!( - f, - "mcp.canonical_uri `{v}` carries a query or fragment. RFC 8707 resource indicators \ - carry neither: drop everything from the first `?` or `#`." - ), - McpCfgError::CanonicalUriHasNoPath(v) => write!( - f, - "mcp.canonical_uri `{v}` has no path. The MCP endpoint needs its own path so it \ - does not claim the whole deployment. Example: `https://gateway.example.com/mcp`" - ), - McpCfgError::CanonicalUriPathNotClaimed { uri, path } => write!( - f, - "mcp.canonical_uri `{uri}` names the path `{path}`, but the MCP endpoint is served \ - only at `{DEFAULT_MOUNT}`; the path is fixed, not configurable. Keep the scheme, \ - host and port and end the URI in `{DEFAULT_MOUNT}`. Example: \ - `https://gateway.example.com{DEFAULT_MOUNT}`" - ), - McpCfgError::NoAuthorizationServers => write!( - f, - "mcp.authorization_servers must list at least one issuer. It is the entire content \ - of the answer a client with no credential comes here for; with none, the `401` it \ - gets names nowhere to go. Example: \ - `authorization_servers: [https://login.example.com]`" - ), - McpCfgError::AuthorizationServerNotAbsolute(v) => write!( - f, - "mcp.authorization_servers entry `{v}` is not an absolute http(s) URI. An issuer \ - identifier is an absolute URL. Example: `https://login.example.com`" - ), - } - } -} - -/// The RFC 9728 section 3.1 well-known prefix. The resource's own path is appended AFTER this, per the -/// "path insertion" rule the RFC defines for a resource that is not at an origin root. -/// -/// The join itself is on the CODEC side, so the plane — which declares an OPEN claim on the composed -/// discovery path and may not name this crate — reads the same composer rather than a second one. -/// The well-known prefix went with it; there is one place the two are put together now. -/// -/// `DEFAULT_MOUNT` is THE ONE PATH this endpoint is served at: the compile-time claim, read from the -/// claim table rather than written a second time here, so the path validation accepts and the path -/// claimed cannot disagree. -use crate::{codec::protected_resource_metadata_path, tool_claims::DEFAULT_MOUNT}; - -impl McpResource { - /// Validate and derive. Every refusal is fail-closed at BOOT rather than at first request: an - /// operator finds out from a process that will not start, not from an agent that cannot connect. - pub fn from_cfg(cfg: &McpCfg) -> Result { - let uri = cfg.canonical_uri.trim(); - if uri.is_empty() { - return Err(McpCfgError::MissingCanonicalUri); - } - let (origin, path) = split_absolute(uri) - .ok_or_else(|| McpCfgError::CanonicalUriNotAbsolute(uri.to_string()))?; - if path.contains('?') || path.contains('#') || origin.contains('#') { - return Err(McpCfgError::CanonicalUriHasQueryOrFragment(uri.to_string())); - } - let mount_path = normalise_path(path); - if mount_path.is_empty() { - return Err(McpCfgError::CanonicalUriHasNoPath(uri.to_string())); - } - // The mount is not operator-configurable: one path is claimed and nothing else, so an - // address naming any other path is a boot refusal here rather than a deployment that - // advertises a path it does not serve (CG-17). - if mount_path != DEFAULT_MOUNT { - return Err(McpCfgError::CanonicalUriPathNotClaimed { - uri: uri.to_string(), - path: mount_path, - }); - } - if cfg.authorization_servers.is_empty() { - return Err(McpCfgError::NoAuthorizationServers); - } - for issuer in &cfg.authorization_servers { - if split_absolute(issuer.trim()).is_none() { - return Err(McpCfgError::AuthorizationServerNotAbsolute(issuer.clone())); - } - } - let metadata_path = protected_resource_metadata_path(&mount_path); - Ok(Self { - metadata_url: format!("{origin}{metadata_path}"), - canonical_uri: uri.to_string(), - mount_path, - metadata_path, - authorization_servers: cfg - .authorization_servers - .iter() - .map(|s| s.trim().to_string()) - .collect(), - scopes_supported: cfg.scopes_supported.clone(), - allowed_origins: cfg.allowed_origins.clone(), - }) - } - - /// THE audience. Compared for equality by the verifier; never parsed again. - pub fn canonical_uri(&self) -> &str { - &self.canonical_uri - } - - /// The ingress mount path (`/mcp` for `https://host/mcp`). - pub fn mount_path(&self) -> &str { - &self.mount_path - } - - /// The RFC 9728 metadata path this deployment serves the document at. - pub fn metadata_path(&self) -> &str { - &self.metadata_path - } - - /// The absolute metadata URL, for the `resource_metadata` challenge parameter. - pub fn metadata_url(&self) -> &str { - &self.metadata_url - } - - /// The issuers allowed to mint tokens for this resource, trimmed. - pub fn authorization_servers(&self) -> &[String] { - &self.authorization_servers - } - - /// The advisory scope values this resource lists. - pub fn scopes_supported(&self) -> &[String] { - &self.scopes_supported - } - - /// THE OPERATOR'S BROWSER-ORIGIN ALLOWLIST, as data. The plane keeps the data; the verdict on - /// an `Origin` is not made here. The empty allowlist admits no browser origin, which is the - /// documented default. - pub fn allowed_origins(&self) -> &[String] { - &self.allowed_origins - } -} - -/// Split an absolute `http(s)` URI into `(origin, path)`, or `None` when it is not one. -/// -/// Hand-written rather than pulled from a URL crate on purpose: what is needed is a STRICT -/// recogniser for one shape, and a permissive general-purpose parser is the wrong tool for a value -/// whose whole job is to be compared for exact equality. A lenient parse that accepts and normalises -/// `HTTPS://Host:443/mcp` would hand back a string that no longer equals the `aud` the IdP minted. -/// So: recognise, do not normalise. -fn split_absolute(uri: &str) -> Option<(&str, &str)> { - let rest = uri - .strip_prefix("https://") - .or_else(|| uri.strip_prefix("http://"))?; - // The authority must be non-empty, must not itself contain a scheme separator, and must not be - // the whole string's remainder only because the string ended at the scheme. - let authority_len = rest.find('/').unwrap_or(rest.len()); - if authority_len == 0 { - return None; - } - let scheme_len = uri.len() - rest.len(); - let split = scheme_len + authority_len; - Some((&uri[..split], &uri[split..])) -} - -/// `/mcp/` -> `/mcp`, `` -> ``, `/` -> ``. The same normalisation -/// core's plane dispatch applies to its mount path, so the derived mount and the dispatch mount are -/// the same string by construction rather than by two functions agreeing. -fn normalise_path(path: &str) -> String { - let trimmed = path.trim_matches('/'); - if trimmed.is_empty() { - return String::new(); - } - format!("/{trimmed}") -} - -// THE ENGINE BINDING for the plane's test binary: the one module that names the engine crate. Every -// App-needing test below reaches the engine through it (the neutral `testkit::engine_kit` seam). -// Gated on `test-support` alone (never bare `cfg(test)`, where no engine is in the closure): that is -// also the feature a dependent crate's test binary compiles the plane batteries in under -// (`admin_view::adminverbs_tests`), so those reach it too; in that feature-only build its helpers - -#[cfg(test)] -#[path = "tests/endpoint.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/framing.rs b/crates/busbar-plane-mcp/src/framing.rs deleted file mode 100644 index abe642460f..0000000000 --- a/crates/busbar-plane-mcp/src/framing.rs +++ /dev/null @@ -1,268 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE ANSWER'S FRAMING: one JSON-RPC answer re-framed as an event stream, with the log records -//! busbar writes about its own handling of the request riding ahead of it. Pure: the caller's -//! `Accept` value, its `_meta` and the finished answer in; the event-stream bytes out. -//! -//! A caller that did not ask for a stream is answered in JSON, unchanged: [`prefers_event_stream`] -//! is false for every `Accept` that puts `application/json` first, which is every MCP client that -//! has not deliberately asked otherwise. A caller that supplied a `progressToken` has asked for -//! progress, and a stream is the only shape progress can arrive in, so the token is itself a request -//! for one. Only a `200` whose body is one JSON document is re-framed; every other answer goes out as -//! it was written. -//! -//! The log records describe BUSBAR, never an upstream: an upstream's own records would arrive at -//! busbar's caller under busbar's name. Their `logger` is prefixed `busbar.` so that stays visible. - -use serde_json::{json, Value}; - -/// The `_meta` key a caller names the least severe log level it wants with. An extension key under -/// busbar's own prefix: this revision has no `logging/setLevel` (no session to remember it in), so -/// the level rides the request it applies to. -pub const META_LOGGING_LEVEL: &str = "io.busbar/loggingLevel"; - -/// The level a caller that names none is given. -pub const DEFAULT_LEVEL: &str = "info"; - -/// The protocol's eight severities, least severe first. -const SEVERITIES: &[&str] = &[ - "debug", - "info", - "notice", - "warning", - "error", - "critical", - "alert", - "emergency", -]; - -/// The head field that names an answer's media type. -pub const CONTENT_TYPE: &str = "content-type"; - -/// The head field a whole answer states its length in. -pub const CONTENT_LENGTH: &str = "content-length"; - -/// The head field that carries an event-stream answer's cache directive. -pub const CACHE_CONTROL: &str = "cache-control"; - -/// The media type of an event-stream answer. -pub const EVENT_STREAM: &str = "text/event-stream"; - -/// The media type of a JSON answer. -pub const JSON: &str = "application/json"; - -/// The cache directive an event-stream answer carries. -pub const NO_STORE: &str = "no-cache, no-store"; - -fn severity_of(name: &str) -> Option { - SEVERITIES.iter().position(|s| *s == name) -} - -/// One `notifications/message` record busbar writes about its own handling of a request. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct LogRecord { - /// Its severity. - pub level: &'static str, - /// Its logger, always under `busbar.`. - pub logger: &'static str, - /// What it says. - pub data: Value, -} - -impl LogRecord { - fn envelope(&self) -> Value { - json!({ - "jsonrpc": "2.0", - "method": "notifications/message", - "params": { - "level": self.level, - "logger": self.logger, - "data": self.data, - }, - }) - } -} - -/// The least severe level the caller asked for in `_meta`, or [`DEFAULT_LEVEL`]. A level the -/// protocol does not name is the default, never a refusal: the caller asked for a stream, and an -/// unknown filter is not a reason to deny it one. -#[must_use] -pub fn requested_level(meta: Option<&Value>) -> &'static str { - let named = meta - .and_then(|m| m.get(META_LOGGING_LEVEL)) - .and_then(|v| v.as_str()); - match named.and_then(severity_of) { - Some(i) => SEVERITIES[i], - None => DEFAULT_LEVEL, - } -} - -/// Whether a record at `level` passes a caller that asked for `requested`. An unknown name on -/// either side passes: the filter only ever removes what it can place. -#[must_use] -pub fn level_allows(requested: &str, level: &str) -> bool { - match (severity_of(requested), severity_of(level)) { - (Some(min), Some(at)) => at >= min, - _ => true, - } -} - -/// Whether the caller's `Accept` value prefers an event stream to JSON: by quality, then by -/// position, and never when the stream is listed at `q=0` or not at all. -#[must_use] -pub fn prefers_event_stream(accept: Option<&str>) -> bool { - let Some(accept) = accept else { - return false; - }; - let mut sse: Option<(f32, usize)> = None; - let mut json: Option<(f32, usize)> = None; - for (position, entry) in accept.split(',').enumerate() { - let mut parts = entry.split(';'); - let media = parts.next().unwrap_or("").trim().to_ascii_lowercase(); - let q = parts - .filter_map(|p| p.trim().strip_prefix("q=").map(str::trim)) - .find_map(|v| v.parse::().ok()) - .unwrap_or(1.0); - match media.as_str() { - EVENT_STREAM => sse = sse.or(Some((q, position))), - JSON => json = json.or(Some((q, position))), - _ => {} - } - } - let Some((sse_q, sse_pos)) = sse else { - return false; - }; - if sse_q <= 0.0 { - return false; - } - match json { - None => true, - Some((json_q, json_pos)) => { - if (sse_q - json_q).abs() > f32::EPSILON { - sse_q > json_q - } else { - sse_pos < json_pos - } - } - } -} - -/// The two records busbar writes about one request: what was dispatched (`debug`) and how it ended -/// (`info` on a `200`, else `warning`). `target` is the request's `Mcp-Name` member, where its -/// method has one. -#[must_use] -pub fn request_log(method: &str, target: Option, status: u32) -> Vec { - let ok = status == 200; - vec![ - LogRecord { - level: "debug", - logger: "busbar.mcp.dispatch", - data: json!({ - "message": "dispatching MCP method", - "method": method, - "target": target, - }), - }, - LogRecord { - level: if ok { "info" } else { "warning" }, - logger: "busbar.mcp.dispatch", - data: json!({ - "message": if ok { "MCP method completed" } else { "MCP method refused" }, - "method": method, - "httpStatus": status, - }), - }, - ] -} - -/// One answer as an event stream: each progress frame, then each log record, then the answer, one -/// `message` event each. `None` when `body` is not one JSON document (it goes out as it was). -#[must_use] -pub fn event_stream(body: &[u8], logs: &[LogRecord], progress: &[Value]) -> Option> { - let result = serde_json::from_slice::(body).ok()?; - let mut out = String::new(); - for frame in progress { - push_event(&mut out, frame); - } - for log in logs { - push_event(&mut out, &log.envelope()); - } - push_event(&mut out, &result); - Some(out.into_bytes()) -} - -/// Messages as an event stream: one `message` event each, in order. -#[must_use] -pub fn events(frames: &[Value]) -> Vec { - let mut out = String::new(); - for frame in frames { - push_event(&mut out, frame); - } - out.into_bytes() -} - -fn push_event(out: &mut String, value: &Value) { - use std::fmt::Write as _; - let _ = write!( - out, - "event: message\ndata: {}\n\n", - serde_json::to_string(value).unwrap_or_else(|_| "null".to_string()) - ); -} - -/// How one request's answer is framed, decided at its arrival. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Framing { - /// The method, for the log records. - pub method: String, - /// The `Mcp-Name` member's value, for the log records. - pub target: Option, - /// The least severe level the caller asked for. - pub level: &'static str, - /// Whether the caller asked for the frames only a stream carries (its log records and progress). - pub stream: bool, -} - -impl Framing { - /// The framing a request arriving with `accept` and `body` asks for; `None` = JSON. - #[must_use] - pub fn of(accept: Option<&str>, method: &str, body: &Value) -> Option { - let meta = body.get("params").and_then(|p| p.get("_meta")); - let asked_for_progress = meta - .and_then(|m| m.get("progressToken")) - .is_some_and(|v| !v.is_null()); - if !prefers_event_stream(accept) && !asked_for_progress { - return None; - } - let target = crate::codec::name_source_of(method) - .and_then(|source| body.get("params").and_then(|p| p.get(source)).cloned()); - Some(Framing { - method: method.to_string(), - target, - level: requested_level(meta), - stream: true, - }) - } - - /// The answer `(status, body)` framed: a `200` with a JSON body as an event stream with its log - /// records and `progress`, and `true`; anything else as it was, and `false`. - #[must_use] - pub fn frame(&self, status: u32, body: Vec, progress: &[Value]) -> (Vec, bool) { - if status != 200 { - return (body, false); - } - let logs: Vec = request_log(&self.method, self.target.clone(), status) - .into_iter() - .filter(|r| level_allows(self.level, r.level)) - .collect(); - match event_stream(&body, &logs, progress) { - Some(stream) => (stream, true), - None => (body, false), - } - } -} - -#[cfg(test)] -#[path = "tests/framing.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/identity.rs b/crates/busbar-plane-mcp/src/identity.rs deleted file mode 100644 index 2b71032a61..0000000000 --- a/crates/busbar-plane-mcp/src/identity.rs +++ /dev/null @@ -1,211 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE ROUTING KEY: `{server}_{tool}`, and nothing else. -//! -//! One rule holds up the whole client direction: -//! -//! > Route ONLY on bound identity = `(registered-server-id, namespaced-tool-name, schema/description -//! > hash)`. NEVER route on untrusted tool descriptions. -//! -//! This module is that rule as a TYPE. A [`ToolKey`] is the only thing dispatch resolves, the only -//! thing a scope grant names, and the only thing an audit row records. A tool's free-text -//! description is not a field of it, which is the point: a hostile description cannot influence a -//! routing decision that has no way to read one. -//! -//! ## Why the key is a PAIR that RENDERS, rather than a string that is PARSED -//! -//! `{server}_{tool}` is a rendering, and every rendering has an ambiguity question. Server `a_b` -//! offering tool `c` and server `a` offering tool `b_c` both render `a_b_c`. That is not cosmetic: -//! the rendered form is the VALUE of a `ScopeRef{kind: "mcp_tool"}` grant, so an ambiguous -//! rendering is a grant for one tool silently admitting another — a confused deputy created by a -//! separator. -//! -//! Two ways to close it, and only one of them survives contact with real tool names. Forbidding `_` -//! in TOOL names is a non-starter: `read_file` is the canonical MCP tool name. Forbidding `_` in -//! SERVER ids costs an operator nothing (they choose the id at registration, and `-` is right -//! there), and it makes the split at the FIRST `_` total and unambiguous. So that is the rule, it is -//! enforced at construction, and [`ToolKey::parse`] is exact rather than best-effort. -//! -//! ## What "bound identity" adds on top of the key -//! -//! The key names the tool. [`BoundIdentity`] is the key plus the schema/description DIGEST the -//! operator approved — the third component of the rule quoted above. Dispatch validates against it -//! and not the key, so a tool whose schema drifted under a live cache is refused even though its -//! name is unchanged. The digest lives in the engine's client catalogue; this module owns the naming. - -use std::fmt; - -/// Why a server id or tool name was refused. Both arms are construction-time refusals: a malformed -/// identity must never reach the point where it is compared against a grant. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum NameError { - /// A server id was empty, or a tool name was. - Empty { - /// Which name was empty: `server id` or `tool name`. - what: &'static str, - }, - /// A server id contained `_`, which would make `{server}_{tool}` ambiguous. See the module - /// header: this is the separator-as-confused-deputy case, refused at the door. - ServerIdHasSeparator(String), - /// A name carried a character that is not `[A-Za-z0-9_.-]`. Tool and server names travel in - /// scope-grant values, header values and audit resources; a name carrying whitespace, a quote, - /// a control character or a `/` is a name that renders differently depending on which of those - /// it is being written into, and a value compared for equality must have exactly one spelling. - IllegalCharacter { - /// Which name was refused: `server id` or `tool name`. - what: &'static str, - /// The refused name, verbatim. - name: String, - }, -} - -impl fmt::Display for NameError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - NameError::Empty { what } => write!(f, "an MCP {what} may not be empty"), - NameError::ServerIdHasSeparator(id) => write!( - f, - "MCP server id `{id}` contains `_`, which is the `{{server}}_{{tool}}` separator. \ - A server id containing the separator makes the namespaced tool name ambiguous, \ - and that name is the value of an `mcp_tool` scope grant — so the ambiguity would \ - be a grant for one tool admitting another. Use `-` instead." - ), - NameError::IllegalCharacter { what, name } => write!( - f, - "MCP {what} `{name}` contains a character outside `[A-Za-z0-9_.-]`. This name is \ - compared for exact equality as a scope-grant value and written into headers and \ - audit rows, so it must have exactly one spelling." - ), - } - } -} - -/// The `{server}_{tool}` separator, named once so the rendering, the parse and the refusal above -/// cannot disagree about what it is. -const SEP: char = '_'; - -fn legal(name: &str) -> bool { - name.bytes() - .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'-') -} - -/// A registered upstream MCP server's id. Validated at construction, so every later use is a -/// comparison rather than a re-check. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ServerId(String); - -impl ServerId { - /// Validate `id` as a server id: non-empty, only `[A-Za-z0-9_.-]`, and no `_` separator. - pub fn new(id: &str) -> Result { - if id.is_empty() { - return Err(NameError::Empty { what: "server id" }); - } - if !legal(id) { - return Err(NameError::IllegalCharacter { - what: "server id", - name: id.to_string(), - }); - } - if id.contains(SEP) { - return Err(NameError::ServerIdHasSeparator(id.to_string())); - } - Ok(Self(id.to_string())) - } - - /// The id, as registered. - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl fmt::Display for ServerId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) - } -} - -/// THE ROUTING KEY. A `(server, tool)` pair that RENDERS as `{server}_{tool}`. -/// -/// It is deliberately a pair and not a string: equality is structural, so two different tools can -/// never compare equal because their renderings collided, and the rendering is derived rather than -/// stored so there is no second copy to drift. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ToolKey { - server: ServerId, - tool: String, -} - -impl ToolKey { - /// Pair a validated server id with a tool name, refusing an empty or illegal tool name. - pub fn new(server: ServerId, tool: &str) -> Result { - if tool.is_empty() { - return Err(NameError::Empty { what: "tool name" }); - } - if !legal(tool) { - return Err(NameError::IllegalCharacter { - what: "tool name", - name: tool.to_string(), - }); - } - Ok(Self { - server, - tool: tool.to_string(), - }) - } - - /// The server half of the key. - pub fn server(&self) -> &ServerId { - &self.server - } - - /// The tool half of the key, as the upstream names it. - pub fn tool(&self) -> &str { - &self.tool - } - - /// The namespaced name: `{server}_{tool}`. THE value of an `mcp_tool` scope grant, and the name - /// an upstream tool is exposed under. - pub fn namespaced(&self) -> String { - format!("{}{SEP}{}", self.server.0, self.tool) - } - - /// Recover the pair from a rendering. EXACT, not best-effort: the split is at the FIRST `_`, and - /// that is total because a server id may not contain one. - /// - /// A rendering that does not round-trip is refused rather than guessed at. This function is what - /// a scope-grant value is read back through, so "guess" here means "admit a tool the operator - /// did not name". - pub fn parse(namespaced: &str) -> Result { - let (server, tool) = namespaced - .split_once(SEP) - .ok_or(NameError::Empty { what: "tool name" })?; - ToolKey::new(ServerId::new(server)?, tool) - } -} - -impl fmt::Display for ToolKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}{SEP}{}", self.server.0, self.tool) - } -} - -/// THE BOUND IDENTITY, all three components of it: the registered server, the namespaced tool, and -/// the schema/description digest the operator approved. -/// -/// Every one of the three is authenticated state — an id the operator registered, a name derived -/// from it, and a digest the operator approved. None of them is free text an upstream chose the -/// content of at refresh time. That is the whole property, and it is why this struct has no -/// `description` field: a routing decision made over this value cannot read one. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct BoundIdentity { - /// The routing key: the registered server and its tool. - pub key: ToolKey, - /// The approved `sha256:…` digest over the tool's name, description and input schema. Computed by the - /// engine's client catalogue (`tool_digest`). - pub digest: String, -} - -#[cfg(test)] -#[path = "tests/identity_tests.rs"] -mod identity_tests; diff --git a/crates/busbar-plane-mcp/src/jsonrpc.rs b/crates/busbar-plane-mcp/src/jsonrpc.rs deleted file mode 100644 index 59a44dd9d6..0000000000 --- a/crates/busbar-plane-mcp/src/jsonrpc.rs +++ /dev/null @@ -1,347 +0,0 @@ -//! Reading and writing this protocol's envelope, in the shape the existing codec already writes it. -//! -//! ## Reading is done as spans, not as a parse -//! -//! The kernel reads a body as spans, so the reader below walks the bytes once and reports where the -//! members it cares about are. It builds no document and allocates nothing per byte. The ACCEPT and -//! REFUSE decisions it makes are the same three the existing ingress makes, in the same order: the -//! version member must be exactly the one string; the method member must be a string; and the -//! identifier, when present, must be a string or a number and nothing else. -//! -//! ## Writing goes through the same serializer, on purpose -//! -//! This crate writes an envelope by building the same value the codec builds and handing it to the -//! same serializer at the same version. That is what makes byte-identity a property rather than a -//! hope: the member order on the wire is the serializer's, not this module's, and a test asserts -//! which order that is so a change to the serializer's configuration is a red here rather than a -//! silent reshaping of every answer this node gives. -//! -//! ## The two asymmetries that are easy to get wrong -//! -//! On a SUCCESSFUL answer the identifier member is OMITTED when there is none. On an ERROR it is -//! always present, and it is the empty value when there is none — because the specification makes -//! the member required on a response and names the empty value as the spelling for "no correlation", -//! and because a peer's own test for "is this a response" is whether the member is there at all. -//! -//! And every successful result carries a DISCRIMINATOR, stamped by this node rather than passed -//! through from whatever a server said about its own result. Three constructors exist rather than -//! one with a parameter, so which discriminator a caller receives is always visible at the call site -//! and can never be a value that arrived from a third party. - -use busbar_contract::bounded::Span; -use busbar_contract::wire::{Decode, Encode}; - -/// The version string every envelope of this protocol carries. -pub const VERSION: &str = "2.0"; - -/// The member carrying the protocol version. -pub const PTR_VERSION: &str = "/jsonrpc"; - -/// The member carrying the method name. -pub const PTR_METHOD: &str = "/method"; - -/// The member carrying the request identifier. -pub const PTR_ID: &str = "/id"; - -/// The member carrying the parameters. -pub const PTR_PARAMS: &str = "/params"; - -/// The member carrying the result of an answer. -pub const PTR_RESULT: &str = "/result"; - -/// The member carrying the error of an answer. -pub const PTR_ERROR: &str = "/error"; - -/// The member carrying an error's code. -pub const PTR_ERROR_CODE: &str = "/error/code"; - -/// The member carrying the discriminator of a result. -pub const PTR_RESULT_TYPE: &str = "/result/resultType"; - -/// The member saying whether a tool reported that it itself failed. -pub const PTR_IS_ERROR: &str = "/result/isError"; - -/// The discriminator on a result this node is handing over as finished. -pub const RESULT_TYPE_COMPLETE: &str = "complete"; - -/// The discriminator on a result that asks the caller for something first. -pub const RESULT_TYPE_INPUT_REQUIRED: &str = "input_required"; - -/// The discriminator on a result that hands back a task rather than an answer. -pub const RESULT_TYPE_TASK: &str = "task"; - -// THE ERROR-CODE TABLE IS THE DIALECT'S, AND THERE IS ONE OF IT. -// -// Until the codec fold these ten lived in a crate this one could only READ, so they were restated -// here and pinned to the originals by construction (`= busbar_mcp_codec::codec::CODE_…`). The -// dialect came home (#39), and a value restated inside the crate that defines it is a second name -// for one number — the shape a divergence hides in, and one a `pub use` cannot grow. So the -// restatement is gone and these are re-exports: one definition, in `codec`, where the rest of the -// MCP wire vocabulary is, reachable on the path callers already spell. -pub use crate::codec::{ - CODES, CODE_HEADER_MISMATCH, CODE_INTERNAL, CODE_INVALID_PARAMS, CODE_INVALID_REQUEST, - CODE_METHOD_NOT_FOUND, CODE_MISSING_CLIENT_CAPABILITY, CODE_PARSE_ERROR, CODE_REFUSED, - CODE_UNSUPPORTED_PROTOCOL_VERSION, CODE_UPSTREAM_UNAVAILABLE, RETIRED_CODES, -}; - -/// What kind of scalar the identifier member held. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum IdShape { - /// A quoted string. - Str, - /// A number. - Number, -} - -/// One envelope, as the reader found it. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Envelope { - /// Where the method name is, quotes included. - pub method: Span, - /// Where the identifier is, and what shape it was — absent on a notification. - pub id: Option<(Span, IdShape)>, - /// Where the parameters are, where the caller sent any. - pub params: Option, -} - -impl Envelope { - /// The method name, with its quotes stripped. - #[must_use] - pub fn method_str<'b>(&self, body: &'b [u8]) -> Option<&'b str> { - let raw = body.get(self.method.start..self.method.end)?; - let inner = raw.strip_prefix(b"\"")?.strip_suffix(b"\"")?; - core::str::from_utf8(inner).ok() - } - - /// The identifier's raw bytes, exactly as they arrived. - /// - /// Quotes included on a string identifier, because those bytes are what an answer must echo. - #[must_use] - pub fn id_bytes<'b>(&self, body: &'b [u8]) -> Option<&'b [u8]> { - let (span, _) = self.id?; - body.get(span.start..span.end) - } - - /// Whether this envelope obliges an answer. - #[must_use] - pub fn is_request(&self) -> bool { - self.id.is_some() - } -} - -/// Every pointer this plane declares over a REQUEST body, in one table. -/// -/// The table is what the unit's span view is built from: the plane resolves these once, at the one -/// step entitled to read the bytes, and the loop reads the spans off the draft rather than scanning -/// the body again. A pointer named here that the body does not carry is simply absent from the -/// view, which is how "not sent" stays distinguishable from "sent empty". -pub const REQUEST_PTRS: &[&str] = &[ - PTR_VERSION, - PTR_METHOD, - PTR_ID, - PTR_PARAMS, - PTR_PARAMS_NAME, - PTR_PARAMS_URI, - PTR_PARAMS_TASK_ID, - PTR_PARAMS_META, -]; - -/// Every pointer this plane declares over a RESPONSE body, in one table. -pub const RESPONSE_PTRS: &[&str] = &[ - PTR_ID, - PTR_RESULT, - PTR_RESULT_TYPE, - PTR_IS_ERROR, - PTR_ERROR, - PTR_ERROR_CODE, -]; - -/// Where a request's subject is, for the methods whose subject is a name. -pub const PTR_PARAMS_NAME: &str = "/params/name"; - -/// Where a request's subject is, for the methods whose subject is a resource. -pub const PTR_PARAMS_URI: &str = "/params/uri"; - -/// Where a request's subject is, for the methods whose subject is a task. -pub const PTR_PARAMS_TASK_ID: &str = "/params/taskId"; - -/// The caller's own metadata block. -pub const PTR_PARAMS_META: &str = "/params/_meta"; - -/// Read one envelope out of a request body. -/// -/// # Errors -/// Returns a decode error when the bytes are not this protocol's shape: no version member or the -/// wrong one, no method member or a method that is not a string, or an identifier that is present -/// and is neither a string nor a number. -pub fn read(body: &[u8]) -> Result { - let at = |ptr: &str| match busbar_contract::spans::resolve_pointer(body, ptr) { - busbar_contract::spans::Resolved::Found(span) => Some(span), - _ => None, - }; - - let version = at(PTR_VERSION).ok_or(Decode::MissingDeclaredFact)?; - if body.get(version.start..version.end) != Some(b"\"2.0\"".as_slice()) { - return Err(Decode::Malformed); - } - - let method = at(PTR_METHOD).ok_or(Decode::MissingDeclaredFact)?; - let method_bytes = body - .get(method.start..method.end) - .ok_or(Decode::Malformed)?; - if !(method_bytes.starts_with(b"\"") - && method_bytes.len() >= 2 - && method_bytes.ends_with(b"\"")) - { - return Err(Decode::Malformed); - } - - // An identifier written as the empty value is REFUSED rather than read as absent, and this - // protocol is stricter than the underlying one on exactly that point: a notification is a - // message with NO identifier member, and a member present and empty is a caller who meant to - // correlate and wrote it wrongly. - let id = match at(PTR_ID) { - None => None, - Some(span) => { - let raw = body.get(span.start..span.end).ok_or(Decode::Malformed)?; - match id_shape(raw) { - Some(shape) => Some((span, shape)), - None => return Err(Decode::Malformed), - } - } - }; - - Ok(Envelope { - method, - id, - params: at(PTR_PARAMS), - }) -} - -/// What shape an identifier's raw bytes are, or nothing when they are neither shape. -#[must_use] -fn id_shape(raw: &[u8]) -> Option { - match raw.first()? { - b'"' if raw.len() >= 2 && raw.ends_with(b"\"") => Some(IdShape::Str), - b'-' | b'0'..=b'9' if is_number(raw) => Some(IdShape::Number), - _ => None, - } -} - -/// Whether a run of bytes is a NUMBER, by the one grammar a document has for writing one. -/// -/// The test used to be "starts like a number, and every byte after that is a digit or one of the -/// punctuation marks a number can contain". That admits `1.2.3`, `007`, `1e`, `--1` and `1e+` — none -/// of which is a number, all of which the ANSWER path then cannot write, because the answer echoes -/// the identifier by parsing these same bytes into a value. So the reader admitted a request whose -/// every possible reply, success or refusal, failed to encode: the caller was told nothing at all, -/// which is the one outcome this protocol has no spelling for. Reading it the way it will be -/// written is what keeps the two ends agreeing. -fn is_number(raw: &[u8]) -> bool { - let mut i = 0; - if raw.first() == Some(&b'-') { - i += 1; - } - // A leading zero stands alone: `0` is a number and `007` is not one. - match raw.get(i) { - Some(b'0') => i += 1, - Some(b'1'..=b'9') => { - while matches!(raw.get(i), Some(b'0'..=b'9')) { - i += 1; - } - } - _ => return false, - } - // A point, if present, is followed by at least one digit. - if raw.get(i) == Some(&b'.') { - i += 1; - let digits = i; - while matches!(raw.get(i), Some(b'0'..=b'9')) { - i += 1; - } - if i == digits { - return false; - } - } - // An exponent, if present, is a sign at most and then at least one digit. - if matches!(raw.get(i), Some(b'e' | b'E')) { - i += 1; - if matches!(raw.get(i), Some(b'+' | b'-')) { - i += 1; - } - let digits = i; - while matches!(raw.get(i), Some(b'0'..=b'9')) { - i += 1; - } - if i == digits { - return false; - } - } - i == raw.len() -} - -/// The identifier as a document value, for building an answer that echoes it. -/// -/// # Errors -/// Returns an encode error when the bytes are not a value. -pub fn id_value(raw: &[u8]) -> Result { - serde_json::from_slice(raw).map_err(|_| Encode::Unrepresentable) -} - -/// One successful answer, as bytes, with the discriminator this node chose. -/// -/// The discriminator is stamped INTO the result, replacing anything a server said about its own -/// result. That is deliberate and it is the safety property, not a convenience: a server that -/// answered with a demand for the caller's authority would otherwise have that demand handed on -/// under this node's name and this node's authentication. -/// -/// # Errors -/// Returns an encode error when the result bytes are not a document. -pub fn success( - id: Option<&serde_json::Value>, - result_bytes: &[u8], - result_type: &str, -) -> Result, Encode> { - let mut result: serde_json::Value = - serde_json::from_slice(result_bytes).map_err(|_| Encode::Unrepresentable)?; - if let Some(object) = result.as_object_mut() { - object.insert("resultType".into(), result_type.into()); - } - let mut envelope = serde_json::Map::new(); - envelope.insert("jsonrpc".into(), VERSION.into()); - // OMITTED when there is none: on the success path the member is written only if there is one. - if let Some(id) = id { - envelope.insert("id".into(), id.clone()); - } - envelope.insert("result".into(), result); - serde_json::to_vec(&serde_json::Value::Object(envelope)).map_err(|_| Encode::Unrepresentable) -} - -/// One refused or failed answer, as bytes. -/// -/// The identifier member is ALWAYS written, and it is the empty value when there is none. -/// -/// # Errors -/// Returns an encode error when the value cannot be written. -pub fn error( - id: Option<&serde_json::Value>, - code: i64, - message: &str, - data: Option, -) -> Result, Encode> { - let mut err = serde_json::Map::new(); - err.insert("code".into(), code.into()); - err.insert("message".into(), message.into()); - if let Some(d) = data { - err.insert("data".into(), d); - } - let mut envelope = serde_json::Map::new(); - envelope.insert("jsonrpc".into(), VERSION.into()); - envelope.insert("id".into(), id.cloned().unwrap_or(serde_json::Value::Null)); - envelope.insert("error".into(), serde_json::Value::Object(err)); - serde_json::to_vec(&serde_json::Value::Object(envelope)).map_err(|_| Encode::Unrepresentable) -} - -#[cfg(test)] -#[path = "tests/jsonrpc.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/lib.rs b/crates/busbar-plane-mcp/src/lib.rs deleted file mode 100644 index bb97fc80da..0000000000 --- a/crates/busbar-plane-mcp/src/lib.rs +++ /dev/null @@ -1,146 +0,0 @@ -//! The MCP plane: what bytes mean, for a protocol that names its operation in the body. -//! -//! ## What this crate is -//! -//! An ADAPTER. Every method of the plane kind here is a few lines over a codec that already exists: -//! the envelope shape, the method vocabulary, the error code table, the result discriminators and -//! the record kinds. No wire format is written twice, because a wire format written twice is two -//! wire formats that will disagree, and the one that disagrees quietly is the one that reaches a -//! customer. -//! -//! ## What this crate is not -//! -//! It holds no governance, no breaker, no hook seat, no approval decision and no arithmetic over a -//! metered quantity. Those are units, and a unit is on the far side of the kernel from a plane. The -//! metering method here returns LOCATORS — the class, where the number is, and the number the codec -//! already read — and never a price, never a hold and never a decision. The routing method returns a -//! plan and never a connection. Nothing in this crate opens a socket, reads a file or reads a clock -//! other than the one the context hands it. -//! -//! ## What it holds across calls -//! -//! Nothing. The plane is a value with no interior mutability, asserted by a test rather than by a -//! comment. What state a held stream needs lives in the kernel-held per-connection codec state, -//! which the kernel hands in and takes back. -//! -//! ## Where this adapter had to write something down twice, and why -//! -//! The codec crate holds the method table, the error codes and the result discriminators, and it -//! holds all of them where only its own crate can see them; the envelope reader and writer live one -//! crate further away still, on the kernel side, where a plane may not name them at all. This crate -//! may not widen any of that. So those things are written once more here — and each of them is -//! PINNED by a test that reads the codec's own source or the conformance battery's own tables. A -//! copy that is checked is not a second opinion. A copy that is not checked is, and there are none -//! of those here. -//! -//! The full list of places the contract did not fit this protocol is in the notes each module -//! carries: the mount path that is configured where a claim must be a constant, the correlation type -//! that cannot hold a named identifier, the arena that cannot hold a span table, and the -//! introspection verb that takes no argument. - -#![forbid(unsafe_code)] -#![deny(missing_docs)] - -/// The session revisions raised into, and lowered out of, the one dispatch. -pub mod adapt; -pub mod answer; -/// The argument half of the dispatch SSRF guard: a schema-aware walk judging every URL and host a -/// call's arguments carry. -pub mod argguard; -pub mod ask; -pub mod call; -pub mod catalogue; -pub mod checks; -pub mod client; -pub mod codec; -pub mod diagnostics; -pub mod door; -pub mod endpoint; -pub mod framing; -pub mod identity; -pub mod jsonrpc; -/// The line carrier's meanings: one JSON-RPC message per line, each its own unit. -pub mod line; -pub mod tool_arrival; -pub mod tool_claims; -pub mod tool_door; -pub mod tool_facts; -pub mod tool_meta; -pub mod tool_ops; -/// The `transport: stdio` servers: one long-lived child per server, its messages correlated by id. -pub mod tool_program; -pub mod tool_scope; -/// SEP-2663, the tasks extension: a task's state, shapes, sweep and durable rows. -pub mod tool_tasks; -pub mod tools_config; - -/// THE PLANE'S DECLARED METADATA (`declares.json`): its manifest `declares` section, the static -/// statement the root reads for this linked plugin as it reads every default-linked plugin's (the -/// codes its catalog holds, [`diagnostics::DIAGNOSTICS`]; a test holds the two equal) and its -/// breaker fact (ARCHITECT Q4): one transient failure below the trip threshold never benches a -/// member, the 1.5.5 MCP client leg's posture. -pub const DECLARES: &str = include_str!("declares.json"); -/// THE DOOR CRATE'S CONVENTIONAL PATH, `::plane_door::door`: what a test-linked `door:` row's -/// generated table names (the build scripts of the crates that test-link this door). The door -/// itself is [`tool_door::door`]. -pub mod plane_door { - pub use crate::tool_door::door; -} -pub mod reads; -pub mod record; -/// The dated revisions of the one MCP dialect, and their negotiation. -pub mod revision; -pub mod sanitize; -/// The sealed `requestState`: busbar's own ask state, signed by the host. -pub mod seal; -/// `subscriptions/listen`, as a state machine any carrier drives. -pub mod subscribe; -pub mod tool_records; -/// Bounded, owner-bound session state for the session revisions. -pub mod tool_sessions; -/// The trust surface: approvals, sightings, the derived state and the admin views. -pub mod trust; - -/// THE REGISTRY KEY MCP IS KNOWN BY, in the protocol registry and in the plane registry alike. -/// -/// Named ONCE, here, because three declarations read it and all three must agree: this crate's -/// [`plane`] kind `KEY`, the engine's `PLANE_DECLARATION.key`, and the engine's `ProtocolDecl.name`. It -/// lives on the PLANE side because a plane is the thing being named — a key spelled engine-side -/// would be a key the plane could only copy, which is how two answers to "what is this plane -/// called" start to differ. -pub const PLANE_KEY: &str = "mcp"; - -use busbar_contract::plugin::{AbiVersion, Kind, Plugin}; - -/// The MCP plane. -/// -/// It carries no field: it is the type the plane's declarations (`PlaneMeta`) and its identity -/// (`Plugin`) hang on, and the served door reads those. There is no cell here, no lock and no -/// atomic: the purity test asserts that by walking the source, not by trusting this sentence. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct McpPlane; - -impl McpPlane { - /// The plane. Nothing varies per instance, so there is one value of it. - pub const EMPTY: Self = Self; -} - -impl Default for McpPlane { - fn default() -> Self { - Self::EMPTY - } -} - -impl Plugin for McpPlane { - fn key(&self) -> &'static str { - ::KEY - } - - fn kind(&self) -> Kind { - Kind::Plane - } - - fn abi(&self) -> AbiVersion { - AbiVersion(1) - } -} diff --git a/crates/busbar-plane-mcp/src/line.rs b/crates/busbar-plane-mcp/src/line.rs deleted file mode 100644 index 42706c605a..0000000000 --- a/crates/busbar-plane-mcp/src/line.rs +++ /dev/null @@ -1,373 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE LINE CARRIER: one JSON-RPC message per line, each line its own request unit through the -//! door (ARCHITECT round 4 Q-L3B-STDIO-SHAPE (B), Q1a): the carrier is the host's (it holds the -//! process's own stdin/stdout open as one session and opens a unit per line); every meaning a line -//! has is here, on the plane side, and what the plane writes unsolicited it writes with the host's -//! `session.emit` on that session. -//! -//! - THE MIRRORED FIELDS a header block would carry are SYNTHESISED FROM THE BODY -//! ([`crate::codec::mirrored`]): nothing the body does not state is stated, so a body defect stays a -//! body defect. -//! - THE STDIO-ERA VERBS (`initialize`, `ping`) are answered here ([`era`]), and none reaches the -//! dispatch, the catalogue or an upstream. `logging/setLevel`, `resources/subscribe` and -//! `resources/unsubscribe` are REFUSED (`-32601`) and not advertised: this revision keeps no -//! per-session floor or watch set, and nothing on the plane announces a resource's change to -//! deliver (a watch is `subscriptions/listen`'s; the 1.6.0 design, mcp bullet, keeps `subscribe` for -//! the old revisions, on their sessions). -//! - BUSBAR'S OWN ASKS are LIVE REQUESTS on the line ([`LiveAsk`]): an `input_required` answer is -//! issued as one request per ask, in order, each spelled `busbar:`; the caller's answers become -//! `inputResponses`, and the RETRY is the unit of the caller's last answer, through the whole -//! door (its own admission; the seal, the round charge and the epoch checks run as they do when -//! an HTTP caller retries itself). An UPSTREAM's ask, relayed (Law 11), is livened the same way. -//! A live ask unanswered for [`ASK_TIMEOUT_NS`] is dropped and the caller handed the result -//! itself; a request is livened at most [`MAX_LIVE_ASK_ROUNDS`] times. -//! - A SUBSCRIPTION is answered with its acknowledgement and kept on the session: what changes, a -//! keepalive and its end are emitted on the session, until the carrier ends. - -use serde_json::{json, Map, Value}; - -use crate::codec::PROTOCOL_VERSION; -use crate::revision::Revision; - -/// The prefix of the id busbar spells its own requests on the line in. -pub const ASK_ID_PREFIX: &str = "busbar:"; - -/// How long busbar waits for the caller to answer ONE live ask before handing it the -/// `input_required` result itself (the sealed `requestState` makes that a continuation). -pub const ASK_TIMEOUT_NS: u64 = 30 * 1_000_000_000; - -/// The most live ask rounds busbar drives for one request: the carrier's own belt against a -/// composition that never converges (the per-capability `max_caller_ask_rounds` still applies). -pub const MAX_LIVE_ASK_ROUNDS: u32 = 8; - -/// The JSON-RPC code for a method this carrier does not carry. -const METHOD_NOT_FOUND: i64 = -32601; - -/// One success envelope. -#[must_use] -pub fn result(id: &Value, result: Value) -> Value { - json!({ "jsonrpc": "2.0", "id": id, "result": result }) -} - -/// One method-not-found envelope. -fn unsupported(id: &Value, message: &str) -> Value { - json!({ - "jsonrpc": "2.0", - "id": id, - "error": { "code": METHOD_NOT_FOUND, "message": message }, - }) -} - -/// The `initialize` answer to a client that names no session revision: the stateless revision, -/// which needs no handshake and says so. -#[must_use] -pub fn initialize_result(id: &Value) -> Value { - result( - id, - json!({ - "protocolVersion": PROTOCOL_VERSION, - "capabilities": capabilities(), - "serverInfo": server_info(), - "instructions": format!( - "This server speaks MCP revision {PROTOCOL_VERSION}: no handshake is required, \ - and every request states its protocol version and client capabilities in \ - `params._meta`." - ), - }), - ) -} - -/// The `initialize` answer to a client that asked for a SESSION revision this plane carries: that -/// revision, by negotiation (THE DESIGN section 2, the mcp bullet; [`crate::revision::accept_offered`]). -/// The carrier session then speaks it: each following line is raised into the stateless shape and its -/// answer lowered ([`crate::adapt`]). -#[must_use] -pub fn session_initialize_result(id: &Value, revision: Revision) -> Value { - result( - id, - json!({ - "protocolVersion": revision.wire(), - "capabilities": capabilities(), - "serverInfo": server_info(), - }), - ) -} - -/// The capabilities the line carrier declares, in every revision it speaks: no `subscribe` and no -/// `logging`, which the carrier refuses (`-32601`, [`era`]). -fn capabilities() -> Value { - json!({ - "tools": { "listChanged": true }, - "prompts": { "listChanged": true }, - "resources": { "listChanged": true }, - "completions": {}, - }) -} - -/// The server's name and version. -fn server_info() -> Value { - json!({ - "name": "busbar", - "version": crate::tool_door::VERSION, - }) -} - -/// The session revision an `initialize` asks for, when this plane carries it. -fn requested_revision(value: &Value) -> Option { - value - .pointer("/params/protocolVersion") - .and_then(Value::as_str) - .and_then(crate::revision::accept_offered) -} - -/// What a stdio-era verb came to. -#[derive(Debug, Clone, PartialEq)] -pub enum Era { - /// Answered here, whole. - Answer(Value), - /// `initialize` asking for a session revision: the revision the carrier session now speaks, - /// and the answer. - Opened(Revision, Value), - /// Not a stdio-era verb: the one dispatch's. - Dispatch, -} - -/// THE STDIO-ERA VERBS, read off a request (`method`, `id`) before the `_meta` gate, because a -/// legacy-era client sends them without one: that is what the era negotiation is for. -#[must_use] -pub fn era(value: &Value) -> Era { - let (Some(method), Some(id)) = ( - value.get("method").and_then(Value::as_str), - value.get("id").filter(|i| i.is_string() || i.is_number()), - ) else { - return Era::Dispatch; - }; - match method { - crate::adapt::METHOD_INITIALIZE => match requested_revision(value) { - Some(revision) => Era::Opened(revision, session_initialize_result(id, revision)), - None => Era::Answer(initialize_result(id)), - }, - crate::adapt::METHOD_PING => Era::Answer(result(id, json!({}))), - "logging/setLevel" => Era::Answer(unsupported( - id, - "this revision has no `logging/setLevel`: there is no session to remember a floor in. A \ - request states its floor in `params._meta`.", - )), - "resources/subscribe" | "resources/unsubscribe" => Era::Answer(unsupported( - id, - "this revision has no `resources/subscribe`: open `subscriptions/listen` with \ - `notifications.resourceSubscriptions` instead.", - )), - _ => Era::Dispatch, - } -} - -/// What a line that is not a request is, when it answers one of busbar's own. -#[derive(Debug, Clone, PartialEq)] -pub enum Reply { - /// The caller answered ask `n` with this result. - Answered(u64, Value), - /// The caller answered ask `n` with an error, or with no answer. - Failed(u64), -} - -/// The ask number busbar spelled into an id it minted. -fn ask_of(id: &Value) -> Option { - id.as_str()?.strip_prefix(ASK_ID_PREFIX)?.parse().ok() -} - -/// Whether a line answers one of busbar's own requests, and which: a JSON-RPC RESPONSE (no -/// `method`, a `result` or an `error`) whose id busbar minted, or SEP-1036's out-of-band -/// `notifications/elicitation/response`, whose `params.requestId` names the elicitation (admissible -/// only because this one authenticated single-caller channel is the binding HTTP lacks). -#[must_use] -pub fn reply(value: &Value) -> Option { - let obj = value.as_object()?; - match obj.get("method").and_then(Value::as_str) { - None => { - let n = ask_of(obj.get("id")?)?; - match (obj.get("result"), obj.get("error")) { - (Some(result), None) => Some(Reply::Answered(n, result.clone())), - (_, Some(_)) => Some(Reply::Failed(n)), - (None, None) => None, - } - } - Some("notifications/elicitation/response") => { - let n = ask_of(value.pointer("/params/requestId")?)?; - Some(Reply::Answered( - n, - value - .pointer("/params/response") - .cloned() - .unwrap_or(Value::Null), - )) - } - Some(_) => None, - } -} - -/// The request id a `notifications/cancelled` names. -#[must_use] -pub fn cancelled(value: &Value) -> Option { - (value.get("method").and_then(Value::as_str) == Some("notifications/cancelled")) - .then(|| value.pointer("/params/requestId").cloned()) - .flatten() - .filter(|i| i.is_string() || i.is_number()) -} - -/// A stable key for a JSON-RPC id: type-tagged, so the string `"1"` and the number `1` (which never -/// correlate on the wire) never collide. -#[must_use] -pub fn id_key(id: &Value) -> String { - match id { - Value::String(s) => format!("s:{s}"), - other => format!("n:{other}"), - } -} - -/// Whether a request already livened `round` times may be livened again. -#[must_use] -pub fn may_liven(round: u32) -> bool { - round < MAX_LIVE_ASK_ROUNDS -} - -/// Where a live ask stands. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AskStand { - /// Waiting on the caller's answer to the ask in flight. - Waiting, - /// Every ask of the round was answered: the retry went on as its own unit. - Answered, - /// The caller cancelled the request. - Cancelled, - /// The caller answered an ask with an error (or not at all): it is handed the result itself. - Unanswered, -} - -/// ONE LIVE ASK ROUND for one request: the request it continues, the round's asks in order, the -/// answers so far, the sealed state the retry echoes, and the unit holding the caller's answer. -#[derive(Debug, Clone)] -pub struct LiveAsk { - /// The principal it was asked under. - pub principal: String, - /// The request as the caller sent it (its id is the caller's). - pub original: Value, - /// The `input_required` result itself: what the caller is handed when it does not answer. - pub fallback: Vec, - /// The asks not yet issued, in order: `(key, method, params)`. - pub queued: Vec<(String, String, Value)>, - /// The key of the ask in flight. - pub current: String, - /// The number the ask in flight was issued under (its request id is `busbar:`). - pub current_n: u64, - /// The answers so far, by key. - pub responses: Map, - /// The sealed state the retry echoes. - pub state: String, - /// The live round this is (`0` = the request's first). - pub round: u32, - /// The carrier session it was asked on. - pub session: u64, - /// When the ask in flight lapses, on the host's monotonic clock. - pub deadline_ns: u64, - /// Where it stands. - pub stand: AskStand, -} - -impl LiveAsk { - /// The round an `input_required` `result` asks of `original`; `None` when the result names no - /// asks or no state (it is then handed to the caller as it is). - #[must_use] - pub fn of( - principal: &str, - original: Value, - fallback: Vec, - result: &Value, - round: u32, - ) -> Option { - let requests = result.get("inputRequests")?.as_object()?; - let state = result.get("requestState")?.as_str()?.to_string(); - let mut queued: Vec<(String, String, Value)> = requests - .iter() - .filter_map(|(key, ask)| { - Some(( - key.clone(), - ask.get("method")?.as_str()?.to_string(), - ask.get("params").cloned().unwrap_or(Value::Null), - )) - }) - .collect(); - if queued.is_empty() || queued.len() != requests.len() { - return None; - } - queued.reverse(); - Some(LiveAsk { - principal: principal.to_string(), - original, - fallback, - queued, - current: String::new(), - current_n: 0, - responses: Map::new(), - state, - round, - session: 0, - deadline_ns: 0, - stand: AskStand::Waiting, - }) - } - - /// Starts the clock on the ask in flight: it lapses [`ASK_TIMEOUT_NS`] after `now_ns`. - pub fn arm(&mut self, now_ns: u64) { - self.deadline_ns = now_ns.saturating_add(ASK_TIMEOUT_NS); - } - - /// Whether the ask in flight went unanswered past its deadline at `now_ns`. - #[must_use] - pub fn lapsed(&self, now_ns: u64) -> bool { - self.deadline_ns != 0 && now_ns >= self.deadline_ns - } - - /// The next ask as the request line busbar writes, under ask number `n`; `None` once every ask - /// was issued. - pub fn issue(&mut self, n: u64) -> Option> { - let (key, method, params) = self.queued.pop()?; - self.current = key; - self.current_n = n; - let mut line = serde_json::to_vec(&json!({ - "jsonrpc": "2.0", - "id": format!("{ASK_ID_PREFIX}{n}"), - "method": method, - "params": params, - })) - .unwrap_or_default(); - line.push(b'\n'); - Some(line) - } - - /// The caller's answer to the ask in flight. - pub fn answered(&mut self, response: Value) { - self.responses - .insert(std::mem::take(&mut self.current), response); - } - - /// THE RETRY: the request as the caller sent it, carrying the round's answers and the sealed - /// state. Its id stays the caller's, so its answer is the caller's answer. - #[must_use] - pub fn retry(&self) -> Option { - let mut retry = self.original.clone(); - let params = retry.get_mut("params")?.as_object_mut()?; - params.insert( - "inputResponses".to_string(), - Value::Object(self.responses.clone()), - ); - params.insert("requestState".to_string(), Value::from(self.state.clone())); - Some(retry) - } -} - -#[cfg(test)] -#[path = "tests/line_tests.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/reads.rs b/crates/busbar-plane-mcp/src/reads.rs deleted file mode 100644 index 586b6504cf..0000000000 --- a/crates/busbar-plane-mcp/src/reads.rs +++ /dev/null @@ -1,278 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE TWO READS ANSWERED FROM THE SECTION: `prompts/get` and `resources/read`. No upstream is -//! contacted; the content is the operator's own, with the caller's text substituted into it and the -//! whole passed through the markup strip after substitution, never before. -//! -//! Pure: the request's params, the generation's [`Catalogue`] and the caller's `admit` predicate -//! in; the answer's bytes or its refusal out. A prompt that asks its caller for input first -//! ([`PromptEntry::ask_caller`]) reaches [`prompts_get`] only once that exchange let it through. -//! -//! The answers and the refusals are the served engine's: a missing member is `400` + `-32602`; -//! not found and not granted are one `404` + `-32000`; a uri two reachable approvals answer is -//! `409` + `-32000` naming them. - -use std::collections::BTreeMap; - -use serde_json::{json, Map, Value}; - -use crate::catalogue::{ - complete, Catalogue, Lookup, PromptEntry, ResourceEntry, ResourceTemplateEntry, -}; -use crate::codec::{CODE_INVALID_PARAMS, CODE_REFUSED}; -use crate::sanitize::{normalise, normalise_opt}; -use crate::tool_arrival::Refusal; -use crate::tools_config::PromptContentCfg; - -/// The status of a read that names nothing the caller may see. -pub const STATUS_NOT_FOUND: u32 = 404; - -/// The status of a uri more than one reachable approval answers. -pub const STATUS_CONFLICT: u32 = 409; - -fn read_refusal( - status: u32, - id: &Value, - code: i64, - message: String, - data: Option, -) -> Refusal { - Refusal { - status, - id: Some(id.clone()), - code, - message, - data, - } -} - -fn string_param<'a>(params: Option<&'a Value>, key: &str) -> Option<&'a str> { - params.and_then(|p| p.get(key)).and_then(Value::as_str) -} - -/// Substitute `{arg}` placeholders from `params.arguments`, in ONE pass over the template: an -/// argument's value is never read for placeholders, and an unknown placeholder is left as written. -/// Only string arguments substitute. -#[must_use] -pub fn substitute_arguments(template: &str, params: Option<&Value>) -> String { - let Some(args) = params - .and_then(|p| p.get("arguments")) - .and_then(Value::as_object) - else { - return template.to_string(); - }; - let mut out = String::with_capacity(template.len()); - let mut rest = template; - while let Some(open) = rest.find('{') { - out.push_str(&rest[..open]); - let after = &rest[open + 1..]; - let Some(close) = after.find('}') else { - out.push_str(&rest[open..]); - return out; - }; - let name = &after[..close]; - match args.get(name).and_then(Value::as_str) { - Some(text) => out.push_str(text), - None => { - out.push('{'); - out.push_str(name); - out.push('}'); - } - } - rest = &after[close + 1..]; - } - out.push_str(rest); - out -} - -/// The `messages` a prompt renders, in whichever form the operator declared: substituted first, -/// normalised second. Media payloads are opaque and pass untouched. -fn render_messages(prompt: &PromptEntry, params: Option<&Value>) -> Vec { - let render = |s: &str| normalise(&substitute_arguments(s, params)); - if prompt.messages.is_empty() { - return vec![json!({ - "role": "user", - "content": { - "type": "text", - "text": render(prompt.template.as_deref().unwrap_or("")), - }, - })]; - } - prompt - .messages - .iter() - .map(|m| { - let content = match &m.content { - PromptContentCfg::Text { text } => json!({ "type": "text", "text": render(text) }), - PromptContentCfg::Image { data, mime_type } => { - json!({ "type": "image", "data": data, "mimeType": mime_type }) - } - PromptContentCfg::Audio { data, mime_type } => { - json!({ "type": "audio", "data": data, "mimeType": mime_type }) - } - PromptContentCfg::Resource { resource } => { - let mut r = Map::new(); - r.insert("uri".into(), render(&resource.uri).into()); - if let Some(m) = &resource.mime_type { - r.insert("mimeType".into(), m.clone().into()); - } - if let Some(t) = &resource.text { - r.insert("text".into(), render(t).into()); - } - if let Some(b) = &resource.blob { - r.insert("blob".into(), b.clone().into()); - } - json!({ "type": "resource", "resource": r }) - } - }; - json!({ "role": m.role, "content": content }) - }) - .collect() -} - -/// The prompt `params.name` names, under the caller's grants. -/// -/// # Errors -/// -/// `400` + `-32602` without a string `params.name`; `404` + `-32000` for a prompt that does not -/// exist or is not the caller's, in one sentence. -pub fn prompt_named<'c>( - catalogue: &'c Catalogue, - id: &Value, - params: Option<&Value>, - admit: &impl Fn(&str, &str) -> bool, -) -> Result<&'c PromptEntry, Refusal> { - let Some(name) = string_param(params, "name") else { - return Err(read_refusal( - crate::checks::STATUS, - id, - CODE_INVALID_PARAMS, - "`params.name` is required and must be a string.".to_string(), - None, - )); - }; - catalogue.prompt_for(admit, name).ok_or_else(|| { - read_refusal( - STATUS_NOT_FOUND, - id, - CODE_REFUSED, - format!("`{name}` is not a prompt this server exposes."), - None, - ) - }) -} - -/// `prompts/get`'s answer for a prompt [`prompt_named`] found. -#[must_use] -pub fn prompts_get(prompt: &PromptEntry, id: &Value, params: Option<&Value>) -> Vec { - complete( - id, - json!({ - "description": normalise_opt(prompt.description.as_deref()), - "messages": render_messages(prompt, params), - }), - ) -} - -fn concrete_content(res: &ResourceEntry) -> Value { - let mut content = Map::new(); - content.insert("uri".into(), res.uri.clone().into()); - if let Some(m) = &res.mime_type { - content.insert("mimeType".into(), m.clone().into()); - } - match &res.blob { - Some(blob) => { - content.insert("blob".into(), blob.clone().into()); - } - None => { - content.insert( - "text".into(), - normalise(res.text.as_deref().unwrap_or("")).into(), - ); - } - } - Value::Object(content) -} - -fn templated_content( - requested: &str, - template: &ResourceTemplateEntry, - bindings: &BTreeMap, -) -> Value { - let mut text = template.text.clone().unwrap_or_default(); - for (name, value) in bindings { - text = text.replace(&format!("{{{name}}}"), value); - } - let mut content = Map::new(); - content.insert("uri".into(), requested.into()); - if let Some(m) = &template.mime_type { - content.insert("mimeType".into(), m.clone().into()); - } - content.insert("text".into(), normalise(&text).into()); - Value::Object(content) -} - -fn ambiguous(id: &Value, uri: &str, candidates: &[String]) -> Refusal { - read_refusal( - STATUS_CONFLICT, - id, - CODE_REFUSED, - format!( - "`{uri}` is answered by more than one approval you are granted ({}). \ - Narrow the grant so exactly one of them applies.", - candidates.join(", ") - ), - Some(json!({ "reason": "resource_ambiguous", "candidates": candidates })), - ) -} - -/// `resources/read`: a concrete approval first, a template second, never a guess between two. -/// -/// # Errors -/// -/// `400` + `-32602` without a string `params.uri`; `409` + `-32000` for an ambiguity; `404` + -/// `-32000` for a uri nothing the caller may see answers. -pub fn resources_read( - catalogue: &Catalogue, - id: &Value, - params: Option<&Value>, - admit: &impl Fn(&str, &str) -> bool, -) -> Result, Refusal> { - let Some(uri) = string_param(params, "uri") else { - return Err(read_refusal( - crate::checks::STATUS, - id, - CODE_INVALID_PARAMS, - "`params.uri` is required and must be a string.".to_string(), - None, - )); - }; - let content = match catalogue.resource_by_uri(admit, uri) { - Lookup::One(res) => concrete_content(res), - Lookup::Ambiguous(candidates) => return Err(ambiguous(id, uri, &candidates)), - Lookup::NotFound => match catalogue.resource_template_for(admit, uri) { - Lookup::One((template, bindings)) => templated_content(uri, template, &bindings), - Lookup::Ambiguous(candidates) => return Err(ambiguous(id, uri, &candidates)), - Lookup::NotFound => { - return Err(read_refusal( - STATUS_NOT_FOUND, - id, - CODE_REFUSED, - format!("`{uri}` is not a resource this server exposes."), - None, - )) - } - }, - }; - let mut result = json!({ "contents": [content] }); - if let Some(obj) = result.as_object_mut() { - obj.insert("cacheScope".into(), crate::catalogue::CACHE_SCOPE.into()); - obj.insert("ttlMs".into(), crate::catalogue::CACHE_TTL_MS.into()); - } - Ok(complete(id, result)) -} - -#[cfg(test)] -#[path = "tests/reads.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/record.rs b/crates/busbar-plane-mcp/src/record.rs deleted file mode 100644 index 6abc72992e..0000000000 --- a/crates/busbar-plane-mcp/src/record.rs +++ /dev/null @@ -1,259 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE MCP PLANE'S OWN DURABLE RECORD TYPES — relocated here from `busbar-api` (1.7.0 plane -//! extraction). The neutral `busbar_contract::records::RecordStore` contract speaks ONLY the opaque -//! `busbar_contract::records::PlaneRecord` envelope; a plane owns its concrete row schema and serializes it into -//! (and back out of) that envelope's opaque `body` with `serde_json` — byte-for-byte the same the -//! store plugins persist it with. The neutral crates name none of these types. - -use busbar_contract::records::{ - PlaneDisposition, PlaneRecord, PlaneSelector, RecordStoreError, RecordStoreResult, -}; - -// THE TWO KIND STRINGS ARE THE PLANE'S, AND ARE READ FROM IT. A record kind is the name the plane -// declares its schema under (`busbar_plane_mcp::tool_records::SCHEMA_CALL`), so it is named once, there, -// and this crate reads it. Spelling it on both sides is how two answers to "what is this record -// called" come to differ, and the schema id is what a store indexes by. -pub use crate::tool_records::{KIND_CALL, KIND_DEMOTION}; - -/// One MCP TOOL-CALL record, as it crosses the store seam for DURABLE persistence — the per-call -/// evidence the audit claim rests on. The chain is scoped to the PRINCIPAL. A store persists these -/// verbatim and returns them verbatim: the digest is computed and verified engine-side, and a backend -/// never interprets or recomputes it. Plain data, never a credential — arguments and results are -/// deliberately ABSENT. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct McpCallRecord { - /// The authenticated caller this chain belongs to. THE CHAIN SCOPE. - pub principal: String, - /// Monotonic sequence number, 1-based WITHIN `principal`. - pub seq: u64, - /// Unix seconds the call was attempted. - pub ts: u64, - /// The registered MCP server id the call resolved to, or empty when it resolved to none. - pub server: String, - /// `{server}_{tool}` — the namespaced routing key exactly as the call named it. - pub tool: String, - /// Stable outcome token: `dispatched` (the call went out) | `refused` (it did not). - pub outcome: String, - /// The reason token for this outcome, or empty. Never free text. - pub reason: String, - /// The tool digest the call was admitted against, or empty on a refusal that never reached one. - pub tool_digest: String, - /// The catalogue pin generation the call was resolved under. - pub pin_generation: u64, - /// The request-spine join key. EXCLUDED from the digest. - pub request_id: String, - /// The preceding record's `hash` for this principal (empty for the first of a chain). - pub prev_hash: String, - /// The tamper-evidence digest over this record's chained fields (computed + verified engine-side). - pub hash: String, -} - -impl McpCallRecord { - // NB: there is deliberately NO plane-side `to_plane_record` WRITER for the call record. The - // engine owns the append: a `call` chain is persisted ONLY as the neutral `{seq, prev_hash, hash, - // content}` journal body (see [`Self::from_journal_body`]), NOT as a serde-serialized - // `McpCallRecord`. A writer that serialized this struct into a `PlaneRecord` body would emit a - // shape the actual reader (`from_journal_body`) cannot parse — a footgun, so it does not exist. - // The plane owns only the READ-BACK of what the engine wrote. - - /// The list selector that reads one principal's `call` chain back, oldest-first. - pub fn parent_selector(principal: &str) -> PlaneSelector<'_> { - PlaneSelector::Parent(principal.into()) - } - - /// Reconstruct a record from an opaque serde `call` body — the inverse of a plain `serde_json` - /// serialization of this struct. NOT the engine's persisted shape (that is the neutral journal - /// body — see [`Self::from_journal_body`]); this decodes a bare `McpCallRecord` where one is held. - pub fn from_body(body: &[u8]) -> RecordStoreResult { - decode_record(body) - } - - /// Reconstruct a record from the NEUTRAL durable-journal body the engine's call-log seam persists - /// — `{seq, prev_hash, hash, content}`, where `content` is the pre-framed LengthPrefixed field - /// SUFFIX the record's digest was sealed over. This is the shape core's store-backed call journal - /// writes (it carries no plane type), so a plane reading its own call chain back out of the store - /// owns the decode; the field framing is the plane's (it travels with the record). `principal` is - /// the chain SCOPE — the store parent — supplied by the caller and never carried in the body. - /// `request_id` is a join key: never in the digest, so never in the neutral content, and it comes - /// back EMPTY. The rebuilt fields feed the same digest byte stream the stored `hash` sealed, so a - /// chain read back through this verifies byte-identically. - pub fn from_journal_body(principal: &str, body: &[u8]) -> RecordStoreResult { - // The neutral envelope, decoded structurally (matching field names) so this names no core type. - #[derive(serde::Deserialize)] - struct NeutralJournalBody { - seq: u64, - prev_hash: String, - hash: String, - content: Vec, - } - let nb: NeutralJournalBody = decode_record(body)?; - let (ts, server, tool, outcome, reason, tool_digest, pin_generation) = - parse_call_suffix(&nb.content)?; - Ok(McpCallRecord { - principal: principal.to_string(), - seq: nb.seq, - ts, - server, - tool, - outcome, - reason, - tool_digest, - pin_generation, - request_id: String::new(), - prev_hash: nb.prev_hash, - hash: nb.hash, - }) - } -} - -/// THE CALL RECORD'S CONTENT SUFFIX, as the plane writes it to the host's record seam: the chained -/// fields after the prelude the host frames (`prev_hash`, the scope, `seq`), LengthPrefixed — every -/// field `len:u64-be ⧺ bytes`, a numeric field its eight big-endian bytes as one such field — in the -/// call digest's order: `ts, server, tool, outcome, reason, tool_digest, pin_generation`. The -/// `request_id` is not in it: a join key is never in the digest. The inverse is -/// [`parse_call_suffix`], so a record written here reads back as the record it was. -#[must_use] -pub fn call_suffix( - ts: u64, - server: &str, - tool: &str, - outcome: &str, - reason: &str, - tool_digest: &str, - pin_generation: u64, -) -> Vec { - fn text(out: &mut Vec, s: &str) { - out.extend_from_slice(&(s.len() as u64).to_be_bytes()); - out.extend_from_slice(s.as_bytes()); - } - fn num(out: &mut Vec, v: u64) { - let b = v.to_be_bytes(); - out.extend_from_slice(&(b.len() as u64).to_be_bytes()); - out.extend_from_slice(&b); - } - let mut out = Vec::new(); - num(&mut out, ts); - text(&mut out, server); - text(&mut out, tool); - text(&mut out, outcome); - text(&mut out, reason); - text(&mut out, tool_digest); - num(&mut out, pin_generation); - out -} - -/// Parse the LengthPrefixed call SUFFIX (`ts, server, tool, outcome, reason, tool_digest, -/// pin_generation`) the neutral journal body carries — the inverse of the seam's write framing. Every -/// field is `len:u64-be ⧺ bytes`; a numeric field is its eight big-endian bytes carried as one such -/// length-prefixed field. Fails closed on a truncated/oversized field rather than reading past the -/// buffer. -fn parse_call_suffix( - content: &[u8], -) -> RecordStoreResult<(u64, String, String, String, String, String, u64)> { - fn take<'a>(content: &'a [u8], off: &mut usize) -> RecordStoreResult<&'a [u8]> { - // Both cursor advances are CHECKED, because both operands come off the wire: a corrupt or - // hostile prefix can name any 64-bit length, and `off + len` on a `usize` either panics - // (debug) or WRAPS (release) — and a wrapped sum passes the bound test below and then slices - // with a start past its end, which panics too. Checked-then-bounded is the only form that - // keeps the documented "fails closed rather than reading past the buffer" true in both - // profiles. - let end_prefix = off - .checked_add(8) - .filter(|e| *e <= content.len()) - .ok_or_else(|| RecordStoreError("truncated call suffix length prefix".to_string()))?; - let len = usize::try_from(u64::from_be_bytes( - content[*off..end_prefix].try_into().unwrap(), - )) - .map_err(|_| RecordStoreError("truncated call suffix field".to_string()))?; - *off = end_prefix; - let end_field = off - .checked_add(len) - .filter(|e| *e <= content.len()) - .ok_or_else(|| RecordStoreError("truncated call suffix field".to_string()))?; - let s = &content[*off..end_field]; - *off = end_field; - Ok(s) - } - fn take_num(content: &[u8], off: &mut usize) -> RecordStoreResult { - let arr: [u8; 8] = take(content, off)? - .try_into() - .map_err(|_| RecordStoreError("call suffix num field is not 8 bytes".to_string()))?; - Ok(u64::from_be_bytes(arr)) - } - fn take_text(content: &[u8], off: &mut usize) -> RecordStoreResult { - // FAILS CLOSED like `take_num`, and for a sharper reason. A lossy decode substitutes U+FFFD - // for a byte no UTF-8 permits, which SILENTLY CHANGES THE FIELD — and the field is part of - // the byte stream the record's stored digest was sealed over, so verification then fails and - // a flipped bit in storage reads as someone having rewritten the chain. Refusing the field - // reports the corruption as corruption. - String::from_utf8(take(content, off)?.to_vec()) - .map_err(|_| RecordStoreError("call suffix text field is not utf-8".to_string())) - } - let mut off = 0usize; - let ts = take_num(content, &mut off)?; - let server = take_text(content, &mut off)?; - let tool = take_text(content, &mut off)?; - let outcome = take_text(content, &mut off)?; - let reason = take_text(content, &mut off)?; - let tool_digest = take_text(content, &mut off)?; - let pin_generation = take_num(content, &mut off)?; - Ok(( - ts, - server, - tool, - outcome, - reason, - tool_digest, - pin_generation, - )) -} - -/// ONE RECORDED DEMOTION of an upstream MCP server, as it crosses the store seam. Written when a -/// server is demoted, cleared when a later observation agrees with the approval again, and read back -/// at boot so the demotion is in force before the first request is served. Keyed by `server`. -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct McpDemotionRow { - /// The registered upstream's local id — the row's primary key. - pub server: String, - /// The operator-facing word for why it was demoted. Never free-form caller text. - pub reason: String, - /// Unix seconds the demotion was recorded. - pub recorded_at: u64, -} - -impl McpDemotionRow { - /// Serialize this row into the opaque `demotion` [`PlaneRecord`] envelope (kind `demotion`), - /// keyed by its server. Demotions are never purged by age, so the disposition is left `Active`. - pub fn to_plane_record(&self) -> RecordStoreResult { - Ok(PlaneRecord { - kind: KIND_DEMOTION.to_string(), - id: self.server.clone(), - parent: None, - seq: 0, - ts: self.recorded_at, - disposition: PlaneDisposition::Active, - body: encode(self)?, - }) - } - - /// Reconstruct a row from an opaque `demotion` body — the inverse of [`Self::to_plane_record`]. - pub fn from_body(body: &[u8]) -> RecordStoreResult { - decode_record(body) - } -} - -/// Serialize a typed plane row into an opaque `PlaneRecord::body`. `serde_json`. -fn encode(row: &T) -> RecordStoreResult> { - serde_json::to_vec(row).map_err(|e| RecordStoreError(format!("plane body encode: {e}"))) -} - -/// Decode an opaque `PlaneRecord::body` back into its typed plane row — the inverse of [`encode`]. -fn decode_record(body: &[u8]) -> RecordStoreResult { - serde_json::from_slice(body).map_err(|e| RecordStoreError(format!("plane body decode: {e}"))) -} - -#[cfg(test)] -#[path = "tests/record_tests.rs"] -mod record_tests; diff --git a/crates/busbar-plane-mcp/src/revision.rs b/crates/busbar-plane-mcp/src/revision.rs deleted file mode 100644 index c34bce3810..0000000000 --- a/crates/busbar-plane-mcp/src/revision.rs +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE REVISIONS OF THE ONE MCP DIALECT, and how two peers agree on one. -//! -//! MCP is one dialect with dated revisions. This plane speaks four of them, and a revision is not a -//! plugin: it is a set of rules inside the dialect about how a conversation is opened and carried. -//! -//! | revision | opened by | sessions | version header | carried as | -//! |--------------|---------------------------------|----------|----------------|------------| -//! | `2026-07-28` | nothing: every request is whole | none | required | one endpoint, POST only | -//! | `2025-11-25` | `initialize` | yes | required | one endpoint, POST + GET stream + DELETE | -//! | `2025-06-18` | `initialize` | yes | required | one endpoint, POST + GET stream + DELETE | -//! | `2024-11-05` | `initialize` | yes | none | an event stream that names a message address | -//! -//! `2025-03-26` is not implemented: it is the only revision that allows batched bodies, and a -//! client that asks for it is offered `2025-06-18` in the `initialize` answer, which the spec's -//! negotiation rule permits (the client disconnects if it cannot speak what it is offered). -//! -//! OWNER RULING 2026-09-29: 1.6.0 interoperates with all four, in both directions. The revision -//! negotiation below is the spec's own: the client names the revision it wants in `initialize`; a -//! server that implements it answers with it, and otherwise answers with the latest revision it does -//! implement. A client with a stateless probe that is refused falls back to `initialize`, and a -//! client whose `initialize` POST is refused with a 4xx falls back to the event-stream revision. - -/// One revision of the MCP dialect. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Revision { - /// The event-stream revision: a GET opens a stream whose first event names the address - /// messages are POSTed to, and every answer arrives on that stream. - R2024_11_05, - /// The first session revision with the `MCP-Protocol-Version` request header. - R2025_06_18, - /// The latest session revision. - R2025_11_25, - /// The stateless revision: no handshake, no session, every request self-describing. - R2026_07_28, -} - -/// The revisions answered with a session, latest first. The order is the negotiation order. -pub const SESSION_REVISIONS: &[Revision] = &[ - Revision::R2025_11_25, - Revision::R2025_06_18, - Revision::R2024_11_05, -]; - -/// Every implemented revision, latest first. -pub const ALL: &[Revision] = &[ - Revision::R2026_07_28, - Revision::R2025_11_25, - Revision::R2025_06_18, - Revision::R2024_11_05, -]; - -/// The revision a session request is read as when it carries no version header and no session -/// names one: the spec's stated default for a server that cannot otherwise tell. -pub const HEADERLESS_DEFAULT: &str = "2025-03-26"; - -impl Revision { - /// The revision's wire string. - #[must_use] - pub const fn wire(self) -> &'static str { - match self { - Self::R2024_11_05 => "2024-11-05", - Self::R2025_06_18 => "2025-06-18", - Self::R2025_11_25 => "2025-11-25", - Self::R2026_07_28 => crate::codec::PROTOCOL_VERSION, - } - } - - /// Reads a wire string. `None` for a revision this plane does not implement. - #[must_use] - pub fn parse(wire: &str) -> Option { - ALL.iter().copied().find(|r| r.wire() == wire) - } - - /// Whether a conversation in this revision is opened by `initialize` and lives in a session. - #[must_use] - pub const fn has_sessions(self) -> bool { - !matches!(self, Self::R2026_07_28) - } - - /// Whether every request after `initialize` must carry the `MCP-Protocol-Version` header. - #[must_use] - pub const fn requires_version_header(self) -> bool { - matches!( - self, - Self::R2025_06_18 | Self::R2025_11_25 | Self::R2026_07_28 - ) - } - - /// Whether this revision is carried as an event stream naming a message address, rather than - /// on the single endpoint. - #[must_use] - pub const fn is_event_stream_revision(self) -> bool { - matches!(self, Self::R2024_11_05) - } - - /// Whether a disconnected server stream may be resumed with `Last-Event-ID`. - #[must_use] - pub const fn resumable(self) -> bool { - matches!(self, Self::R2025_06_18 | Self::R2025_11_25) - } -} - -/// THE SERVER HALF OF `initialize` NEGOTIATION. The client's requested revision is echoed when it is -/// a session revision this plane implements; anything else, including an unknown string and the -/// stateless revision (which has no `initialize`), is answered with the latest session revision. -#[must_use] -pub fn negotiate(requested: Option<&str>) -> Revision { - requested - .and_then(Revision::parse) - .filter(|r| r.has_sessions()) - .unwrap_or(SESSION_REVISIONS[0]) -} - -/// How a session request's `MCP-Protocol-Version` header reads against the session's revision. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum HeaderCheck { - /// The header agrees with the session, or is absent where the session's revision allows that. - Agrees, - /// The header is absent on a revision that requires it. - Missing, - /// The header names a revision other than the session's (400 per the spec). - Disagrees, -} - -/// Judges a session request's version header. A session knows its revision, so an absent header is -/// only a defect on a revision that requires it. -#[must_use] -pub fn check_header(session: Revision, header: Option<&str>) -> HeaderCheck { - match header { - Some(h) if h == session.wire() => HeaderCheck::Agrees, - Some(_) => HeaderCheck::Disagrees, - None if session.requires_version_header() => HeaderCheck::Missing, - None => HeaderCheck::Agrees, - } -} - -/// What a GET that names no session, and accepts an event stream, is answered with. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum SessionlessGet { - /// Open the `2024-11-05` event stream, whose first event names the message address. - EventStream, - /// `405`: the client speaks a single-endpoint revision, which has no sessionless GET stream. - NotAllowed, -} - -/// THE SESSIONLESS GET (ARCHITECT ruling 2026-09-29). Every single-endpoint revision's client sends -/// `MCP-Protocol-Version`, and a `2024-11-05` client never does, so the header alone tells the two -/// apart. Absent: the event-stream revision's GET. Present and naming any other revision: `405`, as -/// that revision says. Present and naming no revision this plane implements: read the way -/// [`negotiate`] reads an unknown revision, as the latest session revision, so `405` as well. The -/// caller answers a GET that neither names a session nor accepts an event stream with `405` before -/// asking. -#[must_use] -pub fn sessionless_get(version_header: Option<&str>) -> SessionlessGet { - match version_header { - None => SessionlessGet::EventStream, - Some(h) => match Revision::parse(h) { - Some(r) if r.is_event_stream_revision() => SessionlessGet::EventStream, - _ => SessionlessGet::NotAllowed, - }, - } -} - -/// THE CLIENT LADDER: the order in which busbar, as a client, tries to open a conversation with an -/// upstream whose revision it does not yet know. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ClientStep { - /// A stateless `2026-07-28` request. - Stateless, - /// A POSTed `initialize` naming the latest session revision. - Initialize, - /// A GET for the `2024-11-05` event stream and its message address. - EventStream, -} - -/// What one rung of the ladder observed. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum StepOutcome { - /// The upstream answered in the rung's revision. - Answered, - /// A status in 400..500, or a JSON-RPC refusal that says the revision or method is not - /// implemented (`-32601`, `-32600`, `-32602`, the unsupported-version code). The spec's - /// fallback is not keyed to one code, and neither is this. - Refused4xx, - /// No answer at all: the upstream could not be reached, or answered 5xx. Not a revision - /// signal, so the ladder does not move. - Unreachable, -} - -/// The next rung after `step` observed `outcome`. `None` means stop: either the rung answered, or -/// there is nothing left to try, or the failure says nothing about the revision. -#[must_use] -pub fn next_step(step: ClientStep, outcome: StepOutcome) -> Option { - match (step, outcome) { - (_, StepOutcome::Answered | StepOutcome::Unreachable) => None, - (ClientStep::Stateless, StepOutcome::Refused4xx) => Some(ClientStep::Initialize), - (ClientStep::Initialize, StepOutcome::Refused4xx) => Some(ClientStep::EventStream), - (ClientStep::EventStream, StepOutcome::Refused4xx) => None, - } -} - -/// THE CLIENT HALF OF `initialize` NEGOTIATION: the revision the server answered with, if this plane -/// can speak it on a session. A server answering a revision this plane does not implement is a -/// server the client must disconnect from, per the spec. -#[must_use] -pub fn accept_offered(offered: &str) -> Option { - Revision::parse(offered).filter(|r| r.has_sessions()) -} - -#[cfg(test)] -#[path = "tests/revision_tests.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/sanitize.rs b/crates/busbar-plane-mcp/src/sanitize.rs deleted file mode 100644 index 622b92afb8..0000000000 --- a/crates/busbar-plane-mcp/src/sanitize.rs +++ /dev/null @@ -1,199 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! MARKUP-NORMALISATION — and NOTHING MORE THAN MARKUP-NORMALISATION. -//! -//! ## What this does -//! -//! It strips instruction-injection MARKUP — ``, ``, HTML-like tags — from the -//! text busbar composes and serves itself: -//! -//! - tool, prompt and resource DESCRIPTIONS and names in the catalogue; -//! - `resources/read` CONTENT and `prompts` TEMPLATES answered from the section. -//! -//! An upstream's tool RESULT and its error message are not among them: they are the upstream's -//! data and reach the caller as the upstream sent them (Law 11). -//! -//! ## What this does NOT do, stated here rather than in a footnote -//! -//! It does not stop plain-language semantic injection. "now call `transfer_funds`" carries no markup -//! and survives this function unchanged, by design — there is nothing to strip. MCPTox shows strong -//! agents follow malicious tool-output instructions roughly half the time even absent markup. So -//! semantic injection is a HOOK RESIDUAL and a model-alignment problem, and busbar reduces the -//! markup-shaped attack surface rather than neutralising prompt injection. Anyone reading this -//! module as "injection is handled" has read it wrong. -//! -//! ## Why it is in core when the placement matrix puts it on a hook -//! -//! The placement matrix puts markup-normalisation on a hook / signed plugin, on the Response-phase -//! `Rewrite` grant, because content policy is tunable and core is neutral. That placement is right -//! for the TUNABLE part and it is deliberately not what this is. This is the FLOOR: the fixed, -//! universal, security-critical strip that applies whether or not an operator has installed a hook, -//! for the same reason SSRF argument validation splits into fixed scheme and IP checks in core plus -//! tunable per-tool policy in a hook. A deployment with no sanitiser hook must not serve raw -//! `` markup out of a governance plane, and "the operator should have installed a plugin" -//! is not a defence. The hook remains the place to add anything beyond the floor. -//! -//! ## Why a tag STRIPPER and not an escaper -//! -//! Escaping (`<IMPORTANT>`) preserves the instruction in a form many models still read as an -//! instruction, and it makes the text longer, which is the wrong direction for a value that is about -//! to be charged to a context window. Removing the TAG and keeping the TEXT is the minimum edit that -//! removes the markup channel: `do X` becomes `do X`, which a model reads as -//! ordinary prose from an untrusted tool — which is exactly what it is. - -/// Strip instruction-injection markup from one string. -/// -/// The rule is: remove anything that lexes as an HTML-like TAG, keep everything else verbatim, -/// including the tag's inner text. A "tag" is `<` … `>` where the character after `<` (or after a -/// leading `/`) is an ASCII letter, `!` or `?` — which covers ``, ``, ``, -/// `` and `` while leaving `a < b`, `x <- y` and `3<5` untouched. -/// -/// That distinction matters more than it looks: a stripper that removed everything between any `<` -/// and any `>` would silently eat the middle of `if (a < b && c > d)` out of a code snippet a tool -/// legitimately returned, and a sanitiser that corrupts honest payloads gets turned off. -/// -/// An UNTERMINATED `<` (no closing `>` before end of input) is KEPT VERBATIM, `<` and the whole tail -/// with it, because with no closing `>` in the string it cannot lex as a tag — it is a dangling `<`, -/// i.e. ordinary text, exactly like the `a < b` case above. Dropping it would silently discard the -/// rest of the value, and busbar is a security/audit product: a hook, a guardrail, a reviewer or the -/// audit log must see 100% of what a tool returned, so truncate-and-continue is forbidden. The old -/// justification — that `` after it and stays inert. -/// -/// ## The output, not the input, is what must contain no tag -/// -/// Deleting a tag JOINS the text before it to the text after it, and that join can lex as a tag even -/// though neither side did: `<IMPORTANT>` keeps its outer `<` (the byte after it is `<`, -/// not a letter), drops the inner ``, and the surviving `IMPORTANT>` snaps onto the -/// surviving `<`. A scanner that only ever reads FORWARD cannot see that, so this one re-examines the -/// tail of what it has already emitted after every deletion — the only moment at which two -/// non-adjacent pieces of input become adjacent in the output. That check is the difference between -/// "no tag in the input survives" and "no tag is in the output", and only the second is a defence. -pub fn normalise(input: &str) -> String { - let bytes = input.as_bytes(); - let mut out = String::with_capacity(input.len()); - let mut i = 0usize; - // The `>` that closes whatever tag opens at or after the cursor, remembered rather than - // re-derived. The cursor only ever moves FORWARD, so this answer is refreshed only once the - // cursor passes it, and each refresh resumes where the last one stopped — every byte of the - // input is examined by this scan at most once across the whole call. Re-scanning `bytes[i..]` - // for every `<` instead would answer identically and cost O(n^2) on input an untrusted upstream - // chooses, which is the shape `` left after the cursor, no later `<` can close either, so there is nothing to look for. - let mut next_tag_end = find_tag_end(bytes, 0); - while i < bytes.len() { - if bytes[i] == b'<' && starts_tag(bytes, i) { - if next_tag_end.is_some_and(|end| end < i) { - next_tag_end = find_tag_end(bytes, i); - } - if let Some(end) = next_tag_end { - i = end + 1; - // The deletion just made `out`'s tail adjacent to `bytes[i..]`. That is the one and - // only way two non-adjacent pieces of input meet, so it is the one and only place a - // tag can appear in the output that was not one in the input. - collapse_reconstituted_tags(&mut out, bytes, &mut i, &mut next_tag_end); - continue; - } - // Unterminated: no `>` closes this `<` anywhere before end of input, so it is not a tag. - // Fall through and copy the `<` as ordinary text — never drop the tail. Every later - // ``-less and takes the same inert path, so the - // remainder round-trips byte-for-byte. - } - // Copy one whole UTF-8 character, never one byte: slicing a multi-byte character in half - // would produce invalid UTF-8, and `String::push_str` on a bad slice would panic. - let ch_len = utf8_len(bytes[i]); - let end = (i + ch_len).min(bytes.len()); - out.push_str(&input[i..end]); - i = end; - } - out -} - -/// [`normalise`] applied to an optional string, which is the shape every catalogue field has. -pub fn normalise_opt(input: Option<&str>) -> Option { - input.map(normalise) -} - -/// Remove the tag that the just-completed deletion may have reconstituted across the seam between -/// the emitted text and `bytes[i..]`, and keep removing while each removal makes another. -/// -/// Only a `<` or ``, so the cursor -/// strictly increases and `out` strictly shrinks: the loop runs at most as many times as there are -/// bytes, and the whole scan stays linear. -fn collapse_reconstituted_tags( - out: &mut String, - bytes: &[u8], - i: &mut usize, - next_tag_end: &mut Option, -) { - while let Some(opener) = pending_opener(out, bytes, *i) { - if next_tag_end.is_some_and(|end| end < *i) { - *next_tag_end = find_tag_end(bytes, *i); - } - // No `>` left anywhere ahead, so the seam cannot close into a tag and never will — deletions - // only ever remove bytes, so a `>` cannot appear later than the input already has one. - let Some(end) = *next_tag_end else { return }; - out.truncate(out.len() - opener); - *i = end + 1; - } -} - -/// The length of the `<` or ` Option { - let head = - matches!(bytes.get(i), Some(c) if c.is_ascii_alphabetic() || *c == b'!' || *c == b'?'); - if out.ends_with("`". -fn starts_tag(bytes: &[u8], i: usize) -> bool { - let mut j = i + 1; - if bytes.get(j) == Some(&b'/') { - j += 1; - } - matches!(bytes.get(j), Some(c) if c.is_ascii_alphabetic() || *c == b'!' || *c == b'?') -} - -/// The index of the `>` closing the tag opened at `i`, or `None` when the input ends first. -fn find_tag_end(bytes: &[u8], i: usize) -> Option { - bytes[i..].iter().position(|b| *b == b'>').map(|p| i + p) -} - -/// The byte length of the UTF-8 character starting with `b`. A continuation byte answers 1, which -/// cannot happen for well-formed input reached through `&str` and keeps the loop total if it ever -/// did. -fn utf8_len(b: u8) -> usize { - match b { - 0x00..=0x7F => 1, - 0xC0..=0xDF => 2, - 0xE0..=0xEF => 3, - 0xF0..=0xF7 => 4, - _ => 1, - } -} - -#[cfg(test)] -#[path = "tests/sanitize_tests.rs"] -mod sanitize_tests; diff --git a/crates/busbar-plane-mcp/src/seal.rs b/crates/busbar-plane-mcp/src/seal.rs deleted file mode 100644 index 58722dc755..0000000000 --- a/crates/busbar-plane-mcp/src/seal.rs +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE SEALED `requestState`: busbar's own ask state, minted and opened by the door over the host's -//! `sign` (ARCHITECT Q-L3B-ASK). The payload is the [`AskState`] as JSON; the seal is the host's -//! signature of it under the door's declared signing domain, a subkey derived from the deployment's -//! fleet-shared signing key, so a state minted on one node opens on every node of the deployment. -//! -//! The signature is deterministic (Ed25519), so opening re-signs the payload and compares the two in -//! constant time: the plugin never holds key material and the host needs no second verb. -//! -//! The form is `base64url(payload) "." base64url(signature)`, unpadded and strict: a decoder that -//! tolerated trailing bytes would accept a tampered value whose prefix still verifies. - -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use base64::Engine as _; - -use crate::ask::{AskState, Rejected}; - -/// The signature length the host's `sign` answers. -pub const SIGNATURE_LEN: usize = 64; - -/// The sealed form of `state`, signed by `sign`; `None` when it cannot be signed. -pub fn seal_state( - state: &AskState, - sign: &mut dyn FnMut(&[u8]) -> Option>, -) -> Option { - let payload = serde_json::to_vec(state).ok()?; - let signature = sign(&payload)?; - Some(format!( - "{}.{}", - URL_SAFE_NO_PAD.encode(&payload), - URL_SAFE_NO_PAD.encode(signature) - )) -} - -/// The state `blob` seals, its signature re-made by `sign` and compared in constant time. -/// -/// # Errors -/// -/// [`Rejected::Malformed`] when it is not this form or its payload is not a state; -/// [`Rejected::BadSignature`] when the seal does not verify (or cannot be re-made). -pub fn unseal_state( - blob: &str, - sign: &mut dyn FnMut(&[u8]) -> Option>, -) -> Result { - let (payload, signature) = blob.split_once('.').ok_or(Rejected::Malformed)?; - let payload = strict(payload).ok_or(Rejected::Malformed)?; - let presented = strict(signature).ok_or(Rejected::Malformed)?; - let made = sign(&payload).ok_or(Rejected::BadSignature)?; - if presented.len() != SIGNATURE_LEN || !same(&presented, &made) { - return Err(Rejected::BadSignature); - } - serde_json::from_slice(&payload).map_err(|_| Rejected::Malformed) -} - -/// `text` decoded as unpadded base64url, and only when it is that value's one spelling. -fn strict(text: &str) -> Option> { - let bytes = URL_SAFE_NO_PAD.decode(text).ok()?; - (URL_SAFE_NO_PAD.encode(&bytes) == text).then_some(bytes) -} - -/// Whether `a` and `b` are equal, in time that depends only on their lengths. -fn same(a: &[u8], b: &[u8]) -> bool { - a.len() == b.len() && a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0 -} - -#[cfg(test)] -#[path = "tests/seal_tests.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/subscribe.rs b/crates/busbar-plane-mcp/src/subscribe.rs deleted file mode 100644 index e7272dbf7e..0000000000 --- a/crates/busbar-plane-mcp/src/subscribe.rs +++ /dev/null @@ -1,415 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! `subscriptions/listen`: THE SERVER-TO-CLIENT CHANNEL OF REVISION `2026-07-28`, as a pure state -//! machine the door drives on whichever carrier holds it open (the served engine's -//! `subscribe.rs`, every rule kept). -//! -//! - THE ACKNOWLEDGEMENT IS A NARROWING ([`Listen::open`]): it carries the ACCEPTED subset, never -//! the requested one: the three list-changed categories, and `resourceSubscriptions` narrowed per -//! uri to what this caller is entitled to read. A request that opts in to nothing deliverable is -//! refused: a stream that can deliver nothing is a connection held open to say nothing. -//! - WHAT COUNTS AS A CHANGE IS THIS CALLER'S CHANGE: a generation move is the cheap gate, followed -//! by a comparison of the catalogue THIS CALLER CAN SEE ([`change_keys`]), so a registration the -//! caller may not reach never wakes its stream. -//! - THE PERMISSION IS RE-ASKED ON EVERY FRAME (the door passes [`Standing`]): a principal that has -//! stopped resolving live ends the stream on that frame, with the reason. -//! - THE STREAM IS BOUNDED ([`MAX_LIFETIME_NS`]) and ends by saying so: the revision's own -//! `SubscriptionsListenResult`, correlated to the request that opened it. -//! - QUIET IS NOT NOTHING: an idle stream says it is alive every [`KEEPALIVE_NS`]. - -use std::collections::BTreeSet; - -use serde_json::{json, Map, Value}; - -use crate::catalogue::{Catalogue, Lookup}; - -/// How long one subscription is held before it is closed with a graceful result. LOAD-BEARING FOR -/// AUTHORISATION as well as liveness: it bounds everything a frame cannot re-derive. -pub const MAX_LIFETIME_NS: u64 = 300 * 1_000_000_000; - -/// How often a stream that has nothing to say says it is alive. -pub const KEEPALIVE_NS: u64 = 15 * 1_000_000_000; - -/// The ceiling on distinct uris one `subscriptions/listen` may name under `resourceSubscriptions`, -/// checked on the raw (deduplicated) request, before entitlement narrows it, and refused, never -/// truncated. -pub const MAX_SUBSCRIBED_URIS: usize = 64; - -/// The notification that acknowledges a subscription. -pub const ACKNOWLEDGED: &str = "notifications/subscriptions/acknowledged"; - -/// The `_meta` key a frame names its subscription under. -pub const SUBSCRIPTION_ID: &str = "io.modelcontextprotocol/subscriptionId"; - -/// The JSON-RPC code for invalid params. -const INVALID_PARAMS: i64 = -32602; - -/// The base protocol's invalid request (a lapsed permission's closing frame). -const INVALID_REQUEST: i64 = -32600; - -/// The three list-changed categories, and the notification each becomes. -const KINDS: [(&str, &str); 3] = [ - ("toolsListChanged", "notifications/tools/list_changed"), - ("promptsListChanged", "notifications/prompts/list_changed"), - ( - "resourcesListChanged", - "notifications/resources/list_changed", - ), -]; - -/// What a caller asked for, and what it is given: the subscription filter. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct Filter { - /// `toolsListChanged`, `promptsListChanged`, `resourcesListChanged`. - pub lists: [bool; 3], - /// `resourceSubscriptions`; `None` = the category is not in it. - pub resources: Option>, -} - -impl Filter { - /// The filter as the wire carries it. - #[must_use] - pub fn render(&self) -> Value { - let mut out = Map::new(); - for (on, (key, _)) in self.lists.iter().zip(KINDS) { - if *on { - out.insert(key.to_string(), Value::Bool(true)); - } - } - if let Some(uris) = &self.resources { - out.insert("resourceSubscriptions".to_string(), json!(uris)); - } - Value::Object(out) - } - - /// Read a requested filter; a shape the protocol does not define reads as no category. - fn read(notifications: Option<&Value>) -> Self { - let Some(n) = notifications.and_then(Value::as_object) else { - return Filter::default(); - }; - let mut lists = [false; 3]; - for (slot, (key, _)) in lists.iter_mut().zip(KINDS) { - match n.get(key) { - None => {} - Some(Value::Bool(b)) => *slot = *b, - Some(_) => return Filter::default(), - } - } - let resources = match n.get("resourceSubscriptions") { - None => None, - Some(Value::Array(a)) => { - // Deduplicated through a set, and read no further than the first uri past the - // ceiling: the request is refused for it, so the rest of a long array costs nothing. - let mut seen = BTreeSet::new(); - let mut uris = Vec::new(); - for u in a { - let Some(u) = u.as_str() else { - return Filter::default(); - }; - if seen.insert(u) { - uris.push(u.to_string()); - if uris.len() > MAX_SUBSCRIBED_URIS { - break; - } - } - } - Some(uris) - } - Some(_) => return Filter::default(), - }; - Filter { lists, resources } - } -} - -/// Whether the caller's permission still stands this frame. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Standing { - /// It stands. - Live, - /// It lapsed, for this reason: the stream ends on this frame, saying so. - Lapsed { - /// The sentence. - message: String, - /// The machine reason. - reason: String, - }, -} - -/// Where the stream is. -#[derive(Debug, Clone, PartialEq, Eq)] -enum Phase { - /// The acknowledgement is owed, and owed first. - Acknowledge, - /// Acknowledged; watching the generation. - Watch { generation: u64, seen: [u64; 3] }, - /// The final frame went: the next step closes it. - Ended, -} - -/// What one step came to. -#[derive(Debug, Clone, PartialEq)] -pub enum Step { - /// Frames to write, in order; the stream goes on. - Frames(Vec), - /// Nothing happened for a keepalive's while: say it is alive. - Keepalive, - /// Nothing to say yet. - Quiet, - /// The last frames to write; the stream ends after them. - End(Vec), - /// The stream is over. - Closed, -} - -/// ONE SUBSCRIPTION. -#[derive(Debug, Clone)] -pub struct Listen { - /// The listen request's own id: the subscription's only durable name. - pub id: Value, - accepted: Filter, - meta: Value, - phase: Phase, - opened_ns: u64, - last_write_ns: u64, -} - -/// A refusal envelope for an invalid listen request. -fn invalid(id: &Value, message: &str) -> Value { - json!({ - "jsonrpc": "2.0", - "id": id, - "error": { "code": INVALID_PARAMS, "message": message }, - }) -} - -impl Listen { - /// OPEN one subscription for `params` under `id` at `now_ns`: the requested filter read, - /// bounded, and narrowed by `entitled` (whether this caller's grant reaches a resource at a uri); - /// `Err` is the refusal envelope. - /// - /// # Errors - /// - /// Too many uris, or nothing deliverable for this caller. - pub fn open( - params: Option<&Value>, - id: Value, - now_ns: u64, - entitled: impl Fn(&str) -> bool, - ) -> Result { - let requested = Filter::read(params.and_then(|p| p.get("notifications"))); - if let Some(uris) = &requested.resources { - if uris.len() > MAX_SUBSCRIBED_URIS { - return Err(invalid( - &id, - &format!( - "`params.notifications.resourceSubscriptions` names {} distinct uris; a \ - `subscriptions/listen` call may watch at most {MAX_SUBSCRIBED_URIS} at \ - once. Open a second subscription for the rest.", - uris.len() - ), - )); - } - } - let accepted = Filter { - lists: requested.lists, - resources: requested - .resources - .map(|uris| uris.into_iter().filter(|u| entitled(u)).collect::>()) - .filter(|kept| !kept.is_empty()), - }; - if !accepted.lists.iter().any(|on| *on) && accepted.resources.is_none() { - return Err(invalid( - &id, - "`params.notifications` opts in to no category this server delivers for you. \ - busbar delivers `toolsListChanged`, `promptsListChanged`, \ - `resourcesListChanged`, and `resourceSubscriptions` for uris your grant reaches \ - — a subscription naming only resources you cannot read is a stream with nothing \ - to say.", - )); - } - let meta = json!({ SUBSCRIPTION_ID: id }); - Ok(Listen { - id, - accepted, - meta, - phase: Phase::Acknowledge, - opened_ns: now_ns, - last_write_ns: now_ns, - }) - } - - /// The accepted filter. - #[must_use] - pub fn accepted(&self) -> &Filter { - &self.accepted - } - - /// One notification tagged with this subscription. - fn notification(&self, method: &str, extra: Value) -> Value { - let mut params = match extra { - Value::Object(map) => map, - _ => Map::new(), - }; - params.insert("_meta".to_string(), self.meta.clone()); - json!({ "jsonrpc": "2.0", "method": method, "params": Value::Object(params) }) - } - - /// The revision's own graceful end, correlated to the request that opened the stream. - fn complete(&self) -> Value { - json!({ - "jsonrpc": "2.0", - "id": self.id, - "result": { "resultType": "complete", "_meta": self.meta }, - }) - } - - /// THE NEXT STEP at `now_ns`, over the live `catalogue` under the caller's grants `admit`, - /// with its permission re-asked (`standing`), and the upstream announcements `updates` (each - /// `(server, uri)`) recorded since the last step. - pub fn step( - &mut self, - catalogue: &Catalogue, - admit: &impl Fn(&str, &str) -> bool, - standing: &Standing, - updates: &[(String, String)], - now_ns: u64, - ) -> Step { - if self.phase == Phase::Ended { - return Step::Closed; - } - if let Standing::Lapsed { message, reason } = standing { - self.phase = Phase::Ended; - return Step::End(vec![json!({ - "jsonrpc": "2.0", - "id": self.id, - "error": { - "code": INVALID_REQUEST, - "message": message, - "data": { "reason": reason }, - }, - })]); - } - if now_ns.saturating_sub(self.opened_ns) >= MAX_LIFETIME_NS { - self.phase = Phase::Ended; - return Step::End(vec![self.complete()]); - } - let generation = catalogue.generation(); - match &mut self.phase { - Phase::Acknowledge => { - let seen = change_keys(catalogue, admit); - self.phase = Phase::Watch { generation, seen }; - self.last_write_ns = now_ns; - let ack = self.notification( - ACKNOWLEDGED, - json!({ "notifications": self.accepted.render() }), - ); - Step::Frames(vec![ack]) - } - Phase::Watch { - generation: at, - seen, - } => { - let mut changed = Vec::new(); - if *at != generation { - *at = generation; - let fresh = change_keys(catalogue, admit); - for (i, (_, method)) in KINDS.into_iter().enumerate() { - if fresh[i] != seen[i] && self.accepted.lists[i] { - changed.push(method); - } - } - *seen = fresh; - } - let mut out: Vec = changed - .into_iter() - .map(|m| self.notification(m, json!({}))) - .collect(); - // THE RESOURCE-UPDATE RELAY: an upstream's announcement reaches a subscriber only - // when the subscriber asked for the uri, the uri resolves under its live grant to an - // operator-declared resource, and that resource is the announcing server's. - if let Some(uris) = &self.accepted.resources { - for (server, uri) in updates { - if !uris.iter().any(|u| u == uri) { - continue; - } - let entitled = matches!( - catalogue.resource_by_uri(admit, uri), - Lookup::One(entry) if entry.server == *server - ); - if entitled { - out.push(self.notification( - "notifications/resources/updated", - json!({ "uri": uri }), - )); - } - } - } - if !out.is_empty() { - self.last_write_ns = now_ns; - return Step::Frames(out); - } - if now_ns.saturating_sub(self.last_write_ns) >= KEEPALIVE_NS { - self.last_write_ns = now_ns; - return Step::Keepalive; - } - Step::Quiet - } - Phase::Ended => Step::Closed, - } - } - - /// When the stream next has something to say with nothing changing: its keepalive or its bound. - #[must_use] - pub fn next_due_ns(&self) -> u64 { - (self.last_write_ns.saturating_add(KEEPALIVE_NS)) - .min(self.opened_ns.saturating_add(MAX_LIFETIME_NS)) - } -} - -/// A change key over one grant-scoped catalogue slice: FNV-1a, trusted for nothing (a collision -/// costs one missed re-read of a list the caller can re-read at any time). -fn change_key(mut parts: Vec<&str>) -> u64 { - parts.sort_unstable(); - let mut hash: u64 = 0xcbf2_9ce4_8422_2325; - for part in parts { - for byte in part.as_bytes().iter().chain(std::iter::once(&0u8)) { - hash ^= u64::from(*byte); - hash = hash.wrapping_mul(0x0000_0100_0000_01b3); - } - } - hash -} - -/// The three change keys of what ONE CALLER can see. The tool key carries each tool's schema hash: -/// a tool whose arguments changed shape under an unchanged name is one a client must re-read for. -#[must_use] -pub fn change_keys(catalogue: &Catalogue, admit: &impl Fn(&str, &str) -> bool) -> [u64; 3] { - let tools = catalogue.tools_for(admit); - let mut tool_parts: Vec<&str> = Vec::with_capacity(tools.len() * 2); - for tool in &tools { - tool_parts.push(tool.namespaced.as_str()); - tool_parts.push(tool.schema_hash.as_deref().unwrap_or("")); - } - [ - change_key(tool_parts), - change_key( - catalogue - .prompts_for(admit) - .iter() - .map(|p| p.namespaced.as_str()) - .collect(), - ), - change_key( - catalogue - .resources_for(admit) - .iter() - .map(|r| r.namespaced.as_str()) - .collect(), - ), - ] -} - -#[cfg(test)] -#[path = "tests/subscribe_tests.rs"] -mod tests; diff --git a/crates/busbar-plane-mcp/src/tests/adapt_tests.rs b/crates/busbar-plane-mcp/src/tests/adapt_tests.rs deleted file mode 100644 index 4c812a9152..0000000000 --- a/crates/busbar-plane-mcp/src/tests/adapt_tests.rs +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -use super::*; -use serde_json::json; - -#[test] -fn a_stateless_request_keeps_the_stateless_reading_whatever_else_it_carries() { - let stateless = json!({"jsonrpc":"2.0","id":1,"method":"tools/list", - "params":{"_meta":{META_PROTOCOL_VERSION: "2026-07-28"}}}); - assert_eq!(classify_post(&stateless, None, None), PostKind::Stateless); - // The stateless revision ignores a session header (it has no sessions). - assert_eq!( - classify_post(&stateless, Some("abc"), None), - PostKind::Stateless - ); - // A request with no marker, no session and no initialize is answered as it always was. - let bare = json!({"jsonrpc":"2.0","id":1,"method":"tools/list"}); - assert_eq!(classify_post(&bare, None, None), PostKind::Stateless); -} - -#[test] -fn initialize_session_and_message_address_posts_are_told_apart() { - let init = json!({"jsonrpc":"2.0","id":0,"method":"initialize", - "params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"c","version":"1"}}}); - assert_eq!(classify_post(&init, None, None), PostKind::Initialize); - let call = json!({"jsonrpc":"2.0","id":2,"method":"tools/list"}); - assert_eq!(classify_post(&call, Some("s"), None), PostKind::InSession); - assert_eq!( - classify_post(&init, None, Some("s")), - PostKind::EventStreamMessage - ); -} - -#[test] -fn the_message_address_session_is_read_from_the_query() { - assert_eq!(message_session_of(Some("sessionId=abc")), Some("abc")); - assert_eq!( - message_session_of(Some("x=1&sessionId=abc&y=2")), - Some("abc") - ); - assert_eq!(message_session_of(Some("sessionId=")), None); - assert_eq!(message_session_of(Some("session=abc")), None); - assert_eq!(message_session_of(None), None); -} - -#[test] -fn red_no_stateless_only_method_reaches_a_session_client() { - for m in [ - "server/discover", - "subscriptions/listen", - "tasks/get", - "tasks/update", - "tasks/cancel", - "initialize", - ] { - assert_eq!( - session_method(Some(m), true), - SessionMethod::NotFound, - "{m}" - ); - } - assert_eq!( - session_method(Some("tools/call"), true), - SessionMethod::Dispatch - ); - assert_eq!(session_method(Some("ping"), true), SessionMethod::Ping); - assert_eq!( - session_method(Some(METHOD_INITIALIZED), false), - SessionMethod::Accept - ); - assert_eq!(session_method(None, true), SessionMethod::Accept); - for m in [ - "resources/subscribe", - "resources/unsubscribe", - "logging/setLevel", - ] { - assert_eq!( - session_method(Some(m), true), - SessionMethod::Session, - "{m}: subscribe stays for the old revisions" - ); - } -} - -#[test] -fn raising_adds_the_stateless_meta_and_mirrors_the_name() { - let mut m = json!({"jsonrpc":"2.0","id":3,"method":"tools/call", - "params":{"name":"echo","arguments":{},"_meta":{"progressToken":7}}}); - let mirror = raise(&mut m).unwrap(); - assert_eq!(mirror.version, "2026-07-28"); - assert_eq!(mirror.method, "tools/call"); - assert_eq!(mirror.name.as_deref(), Some("echo")); - let meta = &m["params"]["_meta"]; - assert_eq!(meta[META_PROTOCOL_VERSION], "2026-07-28"); - assert_eq!(meta[META_CLIENT_CAPABILITIES], json!({})); - assert_eq!(meta["progressToken"], 7, "the client's own _meta survives"); - - let mut bare = json!({"jsonrpc":"2.0","id":4,"method":"tools/list"}); - let mirror = raise(&mut bare).unwrap(); - assert_eq!(mirror.name, None); - assert!(bare["params"]["_meta"].is_object()); - - let mut bad = json!({"jsonrpc":"2.0","id":5,"method":"tools/list","params":[1]}); - assert_eq!(raise(&mut bad), None); -} - -#[test] -fn lowering_strips_stateless_members_and_refuses_incomplete_results() { - let mut r = json!({"tools":[],"resultType":"complete","cacheScope":"caller","ttlMs":1000}); - lower_result(&mut r).unwrap(); - assert_eq!(r, json!({"tools":[]})); - let mut ir = json!({"resultType":"input_required","inputRequests":{}}); - assert_eq!(lower_result(&mut ir), Err(NotExpressible)); - let mut task = json!({"resultType":"task"}); - assert_eq!(lower_result(&mut task), Err(NotExpressible)); -} - -#[test] -fn red_initialize_declares_no_stateless_only_capability() { - let discovery = json!({ - "protocolVersion":"2026-07-28", - "serverInfo":{"name":"busbar","version":"1.6.0"}, - "capabilities":{ - "tools":{"listChanged":true},"prompts":{"listChanged":true}, - "resources":{"listChanged":true,"subscribe":true}, - "completions":{},"logging":{},"extensions":{"x":{}} - }, - "methods":["server/discover"] - }); - let r = initialize_result(&discovery, Revision::R2025_11_25, false); - assert_eq!(r["protocolVersion"], "2025-11-25"); - assert_eq!(r["serverInfo"]["name"], "busbar"); - let caps = r["capabilities"].as_object().unwrap(); - let mut keys: Vec<&str> = caps.keys().map(String::as_str).collect(); - keys.sort_unstable(); - assert_eq!( - keys, - ["completions", "logging", "prompts", "resources", "tools"] - ); - assert_eq!( - caps["resources"], - json!({"listChanged": false, "subscribe": true}), - "subscribe stays for the old revisions" - ); - assert!(r.get("methods").is_none()); - - let old = initialize_result(&discovery, Revision::R2024_11_05, true); - assert!(old["capabilities"].get("completions").is_none()); - assert_eq!(old["capabilities"]["tools"], json!({"listChanged": true})); - - let none = initialize_result(&json!({"capabilities":{}}), Revision::R2025_06_18, true); - assert_eq!(none["capabilities"], json!({})); -} - -#[test] -fn event_stream_framing() { - assert_eq!( - frame(Some("0-1"), None, "{\"a\":1}"), - "id: 0-1\ndata: {\"a\":1}\n\n" - ); - assert_eq!( - frame(None, Some(EVENT_MESSAGE), "a\nb"), - "event: message\ndata: a\ndata: b\n\n" - ); - assert_eq!( - endpoint_event("/mcp", "0123"), - "event: endpoint\ndata: /mcp?sessionId=0123\n\n" - ); -} - -#[test] -fn the_param_mirror_names_every_annotated_string_argument() { - let schema = json!({"properties":{ - "region":{"type":"string","x-mcp-header":"Region"}, - "count":{"type":"integer","x-mcp-header":"Count"}, - "note":{"type":"string"} - }}); - let args = json!({"region":"eu-west","count":3,"note":"n"}); - assert_eq!( - param_mirror(Some(&schema), Some(&args)), - vec![("mcp-param-Region".to_string(), "eu-west".to_string())] - ); - assert!(param_mirror(None, Some(&args)).is_empty()); - assert!(param_mirror(Some(&schema), None).is_empty()); -} diff --git a/crates/busbar-plane-mcp/src/tests/answer.rs b/crates/busbar-plane-mcp/src/tests/answer.rs deleted file mode 100644 index c780d30ade..0000000000 --- a/crates/busbar-plane-mcp/src/tests/answer.rs +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! Each arrival is answered here or sent on, and what is answered here is the served engine's. - -use serde_json::{json, Value}; - -use super::*; -use crate::tool_arrival::decide; - -const SECTION: &[u8] = br#"{ - "fs": { - "url": "https://mcp.example/fs", - "pin": {"mechanism": "unpinned"}, - "tools_allow": {"read_file": {"schema_hash": "sha256:aa"}}, - "prompts_allow": { - "greet": {"template": "Hello"}, - "confirm": {"template": "Sure?", "ask_caller": [{"ok": {"method": "elicitation/create"}}]} - }, - "resources_allow": {"file:///readme": {"text": "hi"}} - } -}"#; - -fn catalogue() -> Catalogue { - Catalogue::build( - 1, - &crate::door::read_tools_section(SECTION).expect("the section reads"), - ) -} - -fn everyone(_: &str, _: &str) -> bool { - true -} - -/// A stateless-revision request for `method`, decided as an arrival. -fn request(method: &str, params: Value) -> (Disposition, Value) { - let mut params = params; - params["_meta"] = json!({ - "io.modelcontextprotocol/protocolVersion": PROTOCOL_VERSION, - "io.modelcontextprotocol/clientCapabilities": {}, - }); - let body = json!({"jsonrpc": "2.0", "id": 9, "method": method, "params": params}); - let bytes = serde_json::to_vec(&body).expect("bytes"); - let name = params - .get("name") - .or_else(|| params.get("uri")) - .and_then(Value::as_str) - .map(str::to_string); - let method_owned = method.to_string(); - let d = decide(&bytes, |f| match f { - "mcp-protocol-version" => Some(PROTOCOL_VERSION), - "mcp-method" => Some(method_owned.as_str()), - "mcp-name" => name.as_deref(), - _ => None, - }); - (d, params) -} - -fn run(method: &str, params: Value) -> Answer { - let (d, params) = request(method, params); - answer(&d, Some(¶ms), &catalogue(), &everyone, |_| false) -} - -fn body(a: &Answer) -> Value { - match a { - Answer::Here { body, .. } => serde_json::from_slice(body).expect("a document"), - Answer::Far => panic!("answered elsewhere"), - } -} - -#[test] -fn the_catalogue_reads_are_answered_here() { - for (method, member) in [ - ("tools/list", "tools"), - ("prompts/list", "prompts"), - ("resources/list", "resources"), - ("resources/templates/list", "resourceTemplates"), - ] { - let a = run(method, json!({})); - assert!(matches!(a, Answer::Here { status: 200, .. }), "{method}"); - assert!(body(&a)["result"][member].is_array(), "{method}"); - assert_eq!(body(&a)["id"], 9); - } - let read = run("resources/read", json!({"uri": "file:///readme"})); - assert_eq!(body(&read)["result"]["contents"][0]["text"], "hi"); - let get = run("prompts/get", json!({"name": "fs_greet"})); - assert_eq!( - body(&get)["result"]["messages"][0]["content"]["text"], - "Hello" - ); - let done = run("completion/complete", json!({})); - assert_eq!(body(&done)["result"]["completion"]["total"], 0); -} - -#[test] -fn discover_counts_what_the_caller_reaches_and_names_the_revisions() { - let a = run("server/discover", json!({})); - let v = body(&a); - assert_eq!(v["result"]["supportedVersions"], json!([PROTOCOL_VERSION])); - assert_eq!(v["result"]["servers"], json!(["fs"])); - assert_eq!( - v["result"]["counts"], - json!({"tools": 1, "prompts": 2, "resources": 1}) - ); - assert_eq!(v["result"]["registryEmpty"], false); - assert_eq!(v["result"]["resultType"], "complete"); - assert_eq!( - v["result"]["capabilities"]["extensions"], - json!({ TASKS_EXTENSION_ID: {} }) - ); - let (d, params) = request("server/discover", json!({})); - let none = answer( - &d, - Some(¶ms), - &catalogue(), - &|_: &str, _: &str| false, - |_| false, - ); - assert_eq!(body(&none)["result"]["servers"], json!([])); -} - -/// A call, and a prompt that asks its caller first, are not answered here. -#[test] -fn a_call_and_an_asking_prompt_go_on() { - assert_eq!( - run("tools/call", json!({"name": "fs_read_file"})), - Answer::Far - ); - assert_eq!( - run("prompts/get", json!({"name": "fs_confirm"})), - Answer::Far - ); -} - -/// A local refusal is answered with its own status and body. -#[test] -fn a_local_refusal_answers_its_own_status() { - let a = run("resources/read", json!({"uri": "file:///nope"})); - let Answer::Here { status, .. } = &a else { - panic!("answered here") - }; - assert_eq!(*status, 404); - assert_eq!(body(&a)["error"]["code"], -32000); -} - -/// A notification is acknowledged with no body; a refused arrival answers its refusal. -#[test] -fn a_notice_is_acknowledged_and_a_refusal_answered() { - let d = decide(br#"{"jsonrpc":"2.0","method":"notifications/x"}"#, |_| None); - let a = answer(&d, None, &catalogue(), &everyone, |_| false); - assert_eq!( - a, - Answer::Here { - status: STATUS_ACCEPTED, - body: Vec::new() - } - ); - let d = decide(b"not json", |_| None); - let a = answer(&d, None, &catalogue(), &everyone, |_| false); - assert!(matches!(a, Answer::Here { status: 400, .. })); - assert_eq!(body(&a)["error"]["code"], -32700); -} diff --git a/crates/busbar-plane-mcp/src/tests/argguard_tests.rs b/crates/busbar-plane-mcp/src/tests/argguard_tests.rs deleted file mode 100644 index efc643e517..0000000000 --- a/crates/busbar-plane-mcp/src/tests/argguard_tests.rs +++ /dev/null @@ -1,759 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! THE ARGUMENT-SIDE SSRF GUARD: a metadata endpoint planted in a NESTED tool argument is refused, -//! a legitimate external URL in the SAME field still passes, and the walk is proven to have visited -//! a non-zero number of fields. -//! -//! That last one is not decoration. A walker that finds nothing refuses nothing and passes every -//! refusal test it is given, so every test here that asserts a refusal also asserts the walk -//! reached the field, and one test pins the visited counts exactly. - -use super::{guard, ArgRefusal, ArgWhy}; -use busbar_contract::abi::host::service::{ - DEST_ALLOWED, DEST_INTERNAL, DEST_METADATA, DEST_OBFUSCATED, -}; -use busbar_contract::net::{ - dns_name_is_internal, host_ip, host_is_cloud_metadata, ip_is_internal, - is_alternate_ipv4_encoding, -}; -use serde_json::{json, Value}; - -/// A STAND-IN HOST'S `dest.judge`, over the contract's address predicates: what the walk is judged -/// by in these tests. `refuse_private` is `DEST_REFUSE_PRIVATE` (a registration with no private -/// reach). The deployment's real judge answering a served call is proven in the root crate -/// (`root::tests::door_steps`). -fn judge(refuse_private: bool) -> impl FnMut(&str) -> Option { - move |dest: &str| { - let host = dest - .strip_prefix('[') - .and_then(|h| h.strip_suffix(']')) - .unwrap_or(dest); - Some(if host_is_cloud_metadata(host) { - DEST_METADATA - } else if is_alternate_ipv4_encoding(host) { - DEST_OBFUSCATED - } else if refuse_private - && (dns_name_is_internal(host) || host_ip(host).is_some_and(|ip| ip_is_internal(&ip))) - { - DEST_INTERNAL - } else { - DEST_ALLOWED - }) - } -} - -fn public() -> impl FnMut(&str) -> Option { - judge(true) -} - -fn private_ok() -> impl FnMut(&str) -> Option { - judge(false) -} - -/// A schema whose `uri` field sits two levels down inside nested objects — the shape a real -/// webhook-taking tool has, and the shape a flat walk misses. -fn nested_uri_schema() -> Value { - json!({ - "type": "object", - "properties": { - "message": {"type": "string"}, - "delivery": { - "type": "object", - "properties": { - "callback": {"type": "string", "format": "uri"} - } - } - } - }) -} - -fn nested_uri_args(url: &str) -> Value { - json!({"message": "hello", "delivery": {"callback": url}}) -} - -/// THE HEADLINE CASE. A model that has just read a hostile tool description puts the instance -/// metadata endpoint into a NESTED argument of a tool whose pinned schema declares that field a -/// `uri`. It is refused, at the exact field, naming the reason. -#[test] -fn a_metadata_endpoint_in_a_nested_uri_argument_is_refused() { - let args = nested_uri_args("http://169.254.169.254/latest/meta-data/iam/security-credentials/"); - let err = guard(&nested_uri_schema(), &args, public()) - .expect_err("the instance metadata endpoint must not be passed through to the upstream"); - assert_eq!( - err, - ArgRefusal { - pointer: "/delivery/callback".into(), - declared_format: Some("uri".into()), - why: ArgWhy::CloudMetadata("169.254.169.254".into()), - } - ); - // The rendered refusal names the field and the reason, so an operator reading it can act. - let rendered = err.to_string(); - assert!(rendered.contains("/delivery/callback"), "{rendered}"); - assert!(rendered.contains("169.254.169.254"), "{rendered}"); -} - -/// THE CONTROL. A guard with no control is a closed door, and a closed door breaks every real tool. -/// The SAME field, the SAME schema, a legitimate external URL: it passes, and the walk reports that -/// it did in fact judge it. -#[test] -fn a_legitimate_external_url_in_the_same_field_still_passes() { - let args = nested_uri_args("https://hooks.example.com/services/T000/B000/xyz"); - let scan = guard(&nested_uri_schema(), &args, public()) - .expect("an ordinary external webhook URL must still be dispatched"); - assert_eq!( - scan.declared_judged, 1, - "the walk must have actually reached and judged the callback field" - ); - assert_eq!(scan.undeclared_judged, 0); - assert_eq!(scan.strings_seen, 2, "`message` and `callback`"); -} - -/// THE FLOOR, pinned exactly rather than as an inequality. Nested object, array of strings, a field -/// four levels deep and a bare-host field are all in one document, and the counts say which of them -/// the walk reached. Five is the number of URL-ish fields the document declares; if the walk ever -/// reports fewer, it stopped following one of those four shapes. -#[test] -fn the_walk_visits_every_declared_url_field_and_says_how_many() { - let schema = json!({ - "type": "object", - "properties": { - "callback": {"type": "string", "format": "uri"}, - "mirrors": {"type": "array", "items": {"type": "string", "format": "uri"}}, - "delivery": {"type": "object", "properties": { - "endpoint": {"type": "object", "properties": { - "target": {"type": "object", "properties": { - "url": {"type": "string", "format": "uri"} - }} - }} - }}, - "origin": {"type": "string", "format": "hostname"}, - "note": {"type": "string"} - } - }); - let args = json!({ - "callback": "https://hooks.example.com/a", - "mirrors": ["https://a.example.com/1", "https://b.example.com/2"], - "delivery": {"endpoint": {"target": {"url": "https://deep.example.com/z"}}}, - "origin": "api.example.com", - // Free text that MENTIONS a metadata URL mid-sentence. The undeclared detector reads whole - // values, never substrings, so this is untouched — the documented limit, asserted rather - // than described. - "note": "the runbook says never to call http://169.254.169.254/ from a tool" - }); - let scan = guard(&schema, &args, public()).expect("every URL here is a public one"); - assert_eq!( - scan.declared_judged, 5, - "callback + 2 mirrors + the four-deep url + origin" - ); - assert_eq!(scan.undeclared_judged, 0, "the note is prose, not a URL"); - assert_eq!(scan.strings_seen, 6); - assert!(scan.judged() >= 5); -} - -/// Each element of an array of URLs is judged on its own, so a hostile entry cannot hide behind -/// admissible siblings. -#[test] -fn one_hostile_entry_in_an_array_of_urls_refuses_the_whole_call() { - let schema = json!({ - "type": "object", - "properties": { - "mirrors": {"type": "array", "items": {"type": "string", "format": "uri"}} - } - }); - let args = json!({"mirrors": [ - "https://a.example.com/1", - "https://b.example.com/2", - "http://169.254.169.254/latest/meta-data/" - ]}); - let err = guard(&schema, &args, public()).expect_err("the third entry must refuse the call"); - assert_eq!(err.pointer, "/mirrors/2"); - assert_eq!(err.why, ArgWhy::CloudMetadata("169.254.169.254".into())); - // The control: the same array with every entry public passes, and all three were judged. - let ok = json!({"mirrors": [ - "https://a.example.com/1", "https://b.example.com/2", "https://c.example.com/3" - ]}); - let scan = guard(&schema, &ok, public()).expect("three public mirrors are fine"); - assert_eq!(scan.declared_judged, 3); -} - -/// A URL-ish field FOUR levels below the argument root, inside an array inside an object inside an -/// object, is still reached. -#[test] -fn a_url_field_several_levels_deep_is_still_reached() { - let schema = json!({ - "type": "object", - "properties": {"a": {"type": "object", "properties": { - "b": {"type": "object", "properties": { - "c": {"type": "array", "items": {"type": "object", "properties": { - "d": {"type": "string", "format": "uri"} - }}} - }} - }}} - }); - let args = json!({"a": {"b": {"c": [{"d": "http://169.254.169.254/"}]}}}); - let err = guard(&schema, &args, public()).expect_err("depth must not launder the value"); - assert_eq!(err.pointer, "/a/b/c/0/d"); - let ok = json!({"a": {"b": {"c": [{"d": "https://ok.example.com/"}]}}}); - assert_eq!( - guard(&schema, &ok, public()) - .expect("a public URL at the same depth passes") - .declared_judged, - 1 - ); -} - -/// EVERY ENCODING THE TRANSPORT GUARD ALREADY HANDLES, handled here too: the alternate IPv4 forms, -/// IPv4-mapped IPv6, the `localhost` family, and the metadata DNS names. -#[test] -fn the_encodings_the_transport_guard_handles_are_refused_in_arguments_too() { - let metadata = [ - "http://169.254.169.254/latest/meta-data/", - // Alternate encodings of the SAME address: decimal, hex, per-octet octal. - "http://2852039166/", - "http://0xa9fea9fe/", - "http://0251.0376.0251.0376/", - // Percent-encoded dots, which a connecting stack decodes. - "http://169%2E254%2E169%2E254/", - // A trailing root dot, which a resolver treats as the same rooted name. - "http://169.254.169.254./", - // IPv4-mapped IPv6: the v4 ruleset must not be bypassable by a v6 spelling. - "http://[::ffff:169.254.169.254]/", - // Other clouds' endpoints, and the whole link-local range they live in. - "http://169.254.170.2/", - "http://100.100.100.200/", - "http://[fd00:ec2::254]/", - // BOTH metadata DNS names. - "http://metadata.google.internal/computeMetadata/v1/", - "http://instance-data.ec2.internal/latest/", - ]; - assert_eq!(metadata.len(), 12, "the metadata set must not shrink"); - for url in metadata { - let err = guard(&nested_uri_schema(), &nested_uri_args(url), public()) - .err() - .unwrap_or_else(|| panic!("`{url}` must be refused, but it passed")); - assert!( - matches!(err.why, ArgWhy::CloudMetadata(_)), - "`{url}` must be refused AS metadata, got {err:?}" - ); - } - - let obfuscated_loopback = [ - "http://2130706433/", - "http://0x7f000001/", - "http://017700000001/", - "http://127.1/", - ]; - assert_eq!(obfuscated_loopback.len(), 4); - for url in obfuscated_loopback { - let err = guard(&nested_uri_schema(), &nested_uri_args(url), public()) - .expect_err("an alternate IPv4 encoding must be refused"); - assert!( - matches!(err.why, ArgWhy::ObfuscatedHost(_)), - "`{url}` must be refused AS an obfuscated host, got {err:?}" - ); - } - - let internal = [ - "http://localhost:8080/x", - "http://LOCALHOST/x", - "http://tool.localhost/x", - "http://127.0.0.1/x", - "http://[::1]/x", - "http://[::ffff:127.0.0.1]/x", - "http://0.0.0.0/x", - "http://10.0.0.5/x", - "http://172.16.0.1/x", - "http://192.168.1.1/x", - "http://100.64.0.1/x", - "http://[fc00::1]/x", - "http://[fe80::1]/x", - ]; - assert_eq!(internal.len(), 13, "the internal set must not shrink"); - for url in internal { - let err = guard(&nested_uri_schema(), &nested_uri_args(url), public()) - .expect_err("an internal destination must be refused"); - assert!( - matches!(err.why, ArgWhy::InternalHost(_)), - "`{url}` must be refused AS internal, got {err:?}" - ); - } - - // THE CONTROL, again: ordinary public destinations in the same field are NOT refused, so the - // three loops above are not passing because everything is refused. - for url in ["https://api.example.com/v1/x", "https://93.184.216.34/x"] { - assert_eq!( - guard(&nested_uri_schema(), &nested_uri_args(url), public()) - .unwrap_or_else(|e| panic!("`{url}` must pass, got {e}")) - .declared_judged, - 1 - ); - } -} - -/// A metadata refusal is unconditional; an internal one is the operator's to opt into. That is the -/// same split `super::ssrf` draws, and it has to be the same one — an operator saying "this server -/// is on our internal network" has said nothing about the credential-vending endpoint. -#[test] -fn allow_private_admits_internal_hosts_and_never_admits_metadata() { - assert!(matches!( - guard( - &nested_uri_schema(), - &nested_uri_args("http://10.0.0.5/x"), - public() - ), - Err(ArgRefusal { - why: ArgWhy::InternalHost(_), - .. - }) - )); - assert!(guard( - &nested_uri_schema(), - &nested_uri_args("http://mcp.internal:8080/x"), - private_ok() - ) - .is_ok()); - assert!(guard( - &nested_uri_schema(), - &nested_uri_args("http://10.0.0.5/x"), - private_ok() - ) - .is_ok()); - - for url in [ - "http://169.254.169.254/latest/meta-data/", - "http://metadata.google.internal/", - "http://2852039166/", - ] { - let err = guard(&nested_uri_schema(), &nested_uri_args(url), private_ok()) - .expect_err("metadata is refused under every policy"); - assert!( - matches!(err.why, ArgWhy::CloudMetadata(_)), - "`{url}` under allow_private: {err:?}" - ); - } - // An obfuscated encoding stays refused under `allow_private` too: a value spelled so the check - // cannot read it is refused rather than guessed at. - assert!(matches!( - guard( - &nested_uri_schema(), - &nested_uri_args("http://2130706433/"), - private_ok() - ), - Err(ArgRefusal { - why: ArgWhy::ObfuscatedHost(_), - .. - }) - )); -} - -/// A declared absolute-URL field accepts `http(s)` and nothing else. Other schemes are refused by -/// absence rather than by a blocklist somebody has to keep up with. -#[test] -fn a_declared_uri_field_takes_no_scheme_but_http_and_https() { - let banned = [ - "file:///etc/passwd", - "gopher://169.254.169.254/", - "smb://host/share", - "ftp://host/", - "data:text/plain;base64,aGk=", - "ws://host/", - "jar:file:///x!/y", - ]; - assert_eq!(banned.len(), 7, "the banned-scheme set must not shrink"); - for url in banned { - let err = guard(&nested_uri_schema(), &nested_uri_args(url), private_ok()) - .expect_err("only http(s) is passed through"); - assert!( - matches!(err.why, ArgWhy::Scheme(_)), - "`{url}` must be refused on its scheme, got {err:?}" - ); - } -} - -/// The bare-host formats are judged as hosts rather than as URLs, so a `hostname` / `ipv4` / `ipv6` -/// field cannot smuggle a loopback or metadata target past a URL-shaped check. -#[test] -fn bare_host_formats_are_judged_as_hosts() { - let schema = json!({ - "type": "object", - "properties": { - "host": {"type": "string", "format": "hostname"}, - "idn": {"type": "string", "format": "idn-hostname"}, - "v4": {"type": "string", "format": "ipv4"}, - "v6": {"type": "string", "format": "ipv6"} - } - }); - for (field, value) in [ - ("host", "metadata.google.internal"), - ("idn", "localhost"), - ("v4", "169.254.169.254"), - ("v6", "::1"), - ] { - let args = json!({ field: value }); - assert!( - guard(&schema, &args, public()).is_err(), - "`{value}` in `{field}` must be refused" - ); - } - // The control: an ordinary public host in every one of the four fields passes, all four judged. - let ok = json!({"host": "api.example.com", "idn": "api.example.com", - "v4": "93.184.216.34", "v6": "2606:2800:220:1:248:1893:25c8:1946"}); - let scan = guard(&schema, &ok, public()).expect("public hosts pass"); - assert_eq!(scan.declared_judged, 4); -} - -/// S4: a `hostname`/`ipv4`/`ipv6`-declared field that actually carries a FULL URL — a caller smuggling -/// a scheme and path past a check that (before S4) read the whole string as an opaque host and never -/// stripped either. `judge_host` alone reads `https://169.254.169.254/x` as one weird hostname string -/// that matches none of the metadata/private/obfuscated literal comparisons, so the address sails -/// through untouched. The embedded `://` must route the value through the same absolute-URL judgement -/// (scheme allowlist + host judgement on the REAL host) the `Reference` arm already gives a -/// scheme-relative value. -#[test] -fn a_url_smuggled_into_a_bare_host_field_is_judged_on_its_real_host() { - let schema = json!({ - "type": "object", - "properties": { - "host": {"type": "string", "format": "hostname"}, - "v4": {"type": "string", "format": "ipv4"} - } - }); - for (field, value) in [ - ("host", "https://169.254.169.254/latest/meta-data/"), - ("host", "http://localhost:8080/admin"), - ("v4", "https://127.0.0.1/x"), - ] { - let args = json!({ field: value }); - let err = guard(&schema, &args, public()).expect_err(&format!( - "`{value}` in `{field}` must be refused, not read as an opaque host" - )); - assert!( - matches!(err.why, ArgWhy::CloudMetadata(_) | ArgWhy::InternalHost(_)), - "the refusal must be about the REAL host inside the URL, not a scheme/parse failure: {:?}", - err.why - ); - } - // A non-`http(s)` scheme smuggled the same way is refused too, but for the SCHEME, not the host — - // the allowlist in `judge_absolute` runs before the host is even extracted. - let scheme_err = guard( - &schema, - &json!({"host": "ftp://169.254.169.254/x"}), - public(), - ) - .expect_err("a non-http(s) scheme in a bare-host field is still refused"); - assert!(matches!(scheme_err.why, ArgWhy::Scheme(_))); - - // THE CONTROL: a public host wearing a URL wrapper still passes, so this arm does not over-block - // a legitimate value merely for carrying a scheme. - let ok = guard( - &schema, - &json!({"host": "https://api.example.com/v1", "v4": "93.184.216.34"}), - public(), - ) - .expect("a public host, wrapped in a URL or bare, still passes"); - assert_eq!(ok.declared_judged, 2); -} - -/// The NAT64/RFC 6052 form of the same smuggle: `http://[64:ff9b::a9fe:a9fe]/...` is the DNS64 -/// synthesis of the IMDS target `169.254.169.254`, and it is REFUSED. -/// -/// This test was written asserting `is_ok()` — a deliberate canary pinning the then-current -/// behavior, because `host_is_private_or_loopback`/`ssrf_blocked_host` unwrapped an embedded IPv4 -/// with `Ipv6Addr::to_ipv4()` rather than the NAT64-aware `embedded_ipv4` their sibling predicates -/// already used, so this literal was NOT caught. Its own note said to flip it to `is_err()` the day -/// someone wired the NAT64-aware unwrap into the string-based host predicates. -/// -/// That happened: the host-string guards now call `embedded_ipv4`, so a NAT64 spelling of an -/// internal target is judged exactly like the bare form. The canary did its job and is now the -/// assertion it was always waiting to become — and it matters most HERE, because this crate's -/// caller is an LLM tool-call argument, the classic injection vector. -#[test] -fn a_nat64_smuggled_host_is_refused() { - let schema = json!({ - "type": "object", - "properties": { "host": {"type": "string", "format": "hostname"} } - }); - let args = json!({"host": "http://[64:ff9b::a9fe:a9fe]/latest/meta-data/"}); - guard(&schema, &args, public()) - .expect_err("the NAT64 spelling of the IMDS target reaches the same address as the bare form and must be refused"); -} - -/// THE UNDECLARED CASE, and the decision it encodes. Most real MCP tools take a URL in a plain -/// `{"type": "string"}`, so a walk that judged only declared fields would refuse almost nothing. -/// A string whose WHOLE value is an `http(s)` URL is therefore judged wherever it appears — and -/// nothing narrower is, because refusing a hostname-shaped word in free text breaks real tools. -#[test] -fn an_http_url_in_an_undeclared_field_is_judged_and_a_hostname_shaped_word_is_not() { - let schema = json!({ - "type": "object", - "properties": { - "target": {"type": "string"}, - "query": {"type": "string"} - } - }); - let err = guard( - &schema, - &json!({"target": "http://169.254.169.254/latest/meta-data/", "query": "x"}), - public(), - ) - .expect_err("an undeclared field carrying a whole URL is still judged"); - assert_eq!(err.pointer, "/target"); - assert_eq!( - err.declared_format, None, - "the schema declared no format here, and the refusal says so" - ); - - // A field the schema does not mention AT ALL — not in `properties`, no `additionalProperties`. - let err = guard( - &schema, - &json!({"undeclared_extra": "https://localhost/admin"}), - public(), - ) - .expect_err("a field the schema never mentions is still walked"); - assert_eq!(err.pointer, "/undeclared_extra"); - assert!(matches!(err.why, ArgWhy::InternalHost(_))); - - // AND THE LIMIT, asserted: bare hostnames, mid-string URLs and non-http schemes in UNDECLARED - // fields are NOT sniffed. Each of these is a legitimate value for some real tool. - let tolerated = json!({ - "query": "localhost", - "a": "169.254.169.254", - "b": "see http://169.254.169.254/ for the format", - "c": "file:///etc/passwd", - "d": "//169.254.169.254/x" - }); - let scan = guard(&schema, &tolerated, public()) - .expect("the undeclared detector reads whole http(s) values and nothing else"); - assert_eq!(scan.undeclared_judged, 0); - assert_eq!( - scan.strings_seen, 5, - "all five were walked, none was judged" - ); -} - -/// A URL-ish format reached only through a combinator branch, through `additionalProperties`, or at -/// a tuple position is still found. Each is a shape a naive `properties`-only walk misses. -#[test] -fn combinators_additional_properties_and_tuple_positions_are_all_followed() { - let schema = json!({ - "type": "object", - "properties": { - "one": {"anyOf": [{"type": "null"}, {"type": "string", "format": "uri"}]}, - "bag": {"type": "object", "additionalProperties": {"type": "string", "format": "uri"}}, - "pair": {"type": "array", "prefixItems": [ - {"type": "string"}, - {"type": "string", "format": "uri"} - ]} - } - }); - let hostile = "http://169.254.169.254/"; - for (args, pointer) in [ - (json!({"one": hostile}), "/one"), - (json!({"bag": {"whatever": hostile}}), "/bag/whatever"), - (json!({"pair": ["label", hostile]}), "/pair/1"), - ] { - let err = guard(&schema, &args, public()) - .err() - .unwrap_or_else(|| panic!("the field at {pointer} must be reached and refused")); - assert_eq!(err.pointer, pointer); - assert_eq!(err.declared_format.as_deref(), Some("uri")); - } - // The control: the same three shapes with public URLs pass, and each judged one field. - for args in [ - json!({"one": "https://ok.example.com/"}), - json!({"bag": {"whatever": "https://ok.example.com/"}}), - json!({"pair": ["label", "https://ok.example.com/"]}), - ] { - assert_eq!( - guard(&schema, &args, public()) - .expect("public URLs pass in every shape") - .declared_judged, - 1 - ); - } -} - -/// `uri-reference` may legitimately be relative, so a relative reference is admissible — but a -/// SCHEME-RELATIVE one names a host and is judged like any other. -#[test] -fn a_uri_reference_is_relative_or_judged_but_never_waved_through() { - let schema = json!({ - "type": "object", - "properties": {"path": {"type": "string", "format": "uri-reference"}} - }); - for relative in ["/v1/items", "items/3", "?q=1", "#frag"] { - let scan = guard(&schema, &json!({ "path": relative }), public()) - .unwrap_or_else(|e| panic!("`{relative}` is a legitimate relative reference: {e}")); - assert_eq!( - scan.declared_judged, 1, - "it was judged, and it was admissible" - ); - } - for hostile in [ - "//169.254.169.254/latest/", - "http://169.254.169.254/latest/", - ] { - let err = guard(&schema, &json!({ "path": hostile }), public()) - .expect_err("a reference that names a host is judged on that host"); - assert_eq!(err.why, ArgWhy::CloudMetadata("169.254.169.254".into())); - } -} - -/// Arguments nested past the walk's depth bound are REFUSED rather than silently truncated. A walk -/// that quietly stops reports "nothing found" on the one document built to make it stop. -#[test] -fn arguments_nested_past_the_bound_are_refused_rather_than_truncated() { - let mut deep = json!("http://169.254.169.254/"); - for _ in 0..100 { - deep = json!({ "n": deep }); - } - let err = guard(&json!({"type": "object"}), &deep, public()) - .expect_err("a document too deep to walk must refuse, not pass"); - assert_eq!(err.why, ArgWhy::DepthExceeded); - // The control: a shallow document is not refused for depth. - assert!(guard(&json!({"type": "object"}), &json!({"n": "hi"}), public()).is_ok()); -} - -/// A key containing a `/` or a `~` is escaped per RFC 6901, so the pointer in a refusal names the -/// field it actually came from rather than a different one. -#[test] -fn pointer_tokens_are_escaped_so_a_key_cannot_forge_a_location() { - let schema = json!({ - "type": "object", - "properties": {"a/b": {"type": "string", "format": "uri"}} - }); - let err = guard( - &schema, - &json!({"a/b": "http://169.254.169.254/"}), - public(), - ) - .expect_err("refused"); - assert_eq!(err.pointer, "/a~1b"); -} - -/// An address the connector destination guard refuses is refused in a tool argument too: the -/// argument check used the 1.5.5 plaintext predicate, which misses these ranges. The zone forms -/// are spelled `%25lo` as a URL carries them; the host reader percent-decodes to `%lo` and -/// `host_ip` strips the zone before parsing. -#[test] -fn an_address_the_connector_guard_refuses_is_refused_in_an_argument_too() { - let urls = [ - "http://198.18.0.1/x", - "http://192.0.0.8/x", - "http://0.1.2.3/x", - "http://[::ffff:198.18.0.1]/x", - "http://[::1%25lo]/x", - "http://[fe80::1%25eth0]/x", - "http://224.0.0.1/x", - "http://255.255.255.255/x", - "http://[ff02::1]/x", - ]; - assert_eq!(urls.len(), 9); - // ARCHITECT ruling: the transition tunnels are judged by the IPv4 address they deliver to, - // in both predicates. - for v6 in ["2002:c0a8:101::", "2001:0:4136:e378:8000:63bf:f5ff:fffe"] { - let ip: core::net::Ipv6Addr = v6.parse().expect("parses"); - assert!( - busbar_contract::net::host_is_private_or_loopback(v6), - "`{v6}` must be private to the plaintext predicate too" - ); - assert!(busbar_contract::net::ipv6_is_internal(&ip), "`{v6}`"); - } - for url in urls { - let err = guard(&nested_uri_schema(), &nested_uri_args(url), public()) - .err() - .unwrap_or_else(|| panic!("`{url}` must be refused, but it passed")); - assert!( - matches!(err.why, ArgWhy::InternalHost(_)), - "`{url}` must be refused AS internal, got {err:?}" - ); - } -} - -/// The internal-range table, one representative per range, pinned against `ip_is_internal`, and -/// every internal row is refused by the argument check as an `http:///x` URL. -#[test] -fn the_internal_range_table_is_pinned() { - let internal = [ - "127.0.0.1", - "10.0.0.1", - "172.16.0.1", - "192.168.0.1", - "169.254.0.1", - "0.0.0.0", - "0.1.2.3", - "100.64.0.1", - "192.0.0.8", - "198.18.0.1", - "255.255.255.255", - "224.0.0.1", - "192.0.2.1", - "198.51.100.1", - "203.0.113.1", - "::1", - "::", - "fc00::1", - "fe80::1", - "ff02::1", - "::ffff:10.0.0.1", - "64:ff9b::a00:1", - // 6to4 embedding 192.168.1.1, and Teredo whose (inverted) client address is 10.0.0.1. - "2002:c0a8:101::", - "2001:0:4136:e378:8000:63bf:f5ff:fffe", - ]; - // A transition address is judged by the IPv4 address it delivers to: a public one is public. - let not_internal = [ - "8.8.8.8", - "2606:4700::1111", - "2002:808:808::", - "2001:0:4136:e378:8000:63bf:f7f7:f7f7", - ]; - for addr in internal { - let ip: core::net::IpAddr = addr.parse().expect("table entry parses"); - assert!( - busbar_contract::net::ip_is_internal(&ip), - "`{addr}` must be internal" - ); - let url = if ip.is_ipv6() { - format!("http://[{addr}]/x") - } else { - format!("http://{addr}/x") - }; - let err = guard(&nested_uri_schema(), &nested_uri_args(&url), public()) - .err() - .unwrap_or_else(|| panic!("`{url}` must be refused, but it passed")); - // 169.254.0.0/16 is judged as cloud metadata before the internal check runs. - assert!( - matches!(err.why, ArgWhy::InternalHost(_) | ArgWhy::CloudMetadata(_)), - "`{url}` must be refused AS internal or metadata, got {err:?}" - ); - } - for addr in not_internal { - let ip: core::net::IpAddr = addr.parse().expect("table entry parses"); - assert!( - !busbar_contract::net::ip_is_internal(&ip), - "`{addr}` must not be internal" - ); - } -} - -/// RED (ARCHITECT ruling on C3 H5): a 6to4 or Teredo address delivers to the IPv4 address it embeds, -/// so it is judged as that address. 6to4 `2002:c0a8:0101::` is 192.168.1.1; the Teredo address -/// below inverts to client 10.0.0.1. Both were admitted as public before the decode. -#[test] -fn a_transition_address_embedding_a_private_v4_is_refused() { - for url in [ - "http://[2002:c0a8:0101::]/x", - "http://[2001:0:4136:e378:8000:63bf:f5ff:fffe]/x", - ] { - let err = guard(&nested_uri_schema(), &nested_uri_args(url), public()) - .err() - .unwrap_or_else(|| panic!("`{url}` must be refused, but it passed")); - assert!( - matches!(err.why, ArgWhy::InternalHost(_)), - "`{url}`: {err:?}" - ); - } -} diff --git a/crates/busbar-plane-mcp/src/tests/arrival.rs b/crates/busbar-plane-mcp/src/tests/arrival.rs deleted file mode 100644 index 74ef1abb74..0000000000 --- a/crates/busbar-plane-mcp/src/tests/arrival.rs +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! One arrival decided as the served engine decides it. - -use serde_json::json; - -use super::*; -use crate::codec::{ - H_MCP_METHOD, H_PROTOCOL_VERSION, META_CLIENT_CAPABILITIES, META_PROTOCOL_VERSION, - PROTOCOL_VERSION, -}; - -fn meta() -> serde_json::Value { - json!({ META_PROTOCOL_VERSION: PROTOCOL_VERSION, META_CLIENT_CAPABILITIES: {} }) -} - -fn body(v: serde_json::Value) -> Vec { - serde_json::to_vec(&v).expect("a document") -} - -/// The head fields of a well-formed stateless request for `method`. -fn fields(method: &str) -> impl Fn(&str) -> Option<&'static str> + '_ { - move |n| { - if n == H_PROTOCOL_VERSION { - Some(PROTOCOL_VERSION) - } else if n == H_MCP_METHOD { - // A leaked `'static` for the test's one method name. - Some(Box::leak(method.to_string().into_boxed_str())) - } else { - None - } - } -} - -fn no_fields(_: &str) -> Option<&'static str> { - None -} - -#[test] -fn a_body_that_is_not_json_is_a_parse_error_with_a_null_id() { - match decide(b"{not json", no_fields) { - Disposition::Refused(r) => { - assert_eq!((r.status, r.code, r.id), (400, -32700, None)); - assert_eq!(r.message, NOT_JSON); - } - other => panic!("{other:?}"), - } -} - -#[test] -fn a_batch_is_refused_by_the_contracts_reader() { - match decide(b"[]", no_fields) { - Disposition::Refused(r) => assert_eq!((r.status, r.code), (400, -32600)), - other => panic!("{other:?}"), - } -} - -#[test] -fn a_well_formed_stateless_request_is_its_row() { - let b = body( - json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {"_meta": meta()}}), - ); - match decide(&b, fields("tools/list")) { - Disposition::Request { row, id } => { - assert_eq!(row.method, "tools/list"); - assert_eq!(id, json!(1)); - } - other => panic!("{other:?}"), - } -} - -/// An unknown method, and a method only an upstream sends, are `404` + `-32601` naming the method. -#[test] -fn an_uncarried_method_is_404_method_not_found() { - for method in ["tools/nope", "sampling/createMessage"] { - let b = - body(json!({"jsonrpc": "2.0", "id": 5, "method": method, "params": {"_meta": meta()}})); - match decide(&b, fields(method)) { - Disposition::Refused(r) => { - assert_eq!((r.status, r.code, r.id), (404, -32601, Some(json!(5)))); - assert_eq!( - r.message, - format!("Method `{method}` is not implemented by this server.") - ); - } - other => panic!("{method}: {other:?}"), - } - } -} - -/// The envelope checks run before the vocabulary: an unknown method with no `_meta` is `-32602`. -#[test] -fn the_envelope_checks_precede_the_vocabulary() { - let b = body(json!({"jsonrpc": "2.0", "id": 5, "method": "tools/nope", "params": {}})); - match decide(&b, fields("tools/nope")) { - Disposition::Refused(r) => assert_eq!((r.status, r.code), (400, -32602)), - other => panic!("{other:?}"), - } -} - -/// A notification is acknowledged and never answered, a cancel included. -#[test] -fn a_notification_is_acknowledged() { - let cancel = "notifications/cancelled"; - let b = body(json!({"jsonrpc": "2.0", "method": cancel, "params": {"requestId": 7}})); - assert_eq!( - decide(&b, no_fields), - Disposition::Notice { - method: cancel.to_string() - } - ); -} - -#[test] -fn a_refusal_body_echoes_its_id() { - let r = Refusal { - status: 404, - id: Some(json!(3)), - code: -32601, - message: "m".into(), - data: None, - }; - let v: serde_json::Value = serde_json::from_slice(&r.body()).expect("a document"); - assert_eq!( - v, - json!({"jsonrpc": "2.0", "id": 3, "error": {"code": -32601, "message": "m"}}) - ); -} diff --git a/crates/busbar-plane-mcp/src/tests/ask.rs b/crates/busbar-plane-mcp/src/tests/ask.rs deleted file mode 100644 index 4bbf1d9281..0000000000 --- a/crates/busbar-plane-mcp/src/tests/ask.rs +++ /dev/null @@ -1,621 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! Busbar's own ask: the decision in the engine's order, over a seal the test holds. - -use std::collections::BTreeSet; - -use serde_json::{json, Value}; - -use super::*; -use crate::tools_config::AskRoundCfg; - -/// A seal that writes the state as JSON behind a marker and spends each nonce once. -#[derive(Default)] -struct Plain { - spent: BTreeSet, - next: u32, -} - -const MARK: &str = "sealed:"; - -impl Seal for Plain { - fn mint(&mut self, state: &AskState) -> Option { - Some(format!("{MARK}{}", serde_json::to_string(state).ok()?)) - } - fn open(&mut self, blob: &str) -> Result { - let body = blob.strip_prefix(MARK).ok_or(Rejected::BadSignature)?; - serde_json::from_str(body).map_err(|_| Rejected::Malformed) - } - fn nonce(&mut self) -> Option { - self.next += 1; - Some(format!("n{}", self.next)) - } - fn redeem(&mut self, nonce: &str, _: u64, _: u64) -> bool { - self.spent.insert(nonce.to_string()) - } -} - -fn round(entries: &[(&str, &str)]) -> AskRoundCfg { - let map: serde_json::Map = entries - .iter() - .map(|(k, m)| { - ( - (*k).to_string(), - json!({ "method": m, "params": { "message": "sure?" } }), - ) - }) - .collect(); - serde_json::from_value(Value::Object(map)).expect("the operator's round reads") -} - -fn confirm() -> Vec { - vec![round(&[("ok", crate::tools_config::ASK_ELICITATION)])] -} - -fn bind(principal: &str) -> Bind<'_> { - Bind { - principal, - method: "tools/call", - capability: "fs_confirm", - generation: 1, - now: 100, - roots_epoch: 0, - } -} - -fn caps() -> Value { - json!({ "elicitation": {} }) -} - -#[test] -fn no_rounds_proceed_and_unasked_state_is_refused() { - let d = decide(&[], 3, &caps(), Retry::default(), bind("k"), "d", None); - assert_eq!(d, AskDecision::Proceed); - let answers = json!({ "ok": true }); - let retry = Retry { - responses: Some(&answers), - state: None, - }; - let AskDecision::Refuse(r) = decide(&[], 3, &caps(), retry, bind("k"), "d", None) else { - panic!("refused"); - }; - assert_eq!(r.audit_reason(), "ask_unsolicited_state"); - assert_eq!(r.refusal(&json!(1)).status, 403); -} - -#[test] -fn an_undeclared_capability_is_refused_before_the_seal_is_asked() { - let AskDecision::Refuse(r) = decide( - &confirm(), - 3, - &json!({}), - Retry::default(), - bind("k"), - "d", - None, - ) else { - panic!("refused"); - }; - let refusal = r.refusal(&json!(1)); - assert_eq!( - (refusal.status, refusal.code), - (400, crate::codec::CODE_MISSING_CLIENT_CAPABILITY) - ); - assert_eq!( - refusal.data.expect("data")["requiredCapabilities"], - json!({ "elicitation": {} }) - ); -} - -#[test] -fn with_no_seal_a_declared_ask_is_refused_unprotected() { - let AskDecision::Refuse(r) = decide( - &confirm(), - 3, - &caps(), - Retry::default(), - bind("k"), - "d", - None, - ) else { - panic!("refused"); - }; - assert_eq!(r.audit_reason(), "ask_no_sealer"); -} - -#[test] -fn the_exchange_asks_then_proceeds_once() { - let mut seal = Plain::default(); - let AskDecision::Ask { - asks, - request_state, - round, - } = decide( - &confirm(), - 3, - &caps(), - Retry::default(), - bind("k"), - "d", - Some(&mut seal), - ) - else { - panic!("asks"); - }; - assert_eq!((asks.len(), round), (1, 0)); - let body: Value = - serde_json::from_slice(&input_required_result(&json!(4), &asks, &request_state)) - .expect("json"); - assert_eq!(body["result"]["resultType"], json!("input_required")); - assert_eq!( - body["result"]["inputRequests"]["ok"]["method"], - json!("elicitation/create") - ); - let answers = json!({ "ok": true }); - let retry = Retry { - responses: Some(&answers), - state: Some(&request_state), - }; - let d = decide( - &confirm(), - 3, - &caps(), - retry, - bind("k"), - "d", - Some(&mut seal), - ); - assert_eq!(d, AskDecision::Proceed); - let AskDecision::Refuse(r) = decide( - &confirm(), - 3, - &caps(), - retry, - bind("k"), - "d", - Some(&mut seal), - ) else { - panic!("a replay is refused"); - }; - assert_eq!(r.audit_reason(), "state_already_spent"); -} - -#[test] -fn state_for_another_caller_or_request_is_refused_as_tampered() { - let mut seal = Plain::default(); - let AskDecision::Ask { request_state, .. } = decide( - &confirm(), - 3, - &caps(), - Retry::default(), - bind("k"), - "d", - Some(&mut seal), - ) else { - panic!("asks"); - }; - let answers = json!({ "ok": true }); - let retry = Retry { - responses: Some(&answers), - state: Some(&request_state), - }; - for (who, digest, reason) in [ - ("other", "d", "state_wrong_principal"), - ("k", "other", "state_wrong_request"), - ] { - let AskDecision::Refuse(r) = decide( - &confirm(), - 3, - &caps(), - retry, - bind(who), - digest, - Some(&mut seal), - ) else { - panic!("refused"); - }; - assert_eq!(r.audit_reason(), reason); - let refusal = r.refusal(&json!(1)); - assert_eq!( - (refusal.status, refusal.code), - (400, crate::codec::CODE_INVALID_PARAMS) - ); - } - let forged = Retry { - responses: Some(&answers), - state: Some("forged"), - }; - let AskDecision::Refuse(r) = decide( - &confirm(), - 3, - &caps(), - forged, - bind("k"), - "d", - Some(&mut seal), - ) else { - panic!("refused"); - }; - assert_eq!(r.audit_reason(), "state_bad_signature"); -} - -#[test] -fn a_retry_that_answers_nothing_is_refused() { - let mut seal = Plain::default(); - let two = vec![ - round(&[("ok", crate::tools_config::ASK_ELICITATION)]), - round(&[("why", crate::tools_config::ASK_ELICITATION)]), - ]; - let AskDecision::Ask { request_state, .. } = decide( - &two, - 3, - &caps(), - Retry::default(), - bind("k"), - "d", - Some(&mut seal), - ) else { - panic!("asks"); - }; - let retry = Retry { - responses: None, - state: Some(&request_state), - }; - let AskDecision::Refuse(r) = decide(&two, 3, &caps(), retry, bind("k"), "d", Some(&mut seal)) - else { - panic!("refused"); - }; - assert_eq!(r.audit_reason(), "ask_unanswered"); - let AskDecision::Refuse(r) = decide( - &two, - 1, - &caps(), - Retry { - responses: Some(&json!({ "ok": true })), - state: Some(&request_state), - }, - bind("k"), - "d", - Some(&mut seal), - ) else { - panic!("capped"); - }; - assert_eq!(r.audit_reason(), "ask_round_cap"); -} - -#[test] -fn a_roots_answer_goes_stale_when_the_caller_moves_its_roots() { - let mut seal = Plain::default(); - let roots = vec![round(&[("r", crate::tools_config::ASK_ROOTS)])]; - let caps = json!({ "roots": {} }); - let AskDecision::Ask { request_state, .. } = decide( - &roots, - 3, - &caps, - Retry::default(), - bind("k"), - "d", - Some(&mut seal), - ) else { - panic!("asks"); - }; - let answers = json!({ "r": {} }); - let retry = Retry { - responses: Some(&answers), - state: Some(&request_state), - }; - let mut moved = bind("k"); - moved.roots_epoch = 1; - let AskDecision::Refuse(r) = decide(&roots, 3, &caps, retry, moved, "d", Some(&mut seal)) - else { - panic!("refused"); - }; - assert_eq!(r.audit_reason(), "state_stale_roots"); -} - -#[test] -fn the_digest_is_the_serialisations() { - assert_eq!( - digest_arguments(&json!({ "b": 1, "a": 2 })), - busbar_contract::redacted::sha256_hex(br#"{"a":2,"b":1}"#) - ); -} - -/// THE OPERATOR-AUTHORED ASK (choke point H): the `params` a caller is shown are exactly the bytes -/// the operator wrote, deserialised from configuration and carried verbatim into the -/// `input_required` result; nothing else can make a [`CallerAsk`]. -#[test] -fn the_asks_params_are_the_operators_bytes_and_nothing_else() { - let params = json!({ - "mode": "form", - "message": "Confirm the transfer", - "requestedSchema": { "type": "object", "properties": { "ok": { "type": "boolean" } } }, - }); - let rounds: Vec = serde_json::from_value(json!([ - { "confirm": { "method": crate::tools_config::ASK_ELICITATION, "params": params.clone() } } - ])) - .expect("the operator's rounds read"); - let mut seal = Plain::default(); - let AskDecision::Ask { - asks, - request_state, - .. - } = decide( - &rounds, - 3, - &caps(), - Retry::default(), - bind("k"), - "d", - Some(&mut seal), - ) - else { - panic!("asks"); - }; - assert_eq!(asks[0].params(), ¶ms); - let body: Value = - serde_json::from_slice(&input_required_result(&json!(1), &asks, &request_state)) - .expect("json"); - assert_eq!(body["result"]["inputRequests"]["confirm"]["params"], params); -} - -/// LAW 11 (U16): AN UPSTREAM'S ASK IS RELAYED under busbar's one sealed state, which pins the member -/// and nests the upstream's own. Its retry, on a tool with or without rounds of busbar's own, is -/// the relayed retry, spent once; the state presented by another principal, on another call, or -/// forged is refused. -#[test] -fn a_relayed_asks_retry_is_pinned_spent_once_and_bound_to_its_caller() { - let mut seal = Plain::default(); - let leg = UpstreamLeg { - member: "fs".into(), - state: Some(json!("upstream-state")), - round: 1, - child: None, - }; - let state = relay_state(bind("k"), "d", leg.clone(), &mut seal).expect("sealed"); - let answers = json!({ "r": { "roots": [] } }); - let retry = Retry { - responses: Some(&answers), - state: Some(&state), - }; - // Busbar's own rounds do not stand in its way: they were answered before the call went out. - for rounds in [Vec::new(), confirm()] { - let mut fresh = Plain::default(); - assert_eq!( - decide(&rounds, 3, &caps(), retry, bind("k"), "d", Some(&mut fresh)), - AskDecision::Relayed(leg.clone()) - ); - } - // RED: spent once. - assert_eq!( - decide(&[], 3, &caps(), retry, bind("k"), "d", Some(&mut seal)), - AskDecision::Relayed(leg.clone()) - ); - assert_eq!( - decide(&[], 3, &caps(), retry, bind("k"), "d", Some(&mut seal)), - AskDecision::Refuse(AskRefusal::StateRejected(Rejected::AlreadySpent)) - ); - // RED: another principal, another call's arguments. - let mut other = Plain::default(); - assert_eq!( - decide(&[], 3, &caps(), retry, bind("other"), "d", Some(&mut other)), - AskDecision::Refuse(AskRefusal::StateRejected(Rejected::WrongPrincipal)) - ); - assert_eq!( - decide(&[], 3, &caps(), retry, bind("k"), "e", Some(&mut other)), - AskDecision::Refuse(AskRefusal::StateRejected(Rejected::WrongRequest)) - ); - // RED: a forged state is never a relayed retry. - let forged = Retry { - responses: Some(&answers), - state: Some("forged"), - }; - assert!(matches!( - decide( - &confirm(), - 3, - &caps(), - forged, - bind("k"), - "d", - Some(&mut other) - ), - AskDecision::Refuse(AskRefusal::StateRejected(_)) - )); -} - -/// The relayed result is the upstream's, `inputRequests` verbatim; only its state is busbar's. -#[test] -fn the_relayed_result_keeps_the_upstreams_requests_verbatim() { - let upstream = json!({ - "resultType": "input_required", - "inputRequests": { "s": { "method": "sampling/createMessage", "params": { "x": [1, 2] } } }, - "requestState": "theirs", - }); - let body: Value = - serde_json::from_slice(&relayed_result(&json!(7), &upstream, "ours")).expect("json"); - assert_eq!(body["id"], json!(7)); - assert_eq!(body["result"]["inputRequests"], upstream["inputRequests"]); - assert_eq!(body["result"]["requestState"], json!("ours")); -} - -/// A stdio child's two requests, relayed: the keys the caller was handed, the child's own ids. -fn child_leg() -> UpstreamLeg { - UpstreamLeg { - member: "fs".into(), - state: None, - round: 1, - child: Some(ChildLeg { - generation: 3, - wait: 77, - asks: vec![("srv-1".into(), json!("srv-1")), ("9".into(), json!(9))], - work: Some("ab".repeat(16)), - }), - } -} - -/// LAW 11, A STDIO CHILD'S OWN REQUESTS (ARCHITECT Q6): busbar writes none of them, so a retry that -/// leaves one unanswered is refused (`ask_unanswered`) BEFORE its state is spent, and the caller can -/// still answer it; the complete retry is the relayed retry, carrying the child's leg, spent once. -/// RED: a forged state, and a spent one, are refused. -#[test] -fn a_childs_relayed_ask_is_answered_whole_by_the_caller_and_spent_once() { - let mut seal = Plain::default(); - let leg = child_leg(); - let state = relay_state(bind("k"), "d", leg.clone(), &mut seal).expect("sealed"); - let partial = json!({ "srv-1": { "action": "accept" } }); - let retry = Retry { - responses: Some(&partial), - state: Some(&state), - }; - let AskDecision::Refuse(refused) = - decide(&[], 3, &caps(), retry, bind("k"), "d", Some(&mut seal)) - else { - panic!("a partial answer is refused"); - }; - assert_eq!(refused.audit_reason(), "ask_unanswered"); - assert!( - refused.to_string().contains('9'), - "names the unanswered key: {refused}" - ); - // Not spent: the caller answers the rest and the retry goes through, once. - let whole = json!({ "srv-1": { "action": "accept" }, "9": { "roots": [] } }); - let retry = Retry { - responses: Some(&whole), - state: Some(&state), - }; - assert_eq!( - decide(&[], 3, &caps(), retry, bind("k"), "d", Some(&mut seal)), - AskDecision::Relayed(leg) - ); - assert_eq!( - decide(&[], 3, &caps(), retry, bind("k"), "d", Some(&mut seal)), - AskDecision::Refuse(AskRefusal::StateRejected(Rejected::AlreadySpent)), - "a spent state is refused" - ); - let forged = Retry { - responses: Some(&whole), - state: Some("forged"), - }; - assert_eq!( - decide(&[], 3, &caps(), forged, bind("k"), "d", Some(&mut seal)), - AskDecision::Refuse(AskRefusal::Unsolicited { - capability: "fs_confirm".into() - }), - "a forged state is no relayed retry" - ); -} - -/// THE TASK PATH'S RETRY (ARCHITECT Q6): the state `tasks/update` carries back is opened, bound to -/// its principal and call, and spent once; a forged state, a spent one, another caller's, and a -/// state of busbar's own ask (no upstream leg) are refused. -#[test] -fn a_tasks_relayed_state_opens_once_for_its_caller_and_call() { - let mut seal = Plain::default(); - let leg = UpstreamLeg { - member: "fs".into(), - state: Some(json!("theirs")), - round: 1, - child: None, - }; - let state = relay_state_for(bind("k"), "d", leg.clone(), 86_400, &mut seal).expect("sealed"); - // A day later the state still stands: a task waits on a human. - let later = Bind { - now: 100 + 86_000, - ..bind("k") - }; - assert_eq!(open_relayed(&state, later, "d", &mut seal), Ok(leg)); - assert_eq!( - open_relayed(&state, bind("k"), "d", &mut seal), - Err(Rejected::AlreadySpent) - ); - let mut fresh = Plain::default(); - assert_eq!( - open_relayed(&state, bind("other"), "d", &mut fresh), - Err(Rejected::WrongPrincipal) - ); - assert_eq!( - open_relayed(&state, bind("k"), "e", &mut fresh), - Err(Rejected::WrongRequest) - ); - assert_eq!( - open_relayed("forged", bind("k"), "d", &mut fresh), - Err(Rejected::BadSignature) - ); - let own = seal - .mint(&AskState { - principal: "k".into(), - method: "tools/call".into(), - capability: "fs_confirm".into(), - args_digest: "d".into(), - generation: 1, - round: 0, - nonce: "own".into(), - issued_at: 100, - ttl_secs: 300, - roots_epoch: None, - upstream: None, - }) - .expect("sealed"); - assert_eq!( - open_relayed(&own, bind("k"), "d", &mut fresh), - Err(Rejected::WrongRequest), - "busbar's own ask is not a relayed one" - ); -} - -/// THE LAST ROUND'S ANSWER (BUSBAR-1.6.0 spec lines 379-390, the ask's gate; ask.rs's promise that a -/// caller cannot strip the operator's confirmation gate): the state minted for the final round, -/// re-sent with no `inputResponses` (or an empty map), is refused as unanswered and does not -/// proceed; the same state with its answer proceeds once. -#[test] -fn the_final_rounds_state_re_sent_without_answers_does_not_proceed() { - for answers in [None, Some(json!({}))] { - let mut seal = Plain::default(); - let AskDecision::Ask { request_state, .. } = decide( - &confirm(), - 3, - &caps(), - Retry::default(), - bind("k"), - "d", - Some(&mut seal), - ) else { - panic!("asks"); - }; - let retry = Retry { - responses: answers.as_ref(), - state: Some(&request_state), - }; - let d = decide( - &confirm(), - 3, - &caps(), - retry, - bind("k"), - "d", - Some(&mut seal), - ); - let AskDecision::Refuse(r) = d else { - panic!("the unanswered final round must refuse, got {d:?}"); - }; - assert_eq!(r.audit_reason(), "ask_unanswered"); - // The refusal spent nothing: the caller can still answer. - let answered = json!({ "ok": true }); - let retry = Retry { - responses: Some(&answered), - state: Some(&request_state), - }; - let d = decide( - &confirm(), - 3, - &caps(), - retry, - bind("k"), - "d", - Some(&mut seal), - ); - assert_eq!(d, AskDecision::Proceed); - } -} diff --git a/crates/busbar-plane-mcp/src/tests/call.rs b/crates/busbar-plane-mcp/src/tests/call.rs deleted file mode 100644 index 0bd36345ce..0000000000 --- a/crates/busbar-plane-mcp/src/tests/call.rs +++ /dev/null @@ -1,933 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! The relayed call, pure: admission in the engine's order and words, the request the client -//! dialect builds, and the far end's answer settled. - -use serde_json::{json, Value}; - -use super::*; -use crate::catalogue::{Catalogue, ToolEntry}; - -/// One server: an approved tool whose `path` argument is mirrored in `Mcp-Param-Path`, a tool -/// awaiting approval, a tool that must be answered as a task, and a tool that asks its caller. -const SECTION: &str = r#"{"fs": {"url": "https://mcp.example/fs", "pin": {"mechanism": "unpinned"}, - "grants": {"roots": true}, - "tools_allow": { - "read_file": {"schema_hash": "sha256:aa", "input_schema": {"type": "object", "properties": {"path": {"type": "string", "x-mcp-header": "Path"}}}, "output_schema": {"type": "object", "required": ["n"]}}, - "draft": {}, - "batch": {"schema_hash": "sha256:bb", "task_support": "required"}, - "confirm": {"schema_hash": "sha256:cc", "ask_caller": [{"ok": {"method": "elicitation/create", "params": {"message": "sure?"}}}]} - }}}"#; - -fn section() -> crate::tools_config::ToolsCfg { - crate::door::read_tools_section(SECTION.as_bytes()).expect("the section reads") -} - -fn catalogue() -> Catalogue { - Catalogue::build(1, §ion()) -} - -fn everyone(_: &str, _: &str) -> bool { - true -} - -fn params(name: &str, arguments: Value) -> Value { - json!({ "name": name, "arguments": arguments, "_meta": {} }) -} - -fn proceed(_: &ToolEntry, _: &Value) -> crate::ask::AskDecision { - crate::ask::AskDecision::Proceed -} - -fn no_header(_: &str) -> Option { - None -} - -fn refused(a: Admission) -> (Refusal, Option) { - match a { - Admission::Refused(r, line) => (r, line), - other => panic!("not refused: {other:?}"), - } -} - -fn go(a: Admission) -> AdmittedCall { - match a { - Admission::Go(a) => a, - other => panic!("not admitted: {other:?}"), - } -} - -#[test] -fn a_call_with_no_name_is_malformed_and_logged_with_no_tool() { - let (r, line) = refused(admit_call( - &catalogue(), - &json!(1), - Some(&json!({})), - &no_header, - &everyone, - &mut proceed, - )); - assert_eq!((r.status, r.code), (400, crate::codec::CODE_INVALID_PARAMS)); - assert_eq!(r.message, "`params.name` is required and must be a string."); - let line = line.expect("logged"); - assert_eq!( - (line.tool.as_str(), line.reason.as_str()), - ("", "malformed_params") - ); - assert_eq!( - line.audit, None, - "the served engine audited no malformed call" - ); -} - -#[test] -fn a_name_past_the_ceiling_is_refused_before_the_log_opens() { - let long = "x".repeat(MAX_TOOL_NAME_BYTES + 1); - let (r, line) = refused(admit_call( - &catalogue(), - &json!(1), - Some(¶ms(&long, json!({}))), - &no_header, - &everyone, - &mut proceed, - )); - assert_eq!(r.status, 400); - assert!(r.message.starts_with("`params.name` is 257 bytes")); - assert_eq!(line, None, "no log line holds the oversized name"); -} - -#[test] -fn an_unknown_and_an_ungranted_tool_read_the_same_and_log_apart() { - let (unknown, line) = refused(admit_call( - &catalogue(), - &json!(1), - Some(¶ms("fs_nope", json!({}))), - &no_header, - &everyone, - &mut proceed, - )); - assert_eq!(unknown.status, 404); - let line = line.expect("logged"); - assert_eq!(line.reason, "unknown_tool"); - assert_eq!(line.audit, Some(AuditRow::tool("fs_nope", false))); - let (hidden, line) = refused(admit_call( - &catalogue(), - &json!(1), - Some(¶ms("fs_read_file", json!({}))), - &no_header, - &|kind: &str, _: &str| kind != crate::door::SCOPE_TOOL, - &mut proceed, - )); - assert_eq!(hidden.status, 404); - assert_eq!( - hidden.message, - unknown.message.replace("fs_nope", "fs_read_file") - ); - assert_eq!(line.expect("logged").reason, "not_granted"); -} - -/// THE KERNEL'S VERDICT, RENDERED (ARCHITECT Q3): the door judges no trust; it renders what the -/// kernel's Approve (`trust.serves`) answered for the called tool. An item never sighted is not -/// found, as an unknown name is; a known one the operator has not approved is refused; every other -/// distrust is refused under its own reason; a server the re-fetch could not reach fails the call -/// as an upstream failure (a tool error), never sent. -#[test] -fn the_kernels_trust_verdict_is_rendered_and_never_judged_here() { - use busbar_contract::abi::host::service as svc; - let cases: [(Trust, Option<(u32, &str)>); 8] = [ - (Trust::Verdict(svc::DISTRUST_NONE), None), - ( - Trust::Verdict(svc::DISTRUST_UNKNOWN_ITEM), - Some((404, "unknown_tool")), - ), - ( - Trust::Verdict(svc::DISTRUST_NOT_APPROVED), - Some((403, "not_approved")), - ), - ( - Trust::Verdict(svc::DISTRUST_CHANGED), - Some((403, "quarantined")), - ), - ( - Trust::Verdict(svc::DISTRUST_QUARANTINED), - Some((403, "quarantined")), - ), - ( - Trust::Verdict(svc::DISTRUST_UNSIGHTED), - Some((403, "pending")), - ), - ( - Trust::Verdict(svc::DISTRUST_UNKNOWN), - Some((403, "not_serving")), - ), - ( - Trust::Unreached("down".to_string()), - Some((200, "upstream_failed")), - ), - ]; - for (trust, want) in cases { - let mut asked = Vec::new(); - let admission = admit_trusted( - &catalogue(), - &json!(1), - Some(¶ms("fs_draft", json!({}))), - &no_header, - &everyone, - &mut |entry: &ToolEntry| { - asked.push((entry.server.clone(), entry.tool.clone())); - trust.clone() - }, - &mut proceed, - ); - assert_eq!( - asked, - [("fs".to_string(), "draft".to_string())], - "the verdict is asked once, of the called tool's registration and trust key" - ); - match want { - None => { - go(admission); - } - Some((200, reason)) => match admission { - Admission::Unreached(body, line) => { - let body: Value = serde_json::from_slice(&body).expect("json"); - assert_eq!(body["result"]["isError"], json!(true), "a tool error"); - assert_eq!(line.reason, reason); - assert_eq!(line.outcome, "refused", "never sent"); - } - other => panic!("the unreached call is a tool error: {other:?}"), - }, - Some((status, reason)) => { - let (r, line) = refused(admission); - assert_eq!(r.status, status, "{trust:?}"); - assert_eq!(r.data, Some(json!({ "reason": reason })), "{trust:?}"); - assert_eq!(line.expect("logged").reason, reason, "{trust:?}"); - } - } - } -} - -/// The verdict is asked only of a tool the caller is granted: an ungranted call is not found before -/// the kernel is asked anything. -#[test] -fn the_verdict_is_asked_only_after_the_grants() { - let (r, _) = refused(admit_trusted( - &catalogue(), - &json!(1), - Some(¶ms("fs_draft", json!({}))), - &no_header, - &|_, _| false, - &mut |_| panic!("the kernel is asked about an ungranted call"), - &mut proceed, - )); - assert_eq!(r.status, 404); -} - -#[test] -fn a_mirrored_parameter_must_agree_with_the_body() { - let cat = catalogue(); - let p = params("fs_read_file", json!({ "path": "/a" })); - let (r, line) = refused(admit_call( - &cat, - &json!(1), - Some(&p), - &no_header, - &everyone, - &mut proceed, - )); - assert_eq!( - (r.status, r.code), - (400, crate::codec::CODE_HEADER_MISMATCH) - ); - assert!(r.message.contains("`Mcp-Param-Path`")); - let line = line.expect("logged"); - assert_eq!( - line.audit, None, - "a header mismatch is malformed, never audited" - ); - assert_eq!( - ( - line.server.as_str(), - line.tool.as_str(), - line.tool_digest.as_str() - ), - ("fs", "fs_read_file", "sha256:aa") - ); - let wrong = |name: &str| (name == "mcp-param-path").then(|| "/b".to_string()); - let (r, _) = refused(admit_call( - &cat, - &json!(1), - Some(&p), - &wrong, - &everyone, - &mut proceed, - )); - assert!(r - .message - .ends_with("does not match the body's `arguments.path`.")); - let right = |name: &str| (name == "mcp-param-path").then(|| "/a".to_string()); - let a = go(admit_call( - &cat, - &json!(1), - Some(&p), - &right, - &everyone, - &mut proceed, - )); - assert_eq!(a.arguments, json!({ "path": "/a" })); - assert_eq!(a.progress_token, None); -} - -#[test] -fn a_required_task_needs_the_extension() { - let (r, line) = refused(admit_call( - &catalogue(), - &json!(1), - Some(¶ms("fs_batch", json!({}))), - &no_header, - &everyone, - &mut proceed, - )); - assert_eq!( - (r.status, r.code), - (400, crate::codec::CODE_MISSING_CLIENT_CAPABILITY) - ); - let line = line.expect("logged"); - assert_eq!(line.reason, REASON_TASKS_UNDECLARED); - assert_eq!(line.audit, Some(AuditRow::tool("fs_batch", false))); - let declared = json!({ "name": "fs_batch", "arguments": {}, - "_meta": { crate::codec::META_CLIENT_CAPABILITIES: { "extensions": { TASKS_EXTENSION_ID: {} } } } }); - go(admit_call( - &catalogue(), - &json!(1), - Some(&declared), - &no_header, - &everyone, - &mut proceed, - )); -} - -#[test] -fn an_answer_binds_only_what_was_asked_and_never_a_sent_argument() { - let cat = catalogue(); - let answered = json!({ "name": "fs_confirm", "arguments": {}, "inputResponses": { "ok": true }, "_meta": {} }); - let a = go(admit_call( - &cat, - &json!(1), - Some(&answered), - &no_header, - &everyone, - &mut proceed, - )); - assert_eq!(a.arguments, json!({ "ok": true })); - let smuggled = json!({ "name": "fs_confirm", "arguments": {}, "inputResponses": { "amount": 1 }, "_meta": {} }); - let (r, line) = refused(admit_call( - &cat, - &json!(1), - Some(&smuggled), - &no_header, - &everyone, - &mut proceed, - )); - assert_eq!(r.status, 404); - assert!(r.message.starts_with("the answer named `amount`")); - assert_eq!(line.expect("logged").reason, REASON_ANSWER_UNDECLARED); - let rewritten = json!({ "name": "fs_confirm", "arguments": { "ok": false }, "inputResponses": { "ok": true }, "_meta": {} }); - refused(admit_call( - &cat, - &json!(1), - Some(&rewritten), - &no_header, - &everyone, - &mut proceed, - )); -} - -/// `fs_read_file` admitted with `arguments`, then judged by `judge` ([`judge_arguments`]). -fn judged(arguments: Value, judge: &mut dyn FnMut(&ToolEntry, &str) -> Option) -> Admission { - let p = params("fs_read_file", arguments); - let header = |name: &str| (name == "mcp-param-path").then(|| "/a".to_string()); - let admission = admit_call( - &catalogue(), - &json!(7), - Some(&p), - &header, - &everyone, - &mut proceed, - ); - judge_arguments(admission, judge) -} - -/// THE ARGUMENT GUARD ASKS THE HOST (BUSBAR-1.6.0.md Appendix C B.3 item 11, `dest.judge`): every -/// host the arguments name is put to the judge for the called tool, an IPv6 literal bracketed as -/// `dest.judge` reads it, and the host's verdict alone decides. A host the plane holds no rule -/// about is refused when the judge refuses it, in the guard's bytes; one the judge admits is sent. -#[test] -fn the_hosts_verdict_decides_an_argument_url() { - let arguments = json!({ "path": "/a", "fetch": "http://public.example/x", "peer": "https://[2001:db8::1]:8443/" }); - let mut asked = Vec::new(); - let a = go(judged(arguments.clone(), &mut |entry, dest| { - asked.push((entry.tool.clone(), dest.to_string())); - Some(busbar_contract::abi::host::service::DEST_ALLOWED) - })); - assert_eq!( - a.arguments, arguments, - "admitted, the arguments are sent as they came" - ); - asked.sort(); - assert_eq!( - asked, - [ - ("read_file".to_string(), "[2001:db8::1]".to_string()), - ("read_file".to_string(), "public.example".to_string()), - ], - "each host is asked of the host, once, for the called tool" - ); - - let (r, line) = refused(judged(arguments, &mut |_, dest| { - Some(if dest == "public.example" { - busbar_contract::abi::host::service::DEST_METADATA - } else { - busbar_contract::abi::host::service::DEST_ALLOWED - }) - })); - assert_eq!((r.status, r.code), (403, crate::codec::CODE_REFUSED)); - assert_eq!(r.data, Some(json!({ "reason": "tool_argument_refused" }))); - assert!( - r.message.starts_with("tool argument /fetch carries a URL"), - "{}", - r.message - ); - let line = line.expect("logged"); - assert_eq!( - (line.outcome, line.reason.as_str()), - ("refused", "tool_argument_refused") - ); -} - -/// A host that gives no verdict (it serves no `dest.judge`, or the judgement failed) refuses the -/// value rather than admitting it unjudged; arguments naming no host never ask it. -#[test] -fn a_host_that_gives_no_verdict_refuses_the_argument() { - let (r, _) = refused(judged( - json!({ "path": "/a", "fetch": "https://public.example/" }), - &mut |_, _| None, - )); - assert_eq!(r.status, 403); - assert!(r.message.contains("could not be checked"), "{}", r.message); - go(judged(json!({ "path": "/a" }), &mut |_, dest| { - panic!("no host is named, yet `{dest}` was asked") - })); -} - -fn admitted() -> AdmittedCall { - let right = |name: &str| (name == "mcp-param-path").then(|| "/a".to_string()); - go(admit_call( - &catalogue(), - &json!(7), - Some(¶ms("fs_read_file", json!({ "path": "/a" }))), - &right, - &everyone, - &mut proceed, - )) -} - -#[test] -fn the_request_is_the_dialect_builders_at_the_members_path() { - let section = section(); - let def = section.servers.get("fs").expect("registered"); - let o = outbound(&admitted(), "fs", def, 0, None).expect("reachable"); - assert_eq!((o.verb, o.target.as_str()), ("POST", "/fs")); - let body: Value = serde_json::from_slice(&o.body).expect("json"); - assert_eq!( - body["params"]["name"], - json!("read_file"), - "the upstream's own name" - ); - assert_eq!(body["id"], json!(0)); - // A RELAYED ASK NEEDS A CALLER WHO CAN ANSWER IT (Law 11): the roots grant alone advertises - // nothing to a caller that declared no roots; with the caller's roots declared it is. - assert_eq!( - body["params"]["_meta"][crate::codec::META_CLIENT_CAPABILITIES], - json!({}), - "nothing is advertised the caller could not answer" - ); - let mut declaring = admitted(); - declaring.capabilities = json!({ "roots": {}, "sampling": {} }); - let o = outbound(&declaring, "fs", def, 0, None).expect("reachable"); - let body: Value = serde_json::from_slice(&o.body).expect("json"); - assert_eq!( - body["params"]["_meta"][crate::codec::META_CLIENT_CAPABILITIES], - json!({ "roots": {} }), - "the roots grant and the caller's roots advertise roots; an ungranted kind is not" - ); - assert!( - o.fields.iter().all(|(n, _)| n != "authorization"), - "no credential here" - ); - assert!(o - .fields - .contains(&("mcp-name".to_string(), "read_file".to_string()))); -} - -#[test] -fn the_target_is_the_urls_path_and_query() { - assert_eq!(path_of("https://h.example"), "/"); - assert_eq!(path_of("https://h.example/a/b"), "/a/b"); - assert_eq!(path_of("https://h.example/a?x=1"), "/a?x=1"); - assert_eq!(path_of("https://h.example?x=1"), "/?x=1"); -} - -#[test] -fn the_last_event_is_the_answer() { - let raw = b"event: message\ndata: {\"a\":1}\n\ndata: {\"b\":\ndata: 2}\n\n"; - assert_eq!(last_sse_data(raw), b"{\"b\":\n2}".to_vec()); - assert_eq!(last_sse_data(b"nothing"), Vec::::new()); -} - -fn answer_of(s: Settled) -> (u32, Value, CallLine) { - match s { - Settled::Answer { status, body, line } => { - (status, serde_json::from_slice(&body).expect("json"), line) - } - other => panic!("an answer: {other:?}"), - } -} - -fn settle_far(far: &str) -> Settled { - let section = section(); - settle_call( - &admitted(), - section.servers.get("fs"), - 200, - far.as_bytes(), - false, - 0, - ) -} - -/// A result with no `resultType` gets the dialect's `complete` ahead of the upstream's own members, -/// which follow as they came; an empty result is the stamp alone. -#[test] -fn a_result_is_stamped_and_dispatched() { - let settled = settle_far( - r#"{"jsonrpc":"2.0","id":0,"result":{"content":[],"structuredContent":{"n":1}}}"#, - ); - let Settled::Answer { body: bytes, .. } = &settled else { - panic!("an answer: {settled:?}") - }; - assert_eq!( - String::from_utf8_lossy(bytes), - r#"{"id":7,"jsonrpc":"2.0","result":{"resultType":"complete","content":[],"structuredContent":{"n":1}}}"# - ); - let (status, body, line) = answer_of(settled); - assert_eq!(status, 200); - assert_eq!(body["id"], json!(7)); - assert_eq!(body["result"]["resultType"], json!("complete")); - assert_eq!((line.outcome, line.reason.as_str()), ("dispatched", "")); - assert_eq!(line.audit, Some(AuditRow::tool("fs_read_file", true))); - let (_, empty, _) = answer_of(settle_far(r#"{"jsonrpc":"2.0","id":0,"result":{ }}"#)); - assert_eq!(empty["result"], json!({ "resultType": "complete" })); -} - -/// The far end's answer carrying `result` as the upstream wrote it. -fn far_result(result: &str) -> String { - format!(r#"{{"jsonrpc":"2.0","id":0,"result":{result}}}"#) -} - -/// LAW 11 (THE DESIGN 2126, 2131-2132; product hard rule 3369-3373): a tool's result is the -/// upstream's data and reaches the caller as the upstream sent it. `Vec` and `x` in -/// the content text, in `structuredContent` and in `_meta` are the tool's answer, not markup busbar -/// may strip, and the result's bytes are the upstream's own, member order included. -/// RED arm: a built-in strip served `Vec` for `Vec` and `x` for `x`. -#[test] -fn a_result_reaches_its_caller_as_the_upstream_sent_it() { - let sent = r#"{"resultType":"complete","structuredContent":{"t":"Vec","n":1,"h":"x"},"content":[{"type":"text","text":"fn f() -> Vec { x }"}],"_meta":{"m":"y"}}"#; - let settled = settle_far(&far_result(sent)); - let Settled::Answer { body: bytes, .. } = &settled else { - panic!("an answer: {settled:?}") - }; - assert!( - bytes.windows(sent.len()).any(|w| w == sent.as_bytes()), - "the upstream's result bytes, verbatim: {}", - String::from_utf8_lossy(bytes) - ); - let (status, body, line) = answer_of(settled); - assert_eq!(status, 200); - assert_eq!(body["id"], json!(7)); - assert_eq!( - body["result"], - serde_json::from_str::(sent).expect("json"), - "the upstream's result, unchanged" - ); - assert_eq!((line.outcome, line.reason.as_str()), ("dispatched", "")); -} - -/// LAW 11 (THE DESIGN 2131-2132): a `structuredContent` that does not match the tool's published -/// `outputSchema` is still the upstream's answer. It is relayed unchanged; busbar never answers in -/// the upstream's place. -/// RED arm: busbar replaced it with its own `isError` result, "The structured result was NOT -/// served". -#[test] -fn structured_output_breaking_the_published_schema_is_relayed_unchanged() { - let sent = r#"{"resultType":"complete","content":[],"structuredContent":{}}"#; - let (status, body, line) = answer_of(settle_far(&far_result(sent))); - assert_eq!(status, 200); - assert_eq!( - body["result"], - serde_json::from_str::(sent).expect("json"), - "the upstream's result, as it came" - ); - assert_eq!((line.outcome, line.reason.as_str()), ("dispatched", "")); - assert_eq!(line.audit, Some(AuditRow::tool("fs_read_file", true))); -} - -/// THE TASK PATH SETTLES THE SAME RESULT (Law 11): a task's completed result is the upstream's, -/// stored and served as it came. A source plant, because the task's continuation is reached only -/// through a live door: no rewrite of the result is named on that path. -/// RED arm: `door_tasks.rs` ran the markup strip over every completed task result. -#[test] -fn the_task_path_names_no_rewrite_of_the_result() { - let source = include_str!("../door_tasks.rs"); - assert!( - !source.contains("sanitize::"), - "a task's completed result passes through no markup strip" - ); -} - -#[test] -fn an_upstream_error_is_a_tool_failure_naming_the_server() { - let (status, body, line) = answer_of(settle_far( - r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32601,"message":"nope"}}"#, - )); - assert_eq!(status, 200); - let text = body["result"]["content"][0]["text"].as_str().expect("text"); - assert_eq!( - text, - "The MCP server `fs` did not complete this tool call: MCP upstream answered JSON-RPC error -32601: nope" - ); - assert_eq!( - (line.outcome, line.reason.as_str()), - ("dispatched", "upstream_failed") - ); - assert_eq!( - line.audit, - Some(AuditRow::tool("fs_read_file", false)), - "the call went out and did not succeed: rejected" - ); -} - -/// LAW 11 (THE DESIGN 2126, 2131-2132; product hard rule 3369-3373): the upstream's JSON-RPC -/// error message is the upstream's data and reaches the caller unchanged inside busbar's failure -/// words; `Vec` and `x` are not markup busbar may strip. -/// RED arm: the failure text ran through the markup strip, serving `Vec` and `x`. -#[test] -fn an_upstream_error_message_reaches_its_caller_unchanged() { - let (_, body, _) = answer_of(settle_far( - r#"{"jsonrpc":"2.0","id":0,"error":{"code":-32602,"message":"want Vec, got x"}}"#, - )); - let text = body["result"]["content"][0]["text"].as_str().expect("text"); - assert_eq!( - text, - "The MCP server `fs` did not complete this tool call: MCP upstream answered JSON-RPC error -32602: want Vec, got x" - ); -} - -/// P-ITEM: EMPTY REPLY / UNARY-EMPTY TERMINALITY (spec DONE item 2, "All P-item behaviours match -/// 1.5.5"; the drive log's P4, commit 470351a480, which the money briefs name "mcp bills empty -/// answer as complete"). An envelope with a real result is the upstream's complete answer; one -/// with neither `result` nor `error` is never settled as one. -/// -/// This is a money boundary. A JSON-RPC answer carries exactly one of `result` or `error`; a -/// document with NEITHER used to fall through to a complete answer and charge the caller for a -/// result that never came. On the served door it is the upstream's failure: an `isError` result -/// naming the server, logged `upstream_failed` and audited rejected, never the `dispatched` line -/// of a delivered result. The 1.5.5 behaviour this plane matches is its one surface's (the llm -/// surface; owner correction 2026-09-28): a response the caller cannot use is not billed as a -/// delivered one (v1.5.5 `crates/busbar/src/proxy/response_body.rs:415-440`). -#[test] -fn p_item_empty_reply_only_a_real_result_settles_complete() { - let (status, body, line) = answer_of(settle_far(r#"{"jsonrpc":"2.0","id":0,"result":{}}"#)); - assert_eq!(status, 200); - assert_eq!(body["result"], json!({ "resultType": "complete" })); - assert_eq!((line.outcome, line.reason.as_str()), ("dispatched", "")); - - let (status, body, line) = answer_of(settle_far(r#"{"jsonrpc":"2.0","id":0}"#)); - assert_eq!(status, 200); - assert_eq!(body["id"], json!(7)); - assert_eq!(body["result"]["isError"], json!(true)); - let text = body["result"]["content"][0]["text"].as_str().expect("text"); - assert!( - text.starts_with("The MCP server `fs` did not complete this tool call:") - && text.contains("neither `result` nor `error`"), - "{text}" - ); - assert_eq!( - (line.outcome, line.reason.as_str()), - ("dispatched", "upstream_failed"), - "an empty envelope is the upstream's failure, never a delivered result" - ); - assert_eq!(line.audit, Some(AuditRow::tool("fs_read_file", false))); -} - -#[test] -fn an_answer_to_something_else_is_never_served() { - let (_, body, _) = answer_of(settle_far(r#"{"jsonrpc":"2.0","id":5,"result":{}}"#)); - assert!(body["result"]["content"][0]["text"] - .as_str() - .is_some_and(|t| t.contains("cannot correlate"))); -} - -#[test] -fn an_ungranted_ask_is_refused_and_never_forwarded() { - let (status, body, line) = answer_of(settle_far( - r#"{"jsonrpc":"2.0","id":0,"result":{"resultType":"input_required","inputRequests":{"e":{"method":"elicitation/create"}}}}"#, - )); - assert_eq!(status, 403); - assert_eq!(body["error"]["data"]["reason"], json!("ask_ungranted")); - assert!(body.get("result").is_none()); - assert_eq!(line.outcome, "refused"); -} - -/// LAW 11 (U16): a granted roots ask is RELAYED to the caller, its result as it came; busbar -/// answers none of it. RED arm: a satisfier answering from the operator's roots would settle a -/// next round (an answer busbar wrote) instead of handing the upstream's result back. -#[test] -fn a_granted_roots_ask_is_relayed_to_the_caller_as_it_came() { - let far = r#"{"jsonrpc":"2.0","id":0,"result":{"resultType":"input_required","inputRequests":{"r":{"method":"roots/list"}},"requestState":"s"}}"#; - match settle_far(far) { - Settled::Relay { - result, - round, - child, - } => { - assert_eq!(round, 0); - assert_eq!( - child, None, - "an `input_required` result is not a child's request" - ); - let sent: Value = serde_json::from_str(far).expect("json"); - assert_eq!( - result, sent["result"], - "the upstream's result, byte for byte" - ); - } - other => panic!("relayed: {other:?}"), - } -} - -/// LAW 11 (BUSBAR-1.6.0 lines 2127-2132, the relay-only ruling at 418-422): a granted SAMPLING ask -/// is relayed to the caller exactly as the upstream sent it. busbar runs no completion for it on any -/// plane; the caller answers it. A pin: predev already relays on this path, and this holds it there. -#[test] -fn a_granted_sampling_ask_is_relayed_to_the_caller_unchanged() { - let section = section(); - let mut def = section.servers.get("fs").expect("fs").clone(); - def.grants.sampling = true; - let far = r#"{"jsonrpc":"2.0","id":0,"result":{"resultType":"input_required","inputRequests":{"s":{"method":"sampling/createMessage","params":{"messages":[{"role":"user","content":{"type":"text","text":"hi"}}],"maxTokens":5}}},"requestState":"opaque"}}"#; - match settle_call(&admitted(), Some(&def), 200, far.as_bytes(), false, 0) { - Settled::Relay { - result, - round, - child, - } => { - assert_eq!(round, 0); - assert_eq!(child, None); - let sent: Value = serde_json::from_str(far).expect("json"); - assert_eq!( - result, sent["result"], - "the upstream's sampling ask, byte for byte" - ); - } - other => panic!("relayed: {other:?}"), - } -} - -/// LAW 11 (finding 20): what the caller reads about an upstream's ask says what the code does — the -/// ask is RELAYED to the caller, or not relayed under the operator's policy. It never says busbar -/// satisfies an ask, or that an ask terminates at busbar. -#[test] -fn an_ask_refusal_tells_the_caller_the_ask_is_relayed_never_satisfied() { - let ungranted = settle_far( - r#"{"jsonrpc":"2.0","id":0,"result":{"resultType":"input_required","inputRequests":{"s":{"method":"sampling/createMessage"}}}}"#, - ); - let mixed = - settle_far(r#"{"jsonrpc":"2.0","id":0,"result":{"content":[],"requestState":"x"}}"#); - for settled in [ungranted, mixed] { - let (status, body, _) = answer_of(settled); - assert_eq!(status, 403); - let message = body["error"]["message"].as_str().expect("message"); - for stale in ["satisf", "terminates at busbar", "never forwarded"] { - assert!( - !message.contains(stale), - "the caller-visible text says `{stale}`: {message}" - ); - } - assert!( - message.contains("relayed"), - "the caller-visible text names the relay: {message}" - ); - } -} - -/// An ask mixing a granted kind with an ungranted one is refused naming the ungranted one: the -/// most privileged kind alone does not decide a map whose lesser entries are ungranted. -#[test] -fn an_ask_mixing_an_ungranted_kind_is_refused() { - let (status, body, _) = answer_of(settle_far( - r#"{"jsonrpc":"2.0","id":0,"result":{"resultType":"input_required","inputRequests":{"r":{"method":"roots/list"},"e":{"method":"elicitation/create"}}}}"#, - )); - assert_eq!(status, 403); - assert_eq!(body["error"]["data"]["reason"], json!("ask_ungranted")); - assert!(body["error"]["message"] - .as_str() - .is_some_and(|m| m.contains("elicitation"))); -} - -#[test] -fn the_round_cap_bites_before_the_grant_is_read() { - let section = section(); - let far = br#"{"jsonrpc":"2.0","id":3,"result":{"resultType":"input_required","inputRequests":{"r":{"method":"roots/list"}}}}"#; - let (status, body, _) = answer_of(settle_call( - &admitted(), - section.servers.get("fs"), - 200, - far, - false, - 3, - )); - assert_eq!(status, 403); - assert_eq!(body["error"]["data"]["reason"], json!("ask_round_cap")); -} - -#[test] -fn a_result_still_carrying_an_ask_is_refused_at_the_terminal_check() { - assert_eq!( - upstream_ask_field(&json!({ "requestState": "x" })), - Some("requestState") - ); - assert_eq!(upstream_ask_field(&json!({ "content": [] })), None); -} - -#[test] -fn a_tasks_verb_is_gated_then_names_no_task_of_this_callers() { - let undeclared = task_verb(&json!(1), Some(&json!({ "taskId": "t", "_meta": {} }))); - assert_eq!( - undeclared.code, - crate::codec::CODE_MISSING_CLIENT_CAPABILITY - ); - let meta = json!({ crate::codec::META_CLIENT_CAPABILITIES: { "extensions": { TASKS_EXTENSION_ID: {} } } }); - let no_id = task_verb(&json!(1), Some(&json!({ "_meta": meta }))); - assert_eq!( - (no_id.status, no_id.code), - (400, crate::codec::CODE_INVALID_PARAMS) - ); - assert_eq!( - no_id.message, - "`params.taskId` is required and must be a string." - ); - let unknown = task_verb(&json!(1), Some(&json!({ "taskId": "t", "_meta": meta }))); - assert_eq!( - unknown.message, - "No task with that `taskId` exists for this caller." - ); -} - -/// THE INVOKE REWRITE CONTRACT, as the served engine applied a `prompt: rw` hook's reply to a -/// call's arguments: an object `content`, a JSON-string `content` that parses to an object, or a -/// role-less entry itself; the last usable entry wins; nothing usable is not applied. -#[test] -fn a_rewrite_replaces_the_arguments_by_the_invoke_contract() { - let body = br#"{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"fs_read","arguments":{"path":"/secret"},"_meta":{"k":"v"}}}"#; - let args = |rewrite: serde_json::Value| { - rewritten(body, &serde_json::to_vec(&rewrite).unwrap()).map(|b| { - let v: serde_json::Value = serde_json::from_slice(&b).unwrap(); - assert_eq!(v["params"]["_meta"], serde_json::json!({ "k": "v" })); - assert_eq!(v["id"], 7); - v["params"]["arguments"].clone() - }) - }; - let object = serde_json::json!({ "path": "/x" }); - assert_eq!( - args(serde_json::json!({ "messages": [{ "role": "user", "content": object }] })), - Some(object.clone()) - ); - assert_eq!( - args( - serde_json::json!({ "messages": [{ "role": "user", "content": "{\"path\":\"/x\"}" }] }) - ), - Some(object.clone()) - ); - assert_eq!( - args(serde_json::json!({ "messages": [{ "path": "/x" }] })), - Some(object.clone()) - ); - assert_eq!( - args(serde_json::json!({ "messages": [ - { "role": "user", "content": { "path": "/first" } }, - { "role": "user", "content": "plain words" }, - { "role": "user", "content": { "path": "/x" } }, - ] })), - Some(object) - ); - // Nothing usable: a role with no content, plain text, a scalar, no messages at all. - assert_eq!( - args(serde_json::json!({ "messages": [{ "role": "user" }] })), - None - ); - assert_eq!( - args(serde_json::json!({ "messages": [{ "role": "user", "content": "7" }] })), - None - ); - assert_eq!(args(serde_json::json!({ "tools": [] })), None); - assert_eq!(rewritten(body, b"not json"), None); - // A request that is not a call is never rewritten. - let list = br#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#; - assert_eq!( - rewritten( - list, - br#"{"messages":[{"role":"user","content":{"path":"/x"}}]}"# - ), - None - ); -} - -/// THE AUDIT ROW OF BUSBAR'S OWN ASK (SEAM-L(k)): an ask of the caller is `mcp.caller_ask` applied on -/// the tool; a task created is the call applied; a task that could not start is rejected. -#[test] -fn an_ask_of_the_caller_and_a_task_are_audited_in_the_engines_words() { - let mut ask = |_: &ToolEntry, _: &Value| crate::ask::AskDecision::Ask { - asks: Vec::new(), - request_state: "s".to_string(), - round: 1, - }; - let line = match admit_call( - &catalogue(), - &json!(1), - Some(¶ms("fs_confirm", json!({}))), - &no_header, - &everyone, - &mut ask, - ) { - Admission::Asked(_, line) => line, - other => panic!("asked: {other:?}"), - }; - assert_eq!( - line.audit, - Some(AuditRow { - action: ACTION_CALLER_ASK, - resource: "mcp_tool:fs_confirm".to_string(), - applied: true, - }) - ); - let entry = catalogue().tool("fs_batch").expect("registered").clone(); - assert_eq!( - task_line(&entry, busbar_contract::vocab::REASON_TASK_CREATED).audit, - Some(AuditRow::tool("fs_batch", true)) - ); - assert_eq!( - task_line(&entry, "task_unavailable").audit, - Some(AuditRow::tool("fs_batch", false)) - ); -} diff --git a/crates/busbar-plane-mcp/src/tests/catalogue.rs b/crates/busbar-plane-mcp/src/tests/catalogue.rs deleted file mode 100644 index 02098abfea..0000000000 --- a/crates/busbar-plane-mcp/src/tests/catalogue.rs +++ /dev/null @@ -1,208 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! The catalogue answers as the served engine does: both grants for every capability, the -//! published name, the normalised description, the caching hints, and an approval visible from the -//! generation that carries it and no earlier one. - -use serde_json::{json, Value}; - -use super::*; - -/// Two servers: `fs` with an approved tool, a pending tool, a renamed tool, a prompt, a resource -/// and a template; `db` with one approved tool. -const SECTION: &[u8] = br#"{ - "fs": { - "url": "https://mcp.example/fs", - "pin": {"mechanism": "unpinned"}, - "tools_allow": { - "read_file": {"schema_hash": "sha256:aa", "description": "Reads a file."}, - "write_file": {}, - "stat": {"schema_hash": " ", "publish_as": "file_stat"} - }, - "prompts_allow": {"summarise": {"description": "Summarise."}}, - "resources_allow": {"file:///readme": {"name": "readme", "mime_type": "text/plain", "text": "hi"}}, - "resource_templates_allow": {"file:///logs/{date}": {"name": "log", "text": "log {date}"}} - }, - "db": { - "url": "https://mcp.example/db", - "pin": {"mechanism": "unpinned"}, - "tools_allow": {"query": {"schema_hash": "sha256:bb"}} - } -}"#; - -fn catalogue(generation: u64, section: &[u8]) -> Catalogue { - Catalogue::build( - generation, - &crate::door::read_tools_section(section).expect("the section reads"), - ) -} - -/// A caller holding exactly `grants`, each `kind:name`. -fn holding(grants: &'static [&'static str]) -> impl Fn(&str, &str) -> bool { - move |kind, name| grants.contains(&format!("{kind}:{name}").as_str()) -} - -fn everyone(_: &str, _: &str) -> bool { - true -} - -fn read(bytes: Vec) -> Value { - serde_json::from_slice(&bytes).expect("a document") -} - -fn names(listing: &Value, member: &str, key: &str) -> Vec { - listing["result"][member] - .as_array() - .expect("a list") - .iter() - .map(|e| e[key].as_str().expect("a string").to_string()) - .collect() -} - -/// Every capability is listed under its published name, in published-name order, pending ones -/// included, and each is rendered as the served engine renders it. -#[test] -fn the_whole_catalogue_is_listed_under_published_names_pending_included() { - let c = catalogue(1, SECTION); - let tools = read(c.tools_list(&json!(1), &everyone, |_| false)); - assert_eq!( - names(&tools, "tools", "name"), - ["db_query", "file_stat", "fs_read_file", "fs_write_file"] - ); - let read_file = &tools["result"]["tools"][2]; - assert_eq!(read_file["description"], "Reads a file."); - assert_eq!(read_file["inputSchema"], json!({"type": "object"})); - assert_eq!( - read_file["_meta"], - json!({"io.busbar/schemaHash": "sha256:aa"}) - ); - // A pending tool, and a blank hash, publish no hash. - assert!(tools["result"]["tools"][3].get("_meta").is_none()); - assert!(tools["result"]["tools"][1].get("_meta").is_none()); - assert_eq!(c.tool("file_stat").map(|t| t.tool.as_str()), Some("stat")); -} - -/// Every listing is a complete, cacheable result: the id echoed, `resultType`, and both hints. -#[test] -fn every_listing_is_a_complete_cacheable_result() { - let c = catalogue(1, SECTION); - for listing in [ - c.tools_list(&json!("a"), &everyone, |_| false), - c.prompts_list(&json!("a"), &everyone), - c.resources_list(&json!("a"), &everyone), - c.resource_templates_list(&json!("a"), &everyone), - ] { - let v = read(listing); - assert_eq!(v["jsonrpc"], "2.0"); - assert_eq!(v["id"], "a"); - assert_eq!(v["result"]["resultType"], "complete"); - assert_eq!(v["result"]["cacheScope"], CACHE_SCOPE); - assert_eq!(v["result"]["ttlMs"], CACHE_TTL_MS); - } -} - -/// Two grants see two different catalogues and a third sees none. -#[test] -fn two_grants_see_two_catalogues_and_a_third_sees_none() { - let c = catalogue(1, SECTION); - let fs = holding(&["mcp_server:fs", "mcp_tool:fs_read_file"]); - let db = holding(&["mcp_server:db", "mcp_tool:db_query"]); - let none = holding(&[]); - let list = |admit: &dyn Fn(&str, &str) -> bool| { - names( - &read(c.tools_list(&json!(1), &|k, n| admit(k, n), |_| false)), - "tools", - "name", - ) - }; - assert_eq!(list(&fs), ["fs_read_file"]); - assert_eq!(list(&db), ["db_query"]); - assert!(list(&none).is_empty()); -} - -/// The server grant alone reaches nothing, and the capability grant alone reaches nothing: both -/// are asked for every kind of capability. -#[test] -fn the_server_grant_and_the_capability_grant_are_both_required() { - let c = catalogue(1, SECTION); - let halves: [&'static [&'static str]; 2] = [ - &["mcp_server:fs"], - &["mcp_tool:fs_read_file", "mcp_tool:fs_summarise"], - ]; - for half in halves { - let admit = holding(half); - assert!(c.tools_for(&admit).is_empty()); - assert!(c.prompts_for(&admit).is_empty()); - assert!(c.resources_for(&admit).is_empty()); - assert!(c.resource_templates_for(&admit).is_empty()); - } - let prompt = holding(&["mcp_server:fs", "mcp_tool:fs_summarise"]); - assert_eq!(c.prompts_for(&prompt).len(), 1); - // The grant value of a resource is its namespaced uri; the wire carries the raw one. - let resource = holding(&["mcp_server:fs", "mcp_tool:fs_file:///readme"]); - let listed = read(c.resources_list(&json!(1), &resource)); - assert_eq!(names(&listed, "resources", "uri"), ["file:///readme"]); - assert_eq!(listed["result"]["resources"][0]["mimeType"], "text/plain"); -} - -/// A caller reaching no template gets the empty list, never a refusal. -#[test] -fn a_caller_reaching_no_template_gets_an_empty_list() { - let c = catalogue(1, SECTION); - let v = read(c.resource_templates_list(&json!(1), &holding(&[]))); - assert_eq!(v["result"]["resourceTemplates"], json!([])); - let all = read(c.resource_templates_list(&json!(1), &everyone)); - assert_eq!( - names(&all, "resourceTemplates", "uriTemplate"), - ["file:///logs/{date}"] - ); -} - -/// A quarantined tool is hidden; the verdict is asked only of what the grants admitted. -#[test] -fn a_quarantined_tool_is_hidden_and_only_granted_tools_are_asked() { - let c = catalogue(1, SECTION); - let fs = holding(&["mcp_server:fs", "mcp_tool:fs_read_file"]); - let v = read(c.tools_list(&json!(1), &fs, |t| { - assert_eq!( - t.namespaced, "fs_read_file", - "an ungranted tool is never asked" - ); - true - })); - assert_eq!(v["result"]["tools"], json!([])); -} - -/// An APPROVAL IS VISIBLE FROM THE GENERATION THAT CARRIES IT, AND NOT BEFORE: the hash an -/// operator approves appears in the listing built over the section that holds it, while the -/// catalogue built before it still lists the tool as pending. -#[test] -fn an_approval_is_visible_from_its_generation_and_not_before() { - let before = catalogue(1, SECTION); - let approved = String::from_utf8(SECTION.to_vec()).expect("utf-8").replace( - r#""write_file": {}"#, - r#""write_file": {"schema_hash": "sha256:cc"}"#, - ); - let after = catalogue(2, approved.as_bytes()); - let hash = |c: &Catalogue| { - read(c.tools_list(&json!(1), &everyone, |_| false))["result"]["tools"][3] - .get("_meta") - .cloned() - }; - assert_eq!(hash(&before), None); - assert_eq!( - hash(&after), - Some(json!({"io.busbar/schemaHash": "sha256:cc"})) - ); - assert_eq!((before.generation(), after.generation()), (1, 2)); -} - -#[test] -fn an_empty_section_is_an_empty_registry() { - let c = catalogue(1, b""); - assert!(c.is_empty()); - let v = read(c.tools_list(&json!(1), &everyone, |_| false)); - assert_eq!(v["result"]["tools"], json!([])); - assert!(!catalogue(1, SECTION).is_empty()); -} diff --git a/crates/busbar-plane-mcp/src/tests/checks.rs b/crates/busbar-plane-mcp/src/tests/checks.rs deleted file mode 100644 index f974cfdf1f..0000000000 --- a/crates/busbar-plane-mcp/src/tests/checks.rs +++ /dev/null @@ -1,170 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! The stateless revision's envelope checks answer as the served engine does: the same order, the -//! same codes and the same sentences. - -use serde_json::{json, Value}; - -use super::*; - -/// A well-formed request for `method`, its body and its mirrored fields. -fn well_formed(method: &str) -> (Value, Vec<(&'static str, String)>) { - let mut params = json!({ - "_meta": { - META_PROTOCOL_VERSION: PROTOCOL_VERSION, - META_CLIENT_CAPABILITIES: {}, - } - }); - let mut fields = vec![ - (H_PROTOCOL_VERSION, PROTOCOL_VERSION.to_string()), - (H_MCP_METHOD, method.to_string()), - ]; - if let Some(source) = name_source_of(method) { - params[source] = json!("read_file"); - fields.push((H_MCP_NAME, "read_file".to_string())); - } - ( - json!({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}), - fields, - ) -} - -fn check(value: &Value, method: &str, fields: &[(&'static str, String)]) -> Result<(), Refused> { - check_request(value, method, |name| { - fields - .iter() - .find(|(n, _)| n.eq_ignore_ascii_case(name)) - .map(|(_, v)| v.as_str()) - }) -} - -fn without(fields: &[(&'static str, String)], name: &str) -> Vec<(&'static str, String)> { - fields.iter().filter(|(n, _)| *n != name).cloned().collect() -} - -fn with( - fields: &[(&'static str, String)], - name: &'static str, - v: &str, -) -> Vec<(&'static str, String)> { - let mut out = without(fields, name); - out.push((name, v.to_string())); - out -} - -#[test] -fn a_well_formed_request_passes_every_check() { - for method in ["tools/list", "tools/call", "prompts/get", "resources/read"] { - let (v, f) = well_formed(method); - assert_eq!(check(&v, method, &f), Ok(()), "{method}"); - } -} - -#[test] -fn a_missing_meta_or_member_is_a_400_invalid_params_in_the_served_words() { - let (mut v, f) = well_formed("tools/list"); - v["params"].as_object_mut().unwrap().remove("_meta"); - let r = check(&v, "tools/list", &f).unwrap_err(); - assert_eq!(r.code, -32602); - assert!(r - .message - .starts_with("`params._meta` is required on every request.")); - - let (mut v, f) = well_formed("tools/list"); - v["params"]["_meta"] - .as_object_mut() - .unwrap() - .remove(META_PROTOCOL_VERSION); - let r = check(&v, "tools/list", &f).unwrap_err(); - assert_eq!(r.code, -32602); - assert!(r - .message - .contains("must carry `io.modelcontextprotocol/protocolVersion`")); - - let (mut v, f) = well_formed("tools/list"); - v["params"]["_meta"] - .as_object_mut() - .unwrap() - .remove(META_CLIENT_CAPABILITIES); - let r = check(&v, "tools/list", &f).unwrap_err(); - assert_eq!(r.code, -32602); - assert!(r - .message - .contains("must carry `io.modelcontextprotocol/clientCapabilities`")); -} - -/// Every mirror/body disagreement is one class: `-32020`. -#[test] -fn every_field_body_disagreement_is_a_header_mismatch() { - let (v, f) = well_formed("tools/call"); - let cases = [ - without(&f, H_PROTOCOL_VERSION), - with(&f, H_PROTOCOL_VERSION, "2025-06-18"), - without(&f, H_MCP_METHOD), - with(&f, H_MCP_METHOD, "tools/list"), - with(&f, H_MCP_METHOD, "TOOLS/CALL"), - without(&f, H_MCP_NAME), - with(&f, H_MCP_NAME, "write_file"), - with(&f, H_MCP_NAME, "=?base64?not base64?="), - ]; - for (i, fields) in cases.iter().enumerate() { - let r = check(&v, "tools/call", fields).unwrap_err(); - assert_eq!(r.code, -32020, "case {i}: {}", r.message); - } -} - -/// The sentinel is decoded before comparison. -#[test] -fn an_encoded_name_matches_its_decoded_body_name() { - let (v, f) = well_formed("tools/call"); - let encoded = format!("=?base64?{}?=", "cmVhZF9maWxl"); - assert_eq!( - check(&v, "tools/call", &with(&f, H_MCP_NAME, &encoded)), - Ok(()) - ); -} - -/// An unsupported revision is judged last and names what is supported. -#[test] -fn an_unsupported_revision_is_a_400_naming_the_supported() { - let (mut v, f) = well_formed("tools/list"); - v["params"]["_meta"][META_PROTOCOL_VERSION] = json!("2025-06-18"); - let f = with(&f, H_PROTOCOL_VERSION, "2025-06-18"); - let r = check(&v, "tools/list", &f).unwrap_err(); - assert_eq!(r.code, -32022); - let data = r.data.expect("data names the revisions"); - assert_eq!(data["requested"], "2025-06-18"); - assert_eq!(data["supported"], json!([PROTOCOL_VERSION])); -} - -/// A malformed request is refused for its malformation before its revision is judged. -#[test] -fn a_malformed_request_is_refused_before_the_revision_is_judged() { - let (mut v, f) = well_formed("tools/list"); - v["params"]["_meta"][META_PROTOCOL_VERSION] = json!("2025-06-18"); - let r = check(&v, "tools/list", &without(&f, H_MCP_METHOD)).unwrap_err(); - assert_eq!(r.code, -32020); -} - -/// The body is the one JSON-RPC error envelope (the contract's builder, the one the served engine -/// renders through), with the id echoed or `null`. Compared as a document: member order is the -/// JSON library's, and it is the same library in both. -#[test] -fn a_refusal_body_is_the_one_error_envelope() { - let r = Refused { - code: -32020, - message: "m", - data: None, - }; - let read = |b: Vec| serde_json::from_slice::(&b).expect("a document"); - assert_eq!( - read(r.body(Some(&json!(7)))), - json!({"jsonrpc": "2.0", "id": 7, "error": {"code": -32020, "message": "m"}}) - ); - assert_eq!( - read(r.body(None)), - json!({"jsonrpc": "2.0", "id": null, "error": {"code": -32020, "message": "m"}}) - ); - assert_eq!(STATUS, 400); -} diff --git a/crates/busbar-plane-mcp/src/tests/claims.rs b/crates/busbar-plane-mcp/src/tests/claims.rs deleted file mode 100644 index 05706d742c..0000000000 --- a/crates/busbar-plane-mcp/src/tests/claims.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! Tests for `claims.rs`. Lifted out of the implementation file so its line count -//! measures implementation and nothing else; still a direct child module, so `use -//! super::*` reaches the private items it always did. - -use super::{ - CARRIER_HTTP, CARRIER_SSE, CARRIER_STDIO, CLAIMS, DEFAULT_METADATA, DEFAULT_MOUNT, SCHEME, - SCHEME_ALTS, -}; -use busbar_contract::grammar::Selector; - -/// The default mount is the codec's own, and the discovery path is composed from it by the -/// codec's own composer. -/// -/// Both are read as VALUES. This asked the same two questions of the server half's SOURCE once, -/// with `include_str!` over `../../busbar-mcp/src/…`, which coupled this crate to a sibling its -/// manifest does not name — so the plane could be neither built nor deleted on its own, and the -/// strong-form deletion gate could not see it. The mount and the composer are the codec's now, -/// and both halves call the one composer, so a served route and this claim cannot be two -/// different strings. -#[test] -fn the_default_mount_is_the_codecs_own() { - assert_eq!(DEFAULT_MOUNT, crate::codec::PATH_MCP); - assert_eq!( - DEFAULT_METADATA, - crate::codec::protected_resource_metadata_path(DEFAULT_MOUNT) - ); -} - -/// The discovery path is a FUNCTION OF THE MOUNT, and this is the assertion that says the note -/// in the module header is still live. -/// -/// The protected-resource metadata rule inserts the well-known segment between the origin and -/// the resource's own path, -/// so an operator who moves the mount moves the discovery document with it. If that composition -/// ever collapses into a constant — if the answer stops depending on its argument — this goes -/// red, and the note stops being true and should be deleted. A note that quietly outlives its -/// cause is worse than no note at all. -#[test] -fn the_mount_is_still_configured() { - let moved = crate::codec::protected_resource_metadata_path("/elsewhere"); - assert_ne!(moved, DEFAULT_METADATA); - assert!(moved.ends_with("/elsewhere")); -} - -/// This plane's registry key is the codec's own. -#[test] -fn the_plane_key_is_the_codecs_own() { - assert_eq!( - ::KEY, - crate::PLANE_KEY - ); - // The other half of this agreement — that the engine's `ProtocolDecl` registers under the same - // key — is asserted where that declaration lives, in `busbar-mcp`'s - // `src/codec/tests/mcp_tests.rs::the_declaration_registers_under_the_planes_key`. A pure kind - // may not name the registry row, so the assertion moved rather than being dropped. -} - -/// Every claim names one of the three declared transports and nothing else. -#[test] -fn every_claim_names_a_declared_transport() { - for c in CLAIMS { - assert!( - [CARRIER_HTTP, CARRIER_SSE, CARRIER_STDIO].contains(&c.transport), - "a claim names the undeclared transport {}", - c.transport - ); - } -} - -/// All three transports are claimed, because the codec serves on all three. -#[test] -fn all_three_transports_are_claimed() { - for transport in [CARRIER_HTTP, CARRIER_SSE, CARRIER_STDIO] { - assert!( - CLAIMS.iter().any(|c| c.transport == transport), - "{transport} is served and not claimed" - ); - } -} - -/// Every claim either declares the one scheme and its alternatives, or declares none. -/// -/// The discovery document is the one open surface: no scheme, and therefore no alternatives. -/// The emptiness is what makes the authenticate step's narrowing check meaningful, because -/// there is nothing there to narrow DOWN to. -#[test] -fn every_claim_declares_one_scheme_or_none() { - let mut open = 0usize; - for c in CLAIMS { - match c.scheme { - Some(scheme) => { - assert_eq!(scheme, SCHEME); - assert_eq!(c.scheme_alternatives, SCHEME_ALTS); - } - None => { - open += 1; - assert!(c.is_anonymous()); - assert!(c.scheme_alternatives.is_empty()); - } - } - assert!(c.idempotency.is_none()); - } - assert_eq!(open, 1, "the discovery document is the one open surface"); -} - -/// No declared alternative is the word "anonymous". -/// -/// The absence of a credential is a property of the CLAIM, not one more thing a plane may -/// narrow to. This is the assertion that keeps the invented alternative from coming back. -#[test] -fn no_alternative_stands_in_for_having_no_credential() { - for c in CLAIMS { - assert!(!c.scheme_alternatives.contains(&"anonymous")); - } -} - -/// Two claims on one transport never name the same selector. -/// -/// Within one plane a contest is settled by order, so a duplicate is not a boot refusal — it is -/// a claim that can never be reached, which is worse, because nothing complains about it. -#[test] -fn no_claim_is_unreachable() { - for (i, later) in CLAIMS.iter().enumerate() { - for earlier in &CLAIMS[..i] { - assert!( - !(earlier.transport == later.transport - && selector_eq(&earlier.selector, &later.selector)), - "a claim on {} repeats one above it", - later.transport - ); - } - } -} - -/// Whether two selectors are literally the same selector. -fn selector_eq(a: &Selector, b: &Selector) -> bool { - a == b -} diff --git a/crates/busbar-plane-mcp/src/tests/codec_tests.rs b/crates/busbar-plane-mcp/src/tests/codec_tests.rs deleted file mode 100644 index abf082a0bb..0000000000 --- a/crates/busbar-plane-mcp/src/tests/codec_tests.rs +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! The notification codec's tests — the same contract the cells are held to, for a message that has -//! no answer: write it, read it back, and assert it is what it was. - -use super::*; -use serde_json::json; - -// ── THE NOTIFICATION VOCABULARY ────────────────────────────────────────────────────────────────── - -/// THE MESSAGE THAT MAKES A SUBSCRIPTION WORTH HAVING, and its sibling that says a tool list moved. -/// One reader and one writer for both directions of travel: busbar emits these when it is the server -/// and receives them when it is the client, and a second implementation per direction is how two -/// readings of one message come to disagree. -#[test] -fn the_notifications_round_trip_and_carry_no_id() { - for n in [ - McpNotification::ToolsListChanged, - McpNotification::ResourceUpdated { - uri: "file:///log.txt".to_string(), - }, - ] { - let out: serde_json::Value = serde_json::from_slice(&n.write()).expect("writes JSON"); - assert_eq!(out["jsonrpc"], "2.0"); - assert_eq!(out["method"], n.method()); - assert!( - out.get("id").is_none(), - "a notification has no id: an id would make it a request, and a request obliges an \ - answer nobody is waiting for" - ); - let back = McpNotification::read( - out["method"].as_str().expect("a method name"), - out.get("params"), - ); - assert_eq!(back.as_ref(), Some(&n), "and it reads back as what it was"); - } -} - -#[test] -fn the_tools_list_changed_notification_carries_no_params_at_all() { - let out: serde_json::Value = - serde_json::from_slice(&McpNotification::ToolsListChanged.write()).expect("writes JSON"); - assert_eq!(out["method"], "notifications/tools/list_changed"); - assert!( - out.get("params").is_none(), - "this message has no parameters, so emitting an empty object would be inventing a member" - ); -} - -/// AN UPDATE THAT NAMES NO RESOURCE IS NOT AN UPDATE. Acting on it would mean guessing which of a -/// caller's subscriptions it was about. -#[test] -fn a_resource_update_that_names_no_resource_is_not_read() { - assert_eq!( - McpNotification::read("notifications/resources/updated", None), - None - ); - assert_eq!( - McpNotification::read( - "notifications/resources/updated", - Some(&serde_json::json!({ "uri": "" })) - ), - None - ); -} - -/// A NOTIFICATION THIS PROTOCOL DOES NOT CARRY IS DROPPED, NOT REFUSED. JSON-RPC 2.0 forbids -/// replying to a notification, so there is nothing to send back and inventing an error envelope -/// would break that rule to report a message that harmed nothing. -#[test] -fn an_unknown_notification_is_simply_not_one_of_these() { - assert_eq!(McpNotification::read("notifications/nope", None), None); -} - -#[test] -fn the_mirrored_fields_are_what_the_body_states_and_nothing_else() { - let call = json!({ - "jsonrpc": "2.0", "id": 1, "method": "tools/call", - "params": { "name": "fs_read", "_meta": { META_PROTOCOL_VERSION: "2026-07-28" } } - }); - let fields = mirrored(&call); - let get = |n: &str| fields.iter().find(|(k, _)| k == n).map(|(_, v)| v.as_str()); - assert_eq!(get(H_MCP_METHOD), Some("tools/call")); - assert_eq!(get(H_MCP_NAME), Some("fs_read")); - assert_eq!(get(H_PROTOCOL_VERSION), Some("2026-07-28")); - assert_eq!(get("accept"), Some("application/json, text/event-stream")); - // A body that names no version states none: the body defect stays a body defect. - let bare = json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }); - let fields = mirrored(&bare); - assert!(fields.iter().all(|(k, _)| k != H_PROTOCOL_VERSION)); - assert!(fields.iter().all(|(k, _)| k != H_MCP_NAME)); - // A request that asked for log records prefers the stream's frames. - let logged = json!({ - "jsonrpc": "2.0", "id": 1, "method": "tools/list", - "params": { "_meta": { crate::framing::META_LOGGING_LEVEL: "debug" } } - }); - assert!(mirrored(&logged) - .iter() - .any(|(k, v)| k == "accept" && v.starts_with("text/event-stream"))); -} diff --git a/crates/busbar-plane-mcp/src/tests/diagnostics_tests.rs b/crates/busbar-plane-mcp/src/tests/diagnostics_tests.rs deleted file mode 100644 index c43c57501f..0000000000 --- a/crates/busbar-plane-mcp/src/tests/diagnostics_tests.rs +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! Per-plane diagnostics-catalog invariants for the MCP plane: code/slug uniqueness, the -//! class↔code thousands-digit contract, and the live-entry documentation floor. The committed -//! markdown/JSON snapshot tests render through the host's renderers, which this crate does not -//! link, so they run where those renderers are linked. - -use super::*; - -/// THE CATALOG IS NOT EMPTY, AND NOT ENTIRELY RETIRED. -/// -/// Every other test in this file is a `for d in DIAGNOSTICS` loop whose assertions live INSIDE the -/// loop, and `every_live_entry_documents_meaning_and_action` additionally `continue`s past every -/// retired entry. A catalog that regressed to empty — a bad `cfg` gate, a botched merge — would -/// therefore make all four of them pass green while every documented diagnostic silently -/// disappeared. This is the floor that makes those loops mean something. -#[test] -fn the_catalog_is_not_empty() { - assert!( - !DIAGNOSTICS.is_empty(), - "the diagnostics catalog is empty, which makes every loop test in this file vacuous" - ); - assert!( - DIAGNOSTICS.iter().any(|d| !d.retired), - "the catalog holds nothing but retired entries, which makes \ - `every_live_entry_documents_meaning_and_action` vacuous" - ); -} - -/// Codes are unique within this plane's catalog — a collision would make one un-resolvable. -#[test] -fn codes_are_unique() { - let mut seen = std::collections::BTreeSet::new(); - for d in DIAGNOSTICS { - assert!( - seen.insert(d.code), - "duplicate code {} ({})", - d.code, - d.slug - ); - } -} - -/// The thousands digit of every code equals its class ordinal, and the x000 slot is reserved. -#[test] -fn code_thousands_digit_matches_class() { - for d in DIAGNOSTICS { - assert_eq!( - d.code / 1000, - d.class.ordinal(), - "{} ({}) class/code mismatch", - d.banner(), - d.slug - ); - assert!( - d.code % 1000 != 0, - "{} ({}) uses the reserved x000 slot", - d.banner(), - d.slug - ); - } -} - -/// Slugs are unique, non-empty, kebab-case — they are stable doc anchors and URL fragments. -#[test] -fn slugs_are_unique_and_kebab_case() { - let mut seen = std::collections::BTreeSet::new(); - for d in DIAGNOSTICS { - assert!( - seen.insert(d.slug), - "duplicate slug {:?} (code {})", - d.slug, - d.code - ); - assert!(!d.slug.is_empty(), "{} has an empty slug", d.banner()); - assert!( - d.slug - .chars() - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'), - "slug {:?} (code {}) is not kebab-case", - d.slug, - d.code - ); - assert!( - !d.slug.starts_with('-') && !d.slug.ends_with('-') && !d.slug.contains("--"), - "slug {:?} (code {}) has a leading/trailing/double hyphen", - d.slug, - d.code - ); - } -} - -/// Every non-retired entry documents its meaning and an action. -#[test] -fn every_live_entry_documents_meaning_and_action() { - for d in DIAGNOSTICS { - if d.retired { - continue; - } - assert!( - d.summary.len() > 20, - "{} ({}) has no real summary", - d.banner(), - d.slug - ); - assert!( - d.action.len() > 3, - "{} ({}) has no action", - d.banner(), - d.slug - ); - } -} - -/// THE PLANE'S `declares.json` IS ITS CATALOG: the static declared metadata the root reads for this -/// linked plugin (its `declares` section: every code this plane raises) states exactly the entries -/// [`DIAGNOSTICS`] holds, field for field, so the two cannot drift — and the plane's breaker fact. -#[test] -fn the_declares_file_states_the_catalog() { - let declared: serde_json::Value = - serde_json::from_str(crate::DECLARES).expect("declares.json is JSON"); - let want: Vec = DIAGNOSTICS - .iter() - .map(|d| { - serde_json::json!({ - "code": d.code, - "slug": d.slug, - "title": d.title, - "severity": d.severity.as_str(), - "summary": d.summary, - "action": d.action, - "since": d.since, - }) - }) - .collect(); - // Beside the catalog, the plane's one breaker fact (ARCHITECT Q4): a transient failure below - // the trip threshold never benches a member (the 1.5.5 MCP client leg's posture). - let whole = serde_json::json!({ - "breaker": { "bench_below_trip_threshold": false }, - "diagnostics": want, - }); - assert_eq!( - declared, - whole, - "declares.json drifted from the catalog; expected:\n{}", - serde_json::to_string_pretty(&whole).unwrap() - ); -} diff --git a/crates/busbar-plane-mcp/src/tests/door.rs b/crates/busbar-plane-mcp/src/tests/door.rs deleted file mode 100644 index d742801988..0000000000 --- a/crates/busbar-plane-mcp/src/tests/door.rs +++ /dev/null @@ -1,307 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! The door's declarations: the tail the loader judges, the trust keys it states against the -//! grammar's own table, the settings reader and each generation's snapshot. - -use busbar_contract::abi::mechanism::call::AbiStr; -use busbar_contract::abi::plane::check::check_tail; -use busbar_contract::abi::plane::{ - CLAIM_EXACT, CLAIM_OPEN, MECHANISM_PEER_KEY, MECHANISM_ROOT, TRUST_PIN, TRUST_PRIVATE_REACH, - TRUST_REVERIFY_TTL, -}; -use busbar_contract::plane::TrustRole; - -use super::*; - -/// A good section: one fronted server. -const GOOD: &[u8] = - br#"{"fs": {"url": "https://mcp.example/fs", "pin": {"mechanism": "unpinned"}}}"#; -/// A registration the grammar refuses: a server id holding the routing-key separator. -const BAD: &[u8] = - br#"{"my_fs": {"url": "https://mcp.example/fs", "pin": {"mechanism": "unpinned"}}}"#; - -/// A tail string read back through the `&'static str` it was built from: the tail's strings are -/// constants of this crate, so a string this test knows is found by address and length. -fn text(a: AbiStr) -> String { - if a.ptr.is_null() { - return String::new(); - } - [ - "pin", - "verify_ttl", - "allow_private", - crate::tools_config::TOOL_APPROVALS_KEY, - crate::tools_config::TOOL_APPROVAL_FIELD, - crate::tools_config::DEFAULT_MCP_VERIFY_TTL, - "pinned_pubkey", - "cert_spki", - "mtls", - "unpinned", - crate::tool_records::KIND_CALL, - crate::tool_records::KIND_DEMOTION, - crate::tool_records::KIND_APPROVAL, - crate::tool_records::KIND_TASK, - crate::tool_meta::CLASS_TOOL_CALLS.as_str(), - crate::tool_meta::CLASS_BYTES.as_str(), - FEE_PER_REQUEST, - ] - .into_iter() - .find(|c| c.as_ptr() == a.ptr && c.len() == a.len) - .expect("a tail string this test knows") - .to_string() -} - -#[test] -fn the_tail_is_one_the_loader_accepts() { - assert_eq!(check_tail(TAIL), Ok(())); -} - -/// The tail's trust keys are the grammar's own table, key by key, in its order: the kernel judges -/// what the plane declares, so the two may not drift. -#[test] -fn the_tails_trust_keys_are_the_grammars() { - assert_eq!(TRUST_KEYS.len(), crate::tools_config::TRUST_KEYS.len()); - for (abi, decl) in TRUST_KEYS.iter().zip(crate::tools_config::TRUST_KEYS) { - assert_eq!(text(abi.key), decl.key); - let role = match decl.role { - TrustRole::Pin => TRUST_PIN, - TrustRole::ReverifyTtl => TRUST_REVERIFY_TTL, - TrustRole::PrivateReach => TRUST_PRIVATE_REACH, - TrustRole::RecoveryBackoff => panic!("the plane declares no recovery-backoff key"), - TrustRole::ItemApprovals => busbar_contract::abi::plane::TRUST_ITEM_APPROVALS, - }; - assert_eq!(abi.role, role, "{}", decl.key); - assert_eq!( - (!abi.default.ptr.is_null()).then(|| text(abi.default)), - decl.default.map(str::to_string), - "{}", - decl.key - ); - assert_eq!(abi.mechanisms_len, decl.mechanisms.len(), "{}", decl.key); - } - for (abi, decl) in PIN_MECHANISMS - .iter() - .zip(crate::tools_config::TRUST_KEYS[0].mechanisms) - { - assert_eq!(text(abi.token), decl.token); - assert_eq!(abi.flags & MECHANISM_ROOT != 0, decl.root, "{}", decl.token); - assert_eq!( - abi.flags & MECHANISM_PEER_KEY != 0, - decl.peer_key, - "{}", - decl.token - ); - } -} - -/// The record kinds and billable classes are the plane's own symbols, stated once. -#[test] -fn the_tail_names_the_planes_own_classes_and_kinds() { - let kinds: Vec = RECORD_KINDS.iter().map(|k| text(*k)).collect(); - assert_eq!(kinds, crate::tool_records::RECORD_KINDS); - let classes: Vec = BILLABLE_CLASSES.iter().map(|c| text(c.class)).collect(); - assert_eq!( - classes, - [ - crate::tool_meta::CLASS_TOOL_CALLS.as_str(), - crate::tool_meta::CLASS_BYTES.as_str(), - FEE_PER_REQUEST, - ] - ); - assert_eq!( - text(BILLABLE_CLASSES[CLASS_FEE_INDEX as usize].class), - FEE_PER_REQUEST - ); -} - -#[test] -fn an_empty_blob_is_the_empty_section() { - assert_eq!( - read_tools_section(b""), - Ok(crate::tools_config::ToolsCfg::default()) - ); -} - -#[test] -fn a_good_section_reads_and_a_bad_one_is_refused_in_the_grammars_words() { - let cfg = read_tools_section(GOOD).expect("one fronted server reads"); - assert_eq!(cfg.servers.len(), 1); - let err = read_tools_section(BAD).expect_err("a separator in the id is refused"); - assert!( - err.starts_with("`tools.my_fs`: an MCP server id may not contain `_`."), - "{err}" - ); -} - -/// With no public base there is no audience and nothing is claimed; with one, the endpoint and its -/// metadata document are absolute and every route is claimed, the discovery document open. -#[test] -fn a_generation_claims_its_routes_only_with_a_public_base() { - let none = endpoint_snapshot_spec(None); - assert!(none.claims.is_empty()); - assert_eq!(none.audience, None); - assert_eq!(none.admin_routes.len(), ADMIN_VERBS.len()); - - let some = endpoint_snapshot_spec(Some("https://gw.example/")); - assert_eq!(some.audience.as_deref(), Some("https://gw.example/mcp")); - assert_eq!( - some.resource_metadata.as_deref(), - Some("https://gw.example/.well-known/oauth-protected-resource/mcp") - ); - assert_eq!(some.claims.len(), ROUTES.len()); - assert_eq!(some.claims[0].flags, CLAIM_EXACT | CLAIM_OPEN); - assert!(some.claims[1..].iter().all(|c| c.flags == CLAIM_EXACT)); -} - -/// THE ENDPOINT THE CLAIMS ARE STATED UNDER (ARCHITECT Q-L3B-AUD, predev's rule): the plane's own -/// `mcp:` block when the document writes one — its canonical URI verbatim as the audience, whatever -/// the public base URL — else the public base URL; neither, nothing; a malformed block, refused in -/// its grammar's words. -#[test] -fn the_claims_are_stated_under_the_endpoint_block_else_the_public_base() { - let owned = br#"{"mcp": {"canonical_uri": "https://Gw.Example:443/mcp", "authorization_servers": ["https://login.example"]}}"#; - assert_eq!( - admitted_endpoint(Some("https://other.example"), owned), - Ok(Some(( - "https://Gw.Example:443/mcp".to_string(), - "https://Gw.Example:443/.well-known/oauth-protected-resource/mcp".to_string() - ))), - "the block's canonical URI, verbatim" - ); - assert_eq!( - admitted_endpoint(Some("https://gw.example/"), b""), - Ok(Some(under_public_url("https://gw.example/"))) - ); - assert_eq!(admitted_endpoint(None, b""), Ok(None)); - assert_eq!(admitted_endpoint(None, br#"{"other": {}}"#), Ok(None)); - let refused = admitted_endpoint( - None, - br#"{"mcp": {"canonical_uri": "https://gw.example/elsewhere", "authorization_servers": ["https://l.example"]}}"#, - ) - .expect_err("a block naming another path is refused"); - assert!(refused.contains("only at `/mcp`"), "{refused}"); - let claimed = snapshot_spec_for(admitted_endpoint(None, owned).unwrap()); - assert_eq!( - claimed.audience.as_deref(), - Some("https://Gw.Example:443/mcp") - ); - assert_eq!(claimed.claims.len(), ROUTES.len()); -} - -/// The Statement's version is the crate's: read from the manifest, as the plane reads no build -/// environment. -#[test] -fn the_statement_version_is_the_crates() { - let manifest = include_str!("../../Cargo.toml"); - assert!( - manifest.contains(&format!("version = \"{}\"", crate::tool_door::VERSION)), - "the Statement names {} and the manifest does not", - crate::tool_door::VERSION - ); -} - -/// The tail states one operation class per method row's class, in their order, and each row's -/// class is found at its own index. -#[test] -fn the_tail_states_every_operation_class() { - assert_eq!(OP_CLASS_TABLE.len(), crate::tool_ops::OP_CLASSES.len()); - for (i, op) in crate::tool_ops::OP_CLASSES.iter().enumerate() { - assert_eq!(op_class_index(*op), Some(i as u32)); - let abi = OP_CLASS_TABLE[i].op; - assert_eq!( - (abi.ptr, abi.len), - (op.as_str().as_ptr(), op.as_str().len()) - ); - } - assert_eq!(TAIL.op_classes_len, OP_CLASS_TABLE.len()); -} - -/// THE DNS-REBINDING ALLOWLIST is the `mcp:` block's `allowed_origins` (none without a block), and -/// the refusal is the served engine's words: an OAuth-style `invalid_origin` object. -#[test] -fn the_admitted_origins_are_the_blocks_and_the_refusal_is_the_served_words() { - assert_eq!(allowed_origins(b""), Ok(Vec::new())); - let owned = br#"{"mcp":{"canonical_uri":"https://gw.example/mcp","allowed_origins":["https://app.example"]}}"#; - assert_eq!( - allowed_origins(owned), - Ok(vec!["https://app.example".to_string()]) - ); - let body: serde_json::Value = - serde_json::from_slice(&forbidden_origin_body()).expect("a JSON object"); - assert_eq!( - body, - serde_json::json!({ - "error": "invalid_origin", - "error_description": - "This Origin is not allowed. Browser origins must be listed in mcp.allowed_origins.", - }) - ); - assert_eq!(STATUS_FORBIDDEN_ORIGIN, 403); -} - -/// THE ADMIN OPENAPI BLOB CARRIES ITS OWN BODIES (ARCHITECT Q2): every admin verb's `200` names a -/// typed body by `$ref`, and every `$ref` in the blob, its schemas' included, names a schema the -/// blob's `components.schemas` states. The schemas' bytes are held to the committed admin document -/// by the admin crate's drift test. -#[test] -fn the_admin_openapi_blob_states_every_body_its_verbs_name() { - fn refs(v: &serde_json::Value, out: &mut Vec) { - match v { - serde_json::Value::Object(m) => { - if let Some(r) = m.get("$ref").and_then(|r| r.as_str()) { - out.push(r.to_string()); - } - m.values().for_each(|v| refs(v, out)); - } - serde_json::Value::Array(a) => a.iter().for_each(|v| refs(v, out)), - _ => {} - } - } - let blob: serde_json::Value = serde_json::from_str(ADMIN_OPENAPI).expect("a JSON object"); - let schemas = blob["components"]["schemas"] - .as_object() - .expect("the blob states its schemas"); - for (verb, target) in ADMIN_VERBS { - let op = &blob[*target][verb.to_ascii_lowercase()]; - let body = &op["responses"]["200"]["content"]["application/json"]["schema"]["$ref"]; - assert!(body.is_string(), "{verb} {target} names no typed body"); - } - let mut named = Vec::new(); - refs(&blob, &mut named); - for r in &named { - let name = r - .strip_prefix("#/components/schemas/") - .unwrap_or_else(|| panic!("`{r}` is no component reference")); - assert!(schemas.contains_key(name), "`{r}` names no stated schema"); - } - for name in schemas.keys() { - assert!( - named.iter().any(|r| r.ends_with(&format!("/{name}"))), - "the blob states `{name}` and nothing names it" - ); - } -} - -/// `validate` reads the blob stage 3g deals a plane (`{tools:
, mcp: }`): the -/// `tools:` section inside it is judged; a blob that writes no `tools:` is the empty section; the -/// endpoint block beside it is not this reader's to judge. -#[test] -fn validate_reads_the_dealt_blob_at_its_tools_section() { - use crate::door::read_dealt_tools; - let empty = crate::tools_config::ToolsCfg::default(); - assert_eq!(read_dealt_tools(b""), Ok(empty.clone())); - assert_eq!(read_dealt_tools(br#"{"mcp":{}}"#), Ok(empty)); - let mut dealt = br#"{"tools":"#.to_vec(); - dealt.extend_from_slice(GOOD); - dealt.push(b'}'); - assert_eq!( - read_dealt_tools(&dealt).expect("the dealt section reads"), - read_tools_section(GOOD).expect("the bare section reads") - ); - let mut bad = br#"{"tools":"#.to_vec(); - bad.extend_from_slice(BAD); - bad.push(b'}'); - assert!(read_dealt_tools(&bad).is_err()); - assert!(read_dealt_tools(b"[]").is_err()); -} diff --git a/crates/busbar-plane-mcp/src/tests/endpoint.rs b/crates/busbar-plane-mcp/src/tests/endpoint.rs deleted file mode 100644 index 832127151e..0000000000 --- a/crates/busbar-plane-mcp/src/tests/endpoint.rs +++ /dev/null @@ -1,227 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! `mcp:` validation and derivation, moved with the code from the retired host crate. -//! -//! Every refusal here is a BOOT refusal, and the reason they are all tested rather than sampled is -//! that each one prevents a deployment that would answer requests while being subtly wrong about its -//! own identity — advertising one audience in its metadata document and enforcing another in its -//! verifier. That failure is invisible from inside busbar and fatal for every client. - -use super::{McpCfg, McpCfgError, McpResource}; -use crate::tool_claims::DEFAULT_MOUNT; - -fn cfg(uri: &str) -> McpCfg { - McpCfg { - canonical_uri: uri.to_string(), - authorization_servers: vec!["https://login.example.com".to_string()], - scopes_supported: Vec::new(), - allowed_origins: Vec::new(), - } -} - -/// THE DERIVATION, which is the whole reason the mount path is not a separate config key: the path -/// a client posts to and the identifier its token is bound to come from ONE string, so they cannot -/// drift. And the RFC 9728 section 3.1 path-INSERTION rule — the resource's path goes AFTER the well-known -/// prefix, not before it. Getting that backwards 404s every compliant client's discovery. -#[test] -fn the_mount_and_the_metadata_path_are_derived_from_the_one_canonical_uri() { - let r = McpResource::from_cfg(&cfg("https://gateway.example.com/mcp")).unwrap(); - assert_eq!(r.canonical_uri(), "https://gateway.example.com/mcp"); - assert_eq!(r.mount_path(), "/mcp"); - assert_eq!( - r.metadata_path(), - "/.well-known/oauth-protected-resource/mcp" - ); - assert_eq!( - r.metadata_url(), - "https://gateway.example.com/.well-known/oauth-protected-resource/mcp" - ); -} - -/// A multi-segment path is refused (below); a trailing slash does not create a second spelling of -/// one deployment, and the audience stays the string the operator wrote. -#[test] -fn a_trailing_slash_derives_the_same_mount() { - let bare = format!("https://h.example{DEFAULT_MOUNT}"); - let slashed = format!("{bare}/"); - let r = McpResource::from_cfg(&cfg(&bare)).unwrap(); - let s = McpResource::from_cfg(&cfg(&slashed)).unwrap(); - assert_eq!(s.mount_path(), DEFAULT_MOUNT); - assert_eq!(s.mount_path(), r.mount_path()); - assert_eq!(s.metadata_path(), r.metadata_path()); - assert_eq!(s.canonical_uri(), slashed); -} - -/// CG-17: THE MOUNT IS FIXED, NOT OPERATOR-CONFIGURABLE. The design rules that inbound paths are -/// compile-time claims, and a configured address naming a path nothing claims refuses boot at -/// validation. -/// -/// Such an address used to be accepted: the process advertised that path in its `401` challenge and -/// bound every token's audience to it, while the claim table served another — a deployment its -/// clients can never reach. It is a boot refusal now, and the refusal says what to type. Paths -/// differ by case, so an upper-cased mount is another path, and so is a nested one. -#[test] -fn a_canonical_uri_naming_any_other_path_is_refused_at_boot() { - let upper = DEFAULT_MOUNT.to_uppercase(); - let cases = [ - format!("/api{DEFAULT_MOUNT}"), - format!("{DEFAULT_MOUNT}/v2"), - format!("{DEFAULT_MOUNT}-staging"), - "/tools".to_string(), - upper, - ]; - for path in &cases { - for uri in [ - format!("https://h.example{path}"), - format!("https://h.example{path}/"), - ] { - let err = McpResource::from_cfg(&cfg(&uri)) - .expect_err(&format!("`{uri}` names `{path}`, which nothing claims")); - assert_eq!( - err.to_string(), - format!( - "mcp.canonical_uri `{uri}` names the path `{path}`, but the MCP endpoint is \ - served only at `{DEFAULT_MOUNT}`; the path is fixed, not configurable. Keep the \ - scheme, host and port and end the URI in `{DEFAULT_MOUNT}`. Example: \ - `https://gateway.example.com{DEFAULT_MOUNT}`" - ), - "`{uri}`" - ); - } - } -} - -/// The canonical URI is NOT normalised, only recognised. It is compared byte-for-byte against the -/// `aud` an authorization server minted, and a parser that helpfully lower-cased the host or -/// stripped a default port would hand back a string that no longer equals what the IdP issued — -/// turning a correct client into a refused one, for a reason nothing logs. -#[test] -fn the_canonical_uri_is_preserved_verbatim_rather_than_normalised() { - let uri = format!("https://Gateway.Example.COM:443{DEFAULT_MOUNT}"); - let r = McpResource::from_cfg(&cfg(&uri)).unwrap(); - assert_eq!(r.canonical_uri(), uri); - assert_eq!(r.mount_path(), DEFAULT_MOUNT); -} - -/// Every refusal arm, with the exact error, because a boot refusal that names the wrong field sends -/// an operator to edit the wrong line. -#[test] -fn every_malformed_canonical_uri_is_refused_with_its_own_diagnosis() { - let cases: &[(&str, McpCfgError)] = &[ - ("", McpCfgError::MissingCanonicalUri), - (" ", McpCfgError::MissingCanonicalUri), - ( - "gateway.example.com/mcp", - McpCfgError::CanonicalUriNotAbsolute("gateway.example.com/mcp".into()), - ), - ( - "ftp://gateway.example.com/mcp", - McpCfgError::CanonicalUriNotAbsolute("ftp://gateway.example.com/mcp".into()), - ), - ( - "https:///mcp", - McpCfgError::CanonicalUriNotAbsolute("https:///mcp".into()), - ), - ( - "https://gateway.example.com/mcp?tenant=a", - McpCfgError::CanonicalUriHasQueryOrFragment( - "https://gateway.example.com/mcp?tenant=a".into(), - ), - ), - ( - "https://gateway.example.com/mcp#frag", - McpCfgError::CanonicalUriHasQueryOrFragment( - "https://gateway.example.com/mcp#frag".into(), - ), - ), - ( - "https://gateway.example.com", - McpCfgError::CanonicalUriHasNoPath("https://gateway.example.com".into()), - ), - ( - "https://gateway.example.com/", - McpCfgError::CanonicalUriHasNoPath("https://gateway.example.com/".into()), - ), - ]; - for (uri, expected) in cases { - assert_eq!( - McpResource::from_cfg(&cfg(uri)).unwrap_err(), - *expected, - "`{uri}`" - ); - } -} - -/// The MCP plane may not mount at the deployment root. The LLM plane is the RESIDUAL — its catch-all -/// claims every unclaimed path by construction — so an MCP mount at `/` would sit in front of every -/// protocol endpoint in the process and answer them all with JSON-RPC errors. -#[test] -fn the_plane_cannot_mount_at_the_deployment_root() { - assert!(matches!( - McpResource::from_cfg(&cfg("https://gateway.example.com/")), - Err(McpCfgError::CanonicalUriHasNoPath(_)) - )); -} - -/// An authorization-server list is the ENTIRE content of the answer a credential-less client came -/// for. With none, the `401` it receives names nowhere to go, and the discovery loop dead-ends at -/// step two with no error anyone can act on. -#[test] -fn a_resource_with_no_authorization_server_is_refused() { - let mut c = cfg("https://gateway.example.com/mcp"); - c.authorization_servers.clear(); - assert_eq!( - McpResource::from_cfg(&c).unwrap_err(), - McpCfgError::NoAuthorizationServers - ); - c.authorization_servers = vec!["login.example.com".to_string()]; - assert_eq!( - McpResource::from_cfg(&c).unwrap_err(), - McpCfgError::AuthorizationServerNotAbsolute("login.example.com".into()) - ); -} - -/// The allowlist is the operator's list verbatim: the default is empty, and what the operator wrote -/// is what the resource carries. The verdict on an `Origin` is not the plane's (one rule for every -/// plane, asserted beside that rule); this holds the data half it reads. -#[test] -fn the_origin_allowlist_is_carried_as_written_and_empty_by_default() { - let mut c = cfg("https://gateway.example.com/mcp"); - let empty = McpResource::from_cfg(&c).unwrap(); - assert!( - empty.allowed_origins().is_empty(), - "the default allowlist is empty" - ); - c.allowed_origins = vec!["https://app.example.com".to_string()]; - let r = McpResource::from_cfg(&c).unwrap(); - assert_eq!(r.allowed_origins(), ["https://app.example.com".to_string()]); -} - -/// The `mcp:` block parses with the field names an operator actually writes, and an unknown key is -/// REFUSED rather than silently ignored — a typo'd `allowed_origin:` that parsed to an empty -/// allowlist would look like a working config and be a closed door. (The composition root reads -/// the block as a parsed value, so a JSON document carries the same shape here.) -#[test] -fn the_config_block_parses_and_refuses_an_unknown_key() { - let parsed: McpCfg = serde_json::from_str( - r#"{"canonical_uri": "https://gateway.example.com/mcp", - "authorization_servers": ["https://login.example.com"], - "scopes_supported": ["mcp:tools:list"], - "allowed_origins": []}"#, - ) - .expect("the documented shape must parse"); - assert_eq!(parsed.canonical_uri, "https://gateway.example.com/mcp"); - assert_eq!(parsed.scopes_supported, vec!["mcp:tools:list".to_string()]); - - let typo = serde_json::from_str::( - r#"{"canonical_uri": "https://gateway.example.com/mcp", - "authorization_servers": ["https://login.example.com"], - "allowed_origin": ["https://app.example.com"]}"#, - ); - assert!( - typo.is_err(), - "an unknown key must be refused: silently ignoring `allowed_origin` leaves a config that \ - reads as permissive and behaves as closed" - ); -} diff --git a/crates/busbar-plane-mcp/src/tests/facts.rs b/crates/busbar-plane-mcp/src/tests/facts.rs deleted file mode 100644 index 771be28044..0000000000 --- a/crates/busbar-plane-mcp/src/tests/facts.rs +++ /dev/null @@ -1,220 +0,0 @@ -//! Tests for `facts.rs`. Lifted out of the implementation file so its line count -//! measures implementation and nothing else; still a direct child module, so `use -//! super::*` reaches the private items it always did. - -use super::{correlation_for, correlation_value, FACT_RPC_ID}; -use busbar_contract::bounded::{PlaneAlloc, PlaneAllocBudget, ScratchBytes, Span}; -use busbar_contract::ids::CorrelationValue; - -/// An arena that hands out leaked bytes, which is what a test arena is. -struct TestPlaneAlloc; - -impl PlaneAlloc for TestPlaneAlloc { - fn alloc_bytes<'a>(&'a self, src: &[u8]) -> Result, PlaneAllocBudget> { - Ok(ScratchBytes::new(Box::leak( - src.to_vec().into_boxed_slice(), - ))) - } - - fn alloc_str<'a>(&'a self, src: &str) -> Result<&'a str, PlaneAllocBudget> { - Ok(Box::leak(src.to_string().into_boxed_str())) - } - - fn alloc_spans<'a>( - &'a self, - src: &[(&'a str, Span)], - ) -> Result<&'a [(&'a str, Span)], PlaneAllocBudget> { - Ok(Box::leak(src.to_vec().into_boxed_slice())) - } - - fn remaining(&self) -> usize { - usize::MAX - } -} - -/// A bare counter is the number it is, so a journal row reads as the caller wrote it. -/// -/// Every request the conformance battery sends is numbered, so this is the case that matters -/// most: the battery's own identifiers arrive in a journal as the battery wrote them. -#[test] -fn a_bare_number_is_itself() { - let arena = TestPlaneAlloc; - for n in 0u64..64 { - assert_eq!( - correlation_value(n.to_string().as_bytes(), &arena), - Some(CorrelationValue::Num(n)) - ); - } -} - -/// A quoted identifier is the text it is, not a number standing in for it. -#[test] -fn a_named_identifier_is_carried_as_itself() { - let arena = TestPlaneAlloc; - assert_eq!( - correlation_value(br#""req-1""#, &arena), - Some(CorrelationValue::Str("req-1")) - ); -} - -/// Two string identifiers of ONE principal on ONE session never answer to each other. -/// -/// This is the collision the digest could not rule out, and the reason the digest is gone: the -/// kernel correlates on (session, principal, fact key, value), so two values that compare equal -/// are one hold. Checked over every pair of a set chosen to include the near misses. -#[test] -fn two_string_identifiers_of_one_principal_never_collide() { - let arena = TestPlaneAlloc; - let raw: [&[u8]; 8] = [ - br#""req-1""#, - br#""req-2""#, - br#""a""#, - br#""b""#, - br#""0""#, - br#""7""#, - br#""req-1 ""#, - br#""REQ-1""#, - ]; - let values: Vec<_> = raw - .iter() - .map(|r| correlation_value(r, &arena).expect("it is carried")) - .collect(); - for (i, left) in values.iter().enumerate() { - for (j, right) in values.iter().enumerate() { - if i != j { - assert_ne!(left, right, "{:?} and {:?} collided", raw[i], raw[j]); - } - } - } -} - -/// A string that reads like a counter is still a string, and never equals the counter. -#[test] -fn a_quoted_seven_is_not_the_number_seven() { - let arena = TestPlaneAlloc; - assert_ne!( - correlation_value(br#""7""#, &arena), - correlation_value(b"7", &arena) - ); -} - -/// Anything that is not a CANONICAL run of digits is carried as text, including the near misses. -/// -/// The leading-zero rows are the ones that matter and the ones this test used to miss: it named -/// a single twenty-digit run, which was carried as text because it was too long to be a number -/// rather than because it began with a zero, so the guard it appeared to test was the length -/// guard. `007` is short enough to be a number and must still not BE one — it is different bytes -/// from `7`, the identifier is compared as a JSON value, and folding the two together makes two -/// open calls of one principal on one session answer to each other. -#[test] -fn the_near_misses_are_carried_as_text() { - let arena = TestPlaneAlloc; - for raw in [ - &b"-1"[..], - &b"1.0"[..], - &b"1e3"[..], - &b"007"[..], - &b"00"[..], - &b"0123"[..], - &b"01234567890123456789"[..], - ] { - assert!( - matches!( - correlation_value(raw, &arena), - Some(CorrelationValue::Str(_)) - ), - "{raw:?} was not carried as text" - ); - } -} - -/// A leading zero makes a DIFFERENT identifier, and the two never answer to each other. -/// -/// The one zero that is a number is the number zero written by itself. -#[test] -fn a_padded_identifier_is_not_the_number_it_pads() { - let arena = TestPlaneAlloc; - assert_eq!( - correlation_value(b"007", &arena), - Some(CorrelationValue::Str("007")) - ); - assert_ne!( - correlation_value(b"007", &arena), - correlation_value(b"7", &arena) - ); - assert_eq!( - correlation_value(b"0", &arena), - Some(CorrelationValue::Num(0)) - ); -} - -/// The EMPTY identifier correlates with nothing. -/// -/// This protocol spells "the request could not be read" as the empty value, so an answer -/// carrying it answers no request of anyone's. It used to be carried as the four letters it is -/// written with, which is a text identifier a caller can send for itself — and then that -/// caller's own answer and an answer to a request nobody could read land on one hold. A QUOTED -/// one is a caller's identifier and is carried as itself. -#[test] -fn the_empty_identifier_correlates_with_nothing() { - let arena = TestPlaneAlloc; - assert_eq!(correlation_value(b"null", &arena), None); - assert!(correlation_for(b"null", &arena).is_none()); - assert_eq!( - correlation_value(br#""null""#, &arena), - Some(CorrelationValue::Str("null")) - ); -} - -/// The same bytes always give the same value. -#[test] -fn the_value_is_deterministic() { - let arena = TestPlaneAlloc; - for raw in [&b"42"[..], &br#""abc""#[..], &b"null"[..]] { - assert_eq!( - correlation_value(raw, &arena), - correlation_value(raw, &arena) - ); - } -} - -/// The correlation carries the declared key and the identifier's value. -#[test] -fn the_correlation_carries_the_declared_key() { - let arena = TestPlaneAlloc; - let c = correlation_for(b"7", &arena).expect("it is carried"); - assert_eq!(c.fact_key, FACT_RPC_ID); - assert_eq!(c.value, CorrelationValue::Num(7)); -} - -// The metadata keys are spelled the way the codec spells them. -// -// `the_metadata_keys_are_the_codecs_own` IS GONE for the same reason as `jsonrpc`'s code test: -// this module re-exports the codec's two keys rather than restating them, so the assertion read -// `X == X`. The pin that still bites — that each QUOTED needle says the same thing as the name it -// was written from — is below, because those two really are two spellings. - -/// Each quoted needle is its own key, in quotes, and nothing else. -/// -/// The quoted forms exist so no request has to build one. That is only safe while they say the -/// same thing as the names they were written from, and this is what says so: change one -/// spelling without the other and this test names the pair that disagree. -#[test] -fn the_quoted_needles_are_the_keys_in_quotes() { - for (name, quoted) in [ - ( - super::META_PROTOCOL_VERSION, - super::META_PROTOCOL_VERSION_QUOTED, - ), - ( - super::META_PROGRESS_TOKEN, - super::META_PROGRESS_TOKEN_QUOTED, - ), - ] { - assert_eq!( - quoted, - format!("\"{name}\"").as_bytes(), - "the quoted needle for {name} is not that key in quotes" - ); - } -} diff --git a/crates/busbar-plane-mcp/src/tests/framing.rs b/crates/busbar-plane-mcp/src/tests/framing.rs deleted file mode 100644 index 3a7d40bfff..0000000000 --- a/crates/busbar-plane-mcp/src/tests/framing.rs +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -use serde_json::{json, Value}; - -use super::*; - -fn events(bytes: &[u8]) -> Vec { - String::from_utf8_lossy(bytes) - .lines() - .filter_map(|l| l.strip_prefix("data: ")) - .map(|d| serde_json::from_str(d).expect("each event is one JSON document")) - .collect() -} - -#[test] -fn the_ordinary_accept_prefers_json() { - assert!(!prefers_event_stream(Some( - "application/json, text/event-stream" - ))); - assert!(!prefers_event_stream(None)); - assert!(!prefers_event_stream(Some("application/json"))); -} - -#[test] -fn a_stream_first_or_alone_is_preferred() { - assert!(prefers_event_stream(Some( - "text/event-stream, application/json" - ))); - assert!(prefers_event_stream(Some("text/event-stream"))); -} - -#[test] -fn a_q_value_decides_before_the_written_order() { - assert!(prefers_event_stream(Some( - "application/json;q=0.5, text/event-stream" - ))); - assert!(!prefers_event_stream(Some( - "text/event-stream;q=0.4, application/json" - ))); - assert!(!prefers_event_stream(Some("text/event-stream;q=0"))); -} - -#[test] -fn the_level_is_the_callers_or_the_default() { - assert_eq!(requested_level(None), DEFAULT_LEVEL); - let meta = json!({ META_LOGGING_LEVEL: "debug" }); - assert_eq!(requested_level(Some(&meta)), "debug"); - let meta = json!({ META_LOGGING_LEVEL: "loud" }); - assert_eq!(requested_level(Some(&meta)), DEFAULT_LEVEL); - assert!(level_allows("info", "warning")); - assert!(!level_allows("info", "debug")); - assert!(level_allows("info", "unheard-of")); -} - -#[test] -fn a_json_caller_is_not_framed() { - let body = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {"_meta": {}}}); - assert_eq!( - Framing::of(Some("application/json"), "tools/list", &body), - None - ); -} - -#[test] -fn a_progress_token_asks_for_a_stream_whatever_the_accept() { - let body = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", - "params": {"name": "fs_read", "_meta": {"progressToken": "p"}}}); - let framing = Framing::of(Some("application/json"), "tools/call", &body).expect("framed"); - assert_eq!(framing.target, Some(json!("fs_read"))); - assert_eq!(framing.level, DEFAULT_LEVEL); -} - -#[test] -fn the_stream_carries_progress_then_the_records_then_the_answer() { - let body = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list", - "params": {"_meta": {META_LOGGING_LEVEL: "debug"}}}); - let framing = Framing::of(Some(EVENT_STREAM), "tools/list", &body).expect("framed"); - let answer = br#"{"jsonrpc":"2.0","id":1,"result":{"tools":[]}}"#.to_vec(); - let progress = [json!({"jsonrpc": "2.0", "method": "notifications/progress"})]; - let (bytes, framed) = framing.frame(200, answer, &progress); - assert!(framed); - let e = events(&bytes); - assert_eq!( - e.len(), - 4, - "one progress frame, two records at debug, the answer" - ); - assert_eq!(e[0]["method"], "notifications/progress"); - assert_eq!(e[1]["params"]["level"], "debug"); - assert_eq!(e[2]["params"]["data"]["message"], "MCP method completed"); - assert_eq!(e[3]["id"], 1); -} - -#[test] -fn the_default_level_drops_the_debug_record() { - let body = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {"_meta": {}}}); - let framing = Framing::of(Some(EVENT_STREAM), "tools/list", &body).expect("framed"); - let (bytes, _) = framing.frame( - 200, - br#"{"jsonrpc":"2.0","id":1,"result":{}}"#.to_vec(), - &[], - ); - let e = events(&bytes); - assert_eq!(e.len(), 2); - assert_eq!(e[0]["params"]["level"], "info"); -} - -#[test] -fn a_refusal_or_an_unreadable_body_goes_out_as_it_was() { - let body = json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {"_meta": {}}}); - let framing = Framing::of(Some(EVENT_STREAM), "tools/list", &body).expect("framed"); - let refused = br#"{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"no"}}"#.to_vec(); - assert_eq!(framing.frame(403, refused.clone(), &[]), (refused, false)); - assert_eq!( - framing.frame(200, b"not json".to_vec(), &[]), - (b"not json".to_vec(), false) - ); -} diff --git a/crates/busbar-plane-mcp/src/tests/identity_tests.rs b/crates/busbar-plane-mcp/src/tests/identity_tests.rs deleted file mode 100644 index b455f0cb1e..0000000000 --- a/crates/busbar-plane-mcp/src/tests/identity_tests.rs +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! `{server}_{tool}` as the routing key: the separator ambiguity, and the round trip. - -use crate::identity::{NameError, ServerId, ToolKey}; - -#[test] -fn a_server_id_containing_the_separator_is_refused() { - let err = ServerId::new("file_system").expect_err("`_` in a server id must be refused"); - assert_eq!(err, NameError::ServerIdHasSeparator("file_system".into())); - // The message has to say WHY, because an operator who reads "invalid" renames it to - // `file__system` and reintroduces the ambiguity. - let msg = err.to_string(); - assert!( - msg.contains("mcp_tool"), - "message must name the grant kind the ambiguity would corrupt: {msg}" - ); - assert!( - msg.contains("Use `-`"), - "message must say what to type instead: {msg}" - ); -} - -/// THE COLLISION THIS RULE EXISTS FOR. Without the refusal above, these two DIFFERENT tools render -/// the same namespaced name, and that name is the value of an `mcp_tool` grant. -#[test] -fn the_separator_rule_is_what_makes_two_different_tools_render_differently() { - // `a_b` + `c` is unrepresentable: the server id is refused. - assert!(ServerId::new("a_b").is_err()); - // So the only representable spelling of `a_b_c` is the one below, and parsing it recovers - // exactly that pair rather than guessing between two readings. - let parsed = ToolKey::parse("a_b_c").expect("`a` + `b_c` is representable"); - assert_eq!(parsed.server().as_str(), "a"); - assert_eq!(parsed.tool(), "b_c"); -} - -#[test] -fn a_tool_name_may_contain_the_separator_because_real_tools_do() { - let k = ToolKey::new(ServerId::new("filesystem").unwrap(), "read_file").unwrap(); - assert_eq!(k.namespaced(), "filesystem_read_file"); - assert_eq!(ToolKey::parse("filesystem_read_file").unwrap(), k); -} - -#[test] -fn namespaced_and_display_are_the_same_string() { - // Two renderings of one value is one rendering too many when the value is compared for equality - // against a scope grant. - let k = ToolKey::new(ServerId::new("fs").unwrap(), "read").unwrap(); - assert_eq!(k.namespaced(), k.to_string()); -} - -#[test] -fn names_outside_the_legal_set_are_refused_on_both_axes() { - for bad in ["fs server", "fs/server", "fs\"x", "fs\nx", "fs:x"] { - assert!( - matches!(ServerId::new(bad), Err(NameError::IllegalCharacter { .. })), - "server id `{bad}` must be refused" - ); - } - let ok = ServerId::new("fs").unwrap(); - for bad in ["read file", "read/file", "read\"x"] { - assert!( - matches!( - ToolKey::new(ok.clone(), bad), - Err(NameError::IllegalCharacter { .. }) - ), - "tool name `{bad}` must be refused" - ); - } -} - -#[test] -fn empty_names_are_refused() { - assert_eq!( - ServerId::new(""), - Err(NameError::Empty { what: "server id" }) - ); - assert_eq!( - ToolKey::new(ServerId::new("fs").unwrap(), ""), - Err(NameError::Empty { what: "tool name" }) - ); -} - -#[test] -fn a_rendering_with_no_separator_does_not_parse() { - // "fs" alone names a server, not a tool. Guessing "server fs, tool fs" would admit a grant - // nobody wrote. - assert!(ToolKey::parse("fs").is_err()); -} - -/// The parse is TOTAL over what it accepts: every key round-trips, including one whose tool name -/// carries several separators. -#[test] -fn every_constructible_key_round_trips() { - let cases = [ - ("fs", "read"), - ("fs", "read_file"), - ("fs", "a_b_c_d"), - ("my-server.v2", "x"), - ]; - // A floor on the discovered set: an empty loop here would pass vacuously. - assert_eq!(cases.len(), 4); - for (s, t) in cases { - let k = ToolKey::new(ServerId::new(s).unwrap(), t).unwrap(); - assert_eq!( - ToolKey::parse(&k.namespaced()).unwrap(), - k, - "round trip {s}/{t}" - ); - } -} diff --git a/crates/busbar-plane-mcp/src/tests/jsonrpc.rs b/crates/busbar-plane-mcp/src/tests/jsonrpc.rs deleted file mode 100644 index f52732bd43..0000000000 --- a/crates/busbar-plane-mcp/src/tests/jsonrpc.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! Tests for `jsonrpc.rs`. Lifted out of the implementation file so its line count -//! measures implementation and nothing else; still a direct child module, so `use -//! super::*` reaches the private items it always did. - -use super::{ - error, id_shape, id_value, read, success, IdShape, CODES, RESULT_TYPE_COMPLETE, RETIRED_CODES, -}; -use busbar_contract::wire::Decode; - -/// The serializer writes members in sorted order, and every byte-identity claim rests on it. -#[test] -fn the_serializer_writes_members_in_sorted_order() { - let v = serde_json::json!({ "jsonrpc": "2.0", "id": 1, "result": {} }); - assert_eq!( - serde_json::to_string(&v).unwrap(), - r#"{"id":1,"jsonrpc":"2.0","result":{}}"# - ); -} - -/// A well-formed request reads as a request. -#[test] -fn a_request_reads_as_a_request() { - let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search"}}"#; - let e = read(body).expect("a well-formed request reads"); - assert_eq!(e.method_str(body), Some("tools/call")); - assert_eq!(e.id_bytes(body), Some(&b"1"[..])); - assert!(e.is_request()); -} - -/// A request with no identifier is a notification and obliges no answer. -#[test] -fn an_absent_identifier_is_a_notification() { - let body = br#"{"jsonrpc":"2.0","method":"notifications/roots/list_changed"}"#; - let e = read(body).expect("a notification reads"); - assert!(!e.is_request()); -} - -/// An empty identifier is refused rather than read as absent. -#[test] -fn an_empty_identifier_is_refused() { - let body = br#"{"jsonrpc":"2.0","id":null,"method":"tools/list"}"#; - assert_eq!(read(body), Err(Decode::Malformed)); -} - -/// An identifier of any other shape is refused. -#[test] -fn an_identifier_of_another_shape_is_refused() { - for body in [ - &br#"{"jsonrpc":"2.0","id":true,"method":"x"}"#[..], - &br#"{"jsonrpc":"2.0","id":[1],"method":"x"}"#[..], - &br#"{"jsonrpc":"2.0","id":{"a":1},"method":"x"}"#[..], - ] { - assert_eq!(read(body), Err(Decode::Malformed), "{body:?} was admitted"); - } -} - -/// The wrong version, or none, is not this protocol. -#[test] -fn the_wrong_version_is_not_this_protocol() { - assert_eq!( - read(br#"{"jsonrpc":"1.0","id":1,"method":"x"}"#), - Err(Decode::Malformed) - ); - assert_eq!( - read(br#"{"id":1,"method":"x"}"#), - Err(Decode::MissingDeclaredFact) - ); -} - -/// The identifier-shape test admits exactly the two shapes and nothing else. -#[test] -fn the_identifier_shapes_are_two() { - assert_eq!(id_shape(br#""a""#), Some(IdShape::Str)); - assert_eq!(id_shape(b"1"), Some(IdShape::Number)); - assert_eq!(id_shape(b"null"), None); - assert_eq!(id_shape(b"true"), None); - assert_eq!(id_shape(b""), None); -} - -/// Every identifier the reader ADMITS is one an answer can echo. -/// -/// The two ends read the same bytes: the reader decides whether a request is admissible, and the -/// answer writes the identifier back by parsing those same bytes into a value. A request the -/// reader admits and the writer cannot echo is a caller who is told nothing at all — not an -/// answer, not a refusal — because both replies encode through the same step. The rows below are -/// the ones the old reader admitted: each merely STARTS like a number. -#[test] -fn every_admitted_identifier_can_be_echoed() { - for raw in [ - &b"1"[..], - &b"0"[..], - &b"-1"[..], - &b"1.5"[..], - &b"1e3"[..], - &b"1E+3"[..], - &b"1.2.3"[..], - &b"007"[..], - &b"1e"[..], - &b"1e+"[..], - &b"--1"[..], - &b"-"[..], - &b"1-2"[..], - &br#""a""#[..], - ] { - if id_shape(raw).is_some() { - assert!( - id_value(raw).is_ok(), - "{raw:?} is admitted as an identifier and cannot be written back" - ); - } - } -} - -/// The identifiers that merely START like a number are refused as identifiers. -#[test] -fn a_run_of_bytes_that_only_starts_like_a_number_is_not_one() { - for raw in [ - &b"1.2.3"[..], - &b"007"[..], - &b"1e"[..], - &b"1e+"[..], - &b"--1"[..], - &b"-"[..], - &b"1-2"[..], - &b"1."[..], - ] { - assert_eq!(id_shape(raw), None, "{raw:?} was read as a number"); - } - // And the shapes that ARE numbers stay admitted. - for raw in [ - &b"0"[..], - &b"-0"[..], - &b"42"[..], - &b"1.5"[..], - &b"-1e-3"[..], - ] { - assert_eq!( - id_shape(raw), - Some(IdShape::Number), - "{raw:?} is a number and was refused" - ); - } -} - -/// A successful answer carries the discriminator and the caller's identifier. -#[test] -fn a_successful_answer_carries_the_discriminator() { - let id = id_value(b"1").expect("a number is a value"); - let bytes = - success(Some(&id), br#"{"tools":[]}"#, RESULT_TYPE_COMPLETE).expect("the result writes"); - assert_eq!( - core::str::from_utf8(&bytes).unwrap(), - r#"{"id":1,"jsonrpc":"2.0","result":{"resultType":"complete","tools":[]}}"# - ); -} - -/// A discriminator a server put on its own result is REPLACED, never passed through. -/// -/// This is the laundering the three-constructor shape exists to prevent, asserted rather than -/// described: a server's demand for the caller's authority does not leave here wearing this -/// node's name. -#[test] -fn a_servers_own_discriminator_is_replaced() { - let id = id_value(b"1").expect("a number is a value"); - let bytes = success( - Some(&id), - br#"{"resultType":"input_required","content":[]}"#, - RESULT_TYPE_COMPLETE, - ) - .expect("the result writes"); - let value: serde_json::Value = serde_json::from_slice(&bytes).expect("it is a document"); - assert_eq!(value["result"]["resultType"], "complete"); -} - -/// On a successful answer with no identifier, the member is omitted. -#[test] -fn a_successful_answer_omits_an_absent_identifier() { - let bytes = success(None, b"{}", RESULT_TYPE_COMPLETE).expect("the result writes"); - assert_eq!( - core::str::from_utf8(&bytes).unwrap(), - r#"{"jsonrpc":"2.0","result":{"resultType":"complete"}}"# - ); -} - -/// On an error the member is always written, and it is empty when there is none. -/// -/// A peer's own test for "is this a response" is whether the member is present at all, so an -/// omitted one would be classified as neither a response nor a notification. -#[test] -fn an_error_always_writes_the_identifier() { - let bytes = error(None, super::CODE_INVALID_REQUEST, "bad", None).expect("it writes"); - assert_eq!( - core::str::from_utf8(&bytes).unwrap(), - r#"{"error":{"code":-32600,"message":"bad"},"id":null,"jsonrpc":"2.0"}"# - ); -} - -/// An error with detail carries it under the member the shared builder uses. -#[test] -fn an_error_carries_its_detail() { - let id = id_value(b"4").expect("a number is a value"); - let bytes = error( - Some(&id), - super::CODE_MISSING_CLIENT_CAPABILITY, - "no capability", - Some(serde_json::json!({ "requiredCapabilities": ["sampling"] })), - ) - .expect("it writes"); - assert_eq!( - core::str::from_utf8(&bytes).unwrap(), - r#"{"error":{"code":-32021,"data":{"requiredCapabilities":["sampling"]},"message":"no capability"},"id":4,"jsonrpc":"2.0"}"# - ); -} - -// `every_code_is_the_codecs_own` IS GONE, AND SO IS THE QUESTION IT ASKED. It compared this -// module's ten codes against the codec's table. The two are one table now (`jsonrpc` re-exports -// `codec`'s), so the loop read `CODES ⊆ CODES` and could not fail for any edit anyone could make. -// A test that cannot go red is not evidence; the compiler holds this one. The SET question that is -// still real — that none of the retired codes is writable — is below and is untouched. - -/// This plane writes none of the codes the current revision retired. -#[test] -fn no_retired_code_is_writable() { - for retired in RETIRED_CODES { - assert!( - !CODES.contains(retired), - "the plane can write the retired code {retired}" - ); - } -} - -/// The reader is deterministic over the same bytes. -#[test] -fn the_reader_is_deterministic() { - let body = br#"{"jsonrpc":"2.0","id":"x","method":"tools/list","params":{}}"#; - assert_eq!(read(body), read(body)); -} diff --git a/crates/busbar-plane-mcp/src/tests/line_tests.rs b/crates/busbar-plane-mcp/src/tests/line_tests.rs deleted file mode 100644 index 5d083b47ee..0000000000 --- a/crates/busbar-plane-mcp/src/tests/line_tests.rs +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! The line carrier's meanings: the fields a body implies, the stdio-era verbs, busbar's own asks -//! issued in order and their retry, and what a line that answers busbar is. - -use serde_json::json; - -use super::*; - -#[test] -fn the_stdio_era_verbs_are_answered_here_and_others_go_to_the_dispatch() { - let init = json!({ "jsonrpc": "2.0", "id": 7, "method": "initialize", "params": {} }); - let Era::Answer(answer) = era(&init) else { - panic!("initialize is answered here"); - }; - assert_eq!(answer["id"], 7); - assert_eq!(answer["result"]["protocolVersion"], PROTOCOL_VERSION); - let ping = json!({ "jsonrpc": "2.0", "id": "p", "method": "ping" }); - assert_eq!(era(&ping), Era::Answer(result(&json!("p"), json!({})))); - for (method, params) in [ - ("logging/setLevel", json!({ "level": "info" })), - ("logging/setLevel", json!({})), - ("resources/subscribe", json!({ "uri": "file:///a" })), - ("resources/unsubscribe", json!({ "uri": "file:///a" })), - ] { - let req = json!({ "jsonrpc": "2.0", "id": 1, "method": method, "params": params }); - let Era::Answer(refusal) = era(&req) else { - panic!("{method} is refused here, never dispatched or acknowledged"); - }; - assert_eq!(refusal["error"]["code"], -32601, "{method}"); - assert!( - refusal.get("result").is_none(), - "{method} is not acknowledged" - ); - } - let list = json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {} }); - assert_eq!(era(&list), Era::Dispatch); - // A notification is not a stdio-era request. - let note = json!({ "jsonrpc": "2.0", "method": "ping" }); - assert_eq!(era(¬e), Era::Dispatch); -} - -#[test] -fn a_line_answers_busbar_only_under_an_id_busbar_minted() { - let answer = json!({ "jsonrpc": "2.0", "id": "busbar:4", "result": { "action": "accept" } }); - assert_eq!( - reply(&answer), - Some(Reply::Answered(4, json!({ "action": "accept" }))) - ); - let error = - json!({ "jsonrpc": "2.0", "id": "busbar:5", "error": { "code": -1, "message": "no" } }); - assert_eq!(reply(&error), Some(Reply::Failed(5))); - let theirs = json!({ "jsonrpc": "2.0", "id": 4, "result": {} }); - assert_eq!( - reply(&theirs), - None, - "an id busbar did not mint answers nothing of busbar's" - ); - let request = json!({ "jsonrpc": "2.0", "id": "busbar:4", "method": "ping" }); - assert_eq!(reply(&request), None, "a request is not an answer"); - let oob = json!({ - "jsonrpc": "2.0", "method": "notifications/elicitation/response", - "params": { "requestId": "busbar:9", "response": { "action": "decline" } } - }); - assert_eq!( - reply(&oob), - Some(Reply::Answered(9, json!({ "action": "decline" }))) - ); - let cancel = json!({ "jsonrpc": "2.0", "method": "notifications/cancelled", "params": { "requestId": 3 } }); - assert_eq!(cancelled(&cancel), Some(json!(3))); - assert_ne!(id_key(&json!("1")), id_key(&json!(1))); -} - -#[test] -fn a_live_round_issues_its_asks_in_order_and_its_retry_carries_the_answers_and_the_state() { - let original = json!({ - "jsonrpc": "2.0", "id": "call-0", "method": "prompts/get", - "params": { "name": "ws_greet", "_meta": {} } - }); - let result = json!({ - "resultType": "input_required", - "inputRequests": { - "a": { "method": "elicitation/create", "params": { "message": "first" } }, - "b": { "method": "roots/list", "params": {} } - }, - "requestState": "sealed" - }); - let mut ask = LiveAsk::of("p", original, b"{}".to_vec(), &result, 0).expect("a round"); - let first: Value = serde_json::from_slice(&ask.issue(10).expect("the first ask")).unwrap(); - assert_eq!(first["id"], "busbar:10"); - assert_eq!(first["method"], "elicitation/create"); - assert_eq!(ask.current_n, 10); - ask.answered(json!({ "action": "accept" })); - let second: Value = serde_json::from_slice(&ask.issue(11).expect("the second ask")).unwrap(); - assert_eq!(second["method"], "roots/list"); - ask.answered(json!({ "roots": [] })); - assert!(ask.issue(12).is_none(), "the round is spent"); - let retry = ask.retry().expect("the retry"); - assert_eq!( - retry["id"], "call-0", - "the retry's answer is the caller's answer" - ); - assert_eq!(retry["params"]["requestState"], "sealed"); - assert_eq!(retry["params"]["inputResponses"]["a"]["action"], "accept"); - assert_eq!(retry["params"]["inputResponses"]["b"]["roots"], json!([])); - // A result naming no asks, or no state, is no round: it is handed to the caller as it is. - let no_state = - json!({ "resultType": "input_required", "inputRequests": { "a": { "method": "m" } } }); - assert!(LiveAsk::of("p", json!({}), Vec::new(), &no_state, 0).is_none()); -} - -#[test] -fn initialize_advertises_neither_subscribe_nor_logging() { - let answer = initialize_result(&json!(1)); - let caps = &answer["result"]["capabilities"]; - assert!(caps["resources"].get("subscribe").is_none()); - assert!(caps.get("logging").is_none()); -} - -#[test] -fn a_live_ask_lapses_at_its_timeout_and_a_request_is_livened_a_bounded_number_of_times() { - let result = json!({ - "resultType": "input_required", - "inputRequests": { "a": { "method": "roots/list", "params": {} } }, - "requestState": "sealed" - }); - let mut ask = LiveAsk::of("p", json!({}), b"{}".to_vec(), &result, 0).expect("a round"); - ask.issue(1).expect("the ask"); - assert!(!ask.lapsed(u64::MAX), "an ask never armed has no deadline"); - ask.arm(1_000); - assert!(!ask.lapsed(1_000 + ASK_TIMEOUT_NS - 1)); - assert!(ask.lapsed(1_000 + ASK_TIMEOUT_NS)); - assert!(may_liven(MAX_LIVE_ASK_ROUNDS - 1)); - assert!(!may_liven(MAX_LIVE_ASK_ROUNDS)); -} - -/// `initialize` naming a session revision this plane carries is answered in it (THE DESIGN section 2, -/// the mcp bullet: revision by negotiation), and the carrier still declares neither `subscribe` nor -/// `logging` in it; naming none, or the stateless one, keeps the stateless answer. -#[test] -fn initialize_negotiates_a_session_revision() { - let init = json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18" } }); - let Era::Opened(revision, answer) = era(&init) else { - panic!("a session revision is opened"); - }; - assert_eq!(revision, crate::revision::Revision::R2025_06_18); - assert_eq!(answer["result"]["protocolVersion"], "2025-06-18"); - let caps = &answer["result"]["capabilities"]; - assert!(caps["resources"].get("subscribe").is_none()); - assert!(caps.get("logging").is_none()); - let stateless = json!({ "jsonrpc": "2.0", "id": 2, "method": "initialize", "params": { "protocolVersion": "2026-07-28" } }); - assert_eq!(era(&stateless), Era::Answer(initialize_result(&json!(2)))); -} diff --git a/crates/busbar-plane-mcp/src/tests/meta.rs b/crates/busbar-plane-mcp/src/tests/meta.rs deleted file mode 100644 index 46b1c43450..0000000000 --- a/crates/busbar-plane-mcp/src/tests/meta.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Tests for `meta.rs`. Lifted out of the implementation file so its line count -//! measures implementation and nothing else; still a direct child module, so `use -//! super::*` reaches the private items it always did. - -use super::{CONFIG_SCHEMA, INTROSPECTION_VERBS, METER_CLASSES}; -use crate::McpPlane; -use busbar_contract::ids::MeterClassId; -use busbar_contract::plane::PlaneMeta; - -/// The registry key is the codec's own. -#[test] -fn the_key_is_the_codecs_own() { - assert_eq!(McpPlane::KEY, crate::PLANE_KEY); -} - -/// The design's plane table gives this protocol exactly these two classes. -#[test] -fn the_declared_classes_are_the_two() { - let keys: Vec<&str> = METER_CLASSES.iter().map(|c| c.key.as_str()).collect(); - assert_eq!(keys, vec!["tool_calls", "bytes"]); -} - -/// No kernel-reserved class is declared here. -#[test] -fn no_kernel_reserved_class_is_declared() { - for reserved in ["requests", "fee", "count", "session_seconds", "tokens"] { - assert!( - !METER_CLASSES - .iter() - .any(|c| c.key == MeterClassId::new(reserved)), - "the plane declares the kernel's own class {reserved}" - ); - } -} - -/// Every declared class carries a family and a divisor that can size a hold. -#[test] -fn every_class_can_size_a_hold() { - for class in METER_CLASSES { - assert!(!class.family.is_empty(), "{} has no family", class.key); - assert!(class.default_divisor > 0, "{} divides by zero", class.key); - } -} - -/// The two classes are in different families, so a cap over one does not count the other. -#[test] -fn the_two_classes_do_not_share_a_family() { - assert_ne!(METER_CLASSES[0].family, METER_CLASSES[1].family); -} - -/// The declared verb is a read-only projection and no more. -#[test] -fn the_verbs_are_read_only() { - for mutating in ["connect", "approve", "install", "promote"] { - assert!( - !INTROSPECTION_VERBS.iter().any(|v| v.as_str() == mutating), - "the plane declares the mutating verb {mutating}" - ); - } -} - -/// The configuration schema is a document, and it names the member the codec requires. -#[test] -fn the_configuration_schema_is_a_document() { - let parsed: serde_json::Value = - serde_json::from_str(CONFIG_SCHEMA).expect("the schema is a document"); - assert!(parsed["properties"].get("canonical_uri").is_some()); - assert_eq!(parsed["required"], serde_json::json!(["canonical_uri"])); -} - -/// Every declared operation class is named by at least one method of the vocabulary. -#[test] -fn every_class_is_reachable_from_the_vocabulary() { - for op in McpPlane::OP_CLASSES { - let from_a_method = crate::tool_ops::METHODS.iter().any(|m| m.op == *op); - let from_a_notice = *op == crate::tool_ops::OP_NOTIFICATION; - assert!( - from_a_method || from_a_notice, - "{op} is declared but nothing produces it" - ); - } -} - -/// Every declared list is a SET: nothing declared twice, nothing declared empty. -/// -/// This used to compare each list to itself, which is true of any two reads of anything and so -/// could not fail. What is worth asserting is what the registry needs to be true of a list it -/// seals at boot: a repeated key would be one declaration silently standing for two, and an -/// empty key would be a class, schema or fact nothing can name. -#[test] -fn every_declared_list_is_a_set() { - fn no_repeats(what: &str, items: &[T]) { - for (i, item) in items.iter().enumerate() { - assert!(!items[..i].contains(item), "{what} declares {item:?} twice"); - } - } - no_repeats("the class list", McpPlane::OP_CLASSES); - no_repeats("the record list", McpPlane::RECORD_SCHEMAS); - no_repeats("the session-fact list", McpPlane::SESSION_FACTS); - no_repeats("the content-fact list", McpPlane::CONTENT_FACTS); - no_repeats("the verb list", McpPlane::INTROSPECTION_VERBS); - no_repeats( - "the meter-class list", - &METER_CLASSES.iter().map(|c| c.key).collect::>(), - ); - for key in McpPlane::SESSION_FACTS - .iter() - .chain(McpPlane::CONTENT_FACTS) - { - assert!(!key.is_empty(), "a fact key is declared as the empty name"); - } - for op in McpPlane::OP_CLASSES { - assert!( - !op.as_str().is_empty(), - "a class is declared as the empty name" - ); - } - assert!( - !McpPlane::CLAIMS.is_empty(), - "a plane that claims nothing takes no bytes" - ); -} - -/// The `--help` lines this plane owns open with the flag it declares, in the help's own two-column -/// shape, and end without a newline — the root splices them between two lines it owns. -#[test] -fn the_help_flags_open_with_the_declared_flag() { - let first = super::HELP_FLAGS - .lines() - .next() - .expect("the help lines are not empty"); - assert!( - first.starts_with(&format!(" {} ", super::STDIO_SERVE_FLAG)), - "{first}" - ); - assert!(!super::HELP_FLAGS.ends_with('\n')); -} diff --git a/crates/busbar-plane-mcp/src/tests/ops.rs b/crates/busbar-plane-mcp/src/tests/ops.rs deleted file mode 100644 index 49ddadf723..0000000000 --- a/crates/busbar-plane-mcp/src/tests/ops.rs +++ /dev/null @@ -1,138 +0,0 @@ -//! Tests for `ops.rs`. Lifted out of the implementation file so its line count -//! measures implementation and nothing else; still a direct child module, so `use -//! super::*` reaches the private items it always did. - -use super::{is_known_notification, method_row_for, Sender, METHODS, NOTIFICATIONS, OP_CLASSES}; - -/// Every method maps to a class the plane declares. -#[test] -fn every_method_names_a_declared_class() { - for row in METHODS { - assert!( - OP_CLASSES.contains(&row.op), - "method {} names an undeclared class {}", - row.method, - row.op - ); - } -} - -/// No method name is listed twice, so a lookup has one answer. -#[test] -fn no_method_name_is_repeated() { - for (i, row) in METHODS.iter().enumerate() { - assert!( - !METHODS[..i].iter().any(|r| r.method == row.method), - "method {} is listed twice", - row.method - ); - } -} - -/// No class is produced by two different methods. -/// -/// One class per method here, deliberately: this protocol spells each operation exactly one way, -/// so two methods sharing a class would mean one of them was mis-filed. -#[test] -fn no_class_is_produced_twice() { - for (i, row) in METHODS.iter().enumerate() { - assert!( - !METHODS[..i].iter().any(|r| r.op == row.op), - "class {} is produced by two methods", - row.op - ); - } -} - -/// The lookup is total over the table and answers nothing else. -#[test] -fn the_lookup_answers_the_table_and_nothing_else() { - for row in METHODS { - assert_eq!(method_row_for(row.method), Some(row)); - } - assert_eq!(method_row_for("this/method/does/not/exist"), None); - assert_eq!(method_row_for(""), None); -} - -/// A notification is not a method, and a method is not a notification. -/// -/// The two lists must not overlap: a name in both would be answered and not answered at once. -#[test] -fn the_two_lists_do_not_overlap() { - for name in NOTIFICATIONS { - assert!(method_row_for(name).is_none(), "{name} is in both lists"); - } - for row in METHODS { - assert!( - !is_known_notification(row.method), - "{} is in both lists", - row.method - ); - } -} - -/// Every method the codec's own dispatch table names is one this plane carries. -/// -/// The table itself, iterated. This SCRAPED it out of the server half's source once — an -/// `include_str!` over `../../busbar-mcp/src/…` followed by a hand-rolled parse for quoted -/// pieces containing a slash — which coupled this crate to a sibling its manifest does not name -/// and could only ever see what the parse happened to catch. The table is the codec's now, so -/// the assertion is over the values themselves. A method the codec dispatches and this plane -/// does not carry would arrive here as an unsupported operation. -#[test] -fn every_dispatched_method_is_carried() { - let mut seen = 0usize; - for method in crate::codec::IMPLEMENTED_METHODS { - if method.contains('/') { - assert!( - method_row_for(method).is_some(), - "the codec dispatches {method} and this plane does not carry it" - ); - seen += 1; - } - } - assert!( - seen >= 12, - "only {seen} methods were read out of the codec's table" - ); -} - -/// The name pointer is the codec's own reading of where a request's subject is. -/// -/// The codec answers the same question in a small function; this CALLS it and asserts the two -/// agree, member for member, in BOTH directions. It read that function's source once — an -/// `include_str!` over `../../busbar-mcp/src/…`, then a substring search for the arm — which -/// coupled this crate to a sibling its manifest does not name, and which could only ever check -/// one direction: a method the codec addressed and this plane gave no pointer for read as a -/// pass, because the loop never visited it. -#[test] -fn the_name_pointers_are_the_codecs_own() { - for row in METHODS { - let member = row.name_pointer.map(|pointer| { - pointer - .rsplit('/') - .next() - .expect("a pointer has a last segment") - }); - assert_eq!( - crate::codec::name_source_of(row.method), - member, - "the codec and this plane disagree about where {}'s subject is", - row.method - ); - } -} - -/// The three an upstream sends back are the three the plane calls provider-initiated. -#[test] -fn the_provider_methods_are_the_three() { - let provider: Vec<&str> = METHODS - .iter() - .filter(|r| r.sender == Sender::Provider) - .map(|r| r.method) - .collect(); - assert_eq!( - provider, - vec!["sampling/createMessage", "roots/list", "elicitation/create"] - ); -} diff --git a/crates/busbar-plane-mcp/src/tests/reads.rs b/crates/busbar-plane-mcp/src/tests/reads.rs deleted file mode 100644 index 6857a6e4f9..0000000000 --- a/crates/busbar-plane-mcp/src/tests/reads.rs +++ /dev/null @@ -1,176 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright (C) 2026 Busbar Inc and contributors - -//! `prompts/get` and `resources/read` answer from the section as the served engine does. - -use serde_json::{json, Value}; - -use super::*; - -const SECTION: &[u8] = br#"{ - "fs": { - "url": "https://mcp.example/fs", - "pin": {"mechanism": "unpinned"}, - "prompts_allow": { - "greet": {"description": "Greets.", "template": "Hello {who} {missing}"}, - "typed": {"messages": [ - {"role": "assistant", "content": {"type": "text", "text": "Hi {who}"}}, - {"content": {"type": "image", "data": "aGk=", "mime_type": "image/png"}} - ]} - }, - "resources_allow": {"file:///shared": {"text": "fs copy"}}, - "resource_templates_allow": {"file:///logs/{date}": {"text": "log for {date}", "mime_type": "text/plain"}} - }, - "db": { - "url": "https://mcp.example/db", - "pin": {"mechanism": "unpinned"}, - "resources_allow": {"file:///shared": {"blob": "YmluYXJ5"}}, - "resource_templates_allow": {"file:///logs/{day}": {"text": "db log {day}"}} - } -}"#; - -fn catalogue() -> Catalogue { - Catalogue::build( - 1, - &crate::door::read_tools_section(SECTION).expect("the section reads"), - ) -} - -fn everyone(_: &str, _: &str) -> bool { - true -} - -fn only_fs(kind: &str, name: &str) -> bool { - (kind == "mcp_server" && name == "fs") || (kind == "mcp_tool" && name.starts_with("fs_")) -} - -fn read(bytes: Vec) -> Value { - serde_json::from_slice(&bytes).expect("a document") -} - -#[test] -fn a_template_substitutes_once_then_normalises_and_keeps_unknown_placeholders() { - let c = catalogue(); - let params = json!({"name": "fs_greet", "arguments": {"who": "{missing}"}}); - let prompt = prompt_named(&c, &json!(1), Some(¶ms), &everyone).expect("found"); - let v = read(prompts_get(prompt, &json!(1), Some(¶ms))); - assert_eq!(v["result"]["resultType"], "complete"); - assert_eq!(v["result"]["description"], "Greets."); - let text = v["result"]["messages"][0]["content"]["text"] - .as_str() - .expect("text"); - assert!(text.starts_with("Hello {missing}"), "{text}"); - assert!(!text.contains("