Skip to main content

protocol_droid/domain/
mod.rs

1//! Domain layer: value objects, aggregates and pure functions.
2//!
3//! This layer has zero I/O dependencies. Time, randomness and session-id
4//! generation are injected from outside it rather than called globally, so
5//! every function here is deterministic and testable.
6//!
7//! Populated by T04 (protocol version value objects), T05-T08 (feature profile,
8//! negotiation, canonical IR, per-revision codecs), T10 (the session aggregate:
9//! lifecycle, in-flight requests, cancellation, progress) and T11 (the registry's
10//! pure half: primitives, visibility, snapshots).
11
12pub mod bridge;
13mod capability;
14// T27. The A2A domain value objects: version negotiation, the
15// per-revision feature profile, the `AgentCard`, and the well-known
16// path. Gated on the `a2a` feature because the `url` dep is
17// unconditional under the gate (only the A2A agent card uses it
18// with the `serde` feature on) and a consumer that never speaks
19// A2A should not pull it in.
20#[cfg(feature = "a2a")]
21pub mod a2a;
22// T29. The ACP (Agent Client Protocol) domain value objects:
23// version, method table, per-feature profile. The schema and the
24// types in this module are pure data; gating is on the experimental
25// flag because the spec is pre-1.0 (`schema-v1.25.0`) and the
26// task explicitly excludes the module from semver guarantees. The
27// schema is data and stays compiled in a lean build; the *types*
28// are gated.
29#[cfg(feature = "acp-experimental")]
30pub mod acp;
31pub mod feature;
32pub mod idempotency;
33pub mod ir;
34pub mod jsonrpc;
35pub mod mcp;
36mod negotiation;
37// OAuth's own vocabulary, behind the feature that pulls its dependencies. The
38// authorization *model* in `mcp::authorization` is ungated because it is pure
39// data every consumer needs to read a revision's requirements; this is gated
40// because it pulls a SHA-256 implementation and a base64url codec, which a
41// lean build has no other reason to link.
42#[cfg(feature = "oauth")]
43pub mod oauth;
44mod protocol_version;
45pub mod registry;
46mod release_date;
47mod request_id;
48pub mod session;
49mod stream;
50mod supported_versions;
51mod transport;
52
53pub use bridge::{Agreement, Leg, RefusedBy, Withheld, agree};
54pub use capability::{
55    CAPABILITY_TABLE, CLIENT_CAPABILITY_WIRE, Capability, CapabilitySet, SERVER_CAPABILITY_WIRE,
56    Side, defined_in,
57};
58pub use feature::{
59    DegradePolicy, FEATURES, Feature, FeatureProfile, FeatureSpec, Negotiation, resolve,
60};
61pub use idempotency::{
62    Claim, ClaimToken, CompletedResults, IdempotencyError, IdempotencyKey, StaleClaim,
63};
64pub use mcp::{
65    AuthorizationModel, BearerMethod, ClientRegistration, MCP_METHOD_HEADER, MCP_NAME_HEADER,
66    McpSessionId, McpSessionIdError, ResourceMetadata, ResourceUri, ResourceUriError,
67    RoutingHeaderError, RoutingHeaders, RoutingName, RoutingTarget, VISIBLE_ASCII_RANGE,
68    WELL_KNOWN, authorization_model, client_method_defined_in,
69};
70pub use negotiation::{
71    NegotiatedSession, NegotiationError, PeerInfo, ServerDecision, StatelessRequest,
72    assumed_version, assumed_version_without_signal, check_protocol_header, discover_defined_in,
73    handshake_defined_in, is_reserved_meta_key, negotiate_client, negotiate_server,
74    negotiate_stateless, stateless_meta_keys,
75};
76pub use protocol_version::{ParseProtocolVersionError, ProtocolVersion, UnknownVersionError};
77pub use release_date::{ParseReleaseDateError, ReleaseDate};
78pub use request_id::RequestId;
79pub use stream::{
80    EventId, EventName, EventNameError, EventRing, RECOMMENDED_CAPACITY, RecommendedCapacity,
81    Replay, StreamEvent, names,
82};
83pub use supported_versions::{SupportedVersions, SupportedVersionsError};
84pub use transport::{
85    FrameDecoder, FramingError, MaxFrameSize, MaxFrameSizeError, MessageEndpoint,
86    MessageEndpointError, RateLimit, SpecError, TransportKind, TransportSpec, Verdict,
87    check_rate_limit, encode_frame, transport_allowed,
88};
89
90#[cfg(test)]
91mod tests {
92    use crate::domain::{ProtocolVersion, ReleaseDate, TransportKind, transport_allowed};
93
94    /// The three ordered types have to agree with each other, not only with
95    /// themselves.
96    ///
97    /// Each of `ReleaseDate`, `ProtocolVersion` and `TransportKind` carries its
98    /// own `derived_ordering_agrees_in_both_directions`, which is the 1.98
99    /// derive trap — that one trait agrees with the other within a type. This
100    /// is the weaker claim that no test inside a single module can make: the
101    /// three orderings mean the same thing, so a revision's position in
102    /// `ProtocolVersion` matches its `ReleaseDate`, and a transport's position
103    /// matches the revision that introduced it.
104    ///
105    /// The disagreement this catches is not hypothetical. `TransportKind` is
106    /// ordered by introducing revision, and `StreamableHttp` predates
107    /// `StatelessHttp` by two revisions rather than one — a transport list
108    /// sorted by "how new is this" and one sorted by declaration order would
109    /// come out the same here, but only because the declarations were written
110    /// in that order deliberately.
111    #[test]
112    fn the_three_orderings_tell_the_same_story() {
113        // Every version sorts exactly where its date says it does.
114        for version in ProtocolVersion::KNOWN {
115            let date = version.date();
116            for other in ProtocolVersion::KNOWN {
117                assert_eq!(
118                    version < other,
119                    date < other.date(),
120                    "{version} and {other} order differently by version than by date"
121                );
122            }
123        }
124
125        // The known versions are in date order, so `KNOWN` being sorted is a
126        // fact about the calendar and not a habit.
127        assert!(ProtocolVersion::KNOWN.is_sorted());
128        assert!(ReleaseDate::V2024_11_05 < ReleaseDate::V2026_07_28);
129
130        // Every transport was introduced by some known revision, and no
131        // transport predates the first one — which is the fact that makes
132        // `TransportKind::ALL` ordered the same way as the revisions are.
133        let introduced_by = [
134            TransportKind::Stdio,
135            TransportKind::LegacySse,
136            TransportKind::StreamableHttp,
137            TransportKind::StatelessHttp,
138        ];
139        for (transport, first) in introduced_by.iter().zip([
140            ProtocolVersion::V2024_11_05,
141            ProtocolVersion::V2024_11_05,
142            ProtocolVersion::V2025_03_26,
143            ProtocolVersion::V2026_07_28,
144        ]) {
145            assert!(
146                transport_allowed(first, *transport),
147                "{transport} claims to start at {first} but is not allowed there"
148            );
149            for earlier in ProtocolVersion::KNOWN
150                .into_iter()
151                .take_while(|v| *v < first)
152            {
153                assert!(
154                    !transport_allowed(earlier, *transport),
155                    "{transport} is allowed on {earlier}, which predates {first}"
156                );
157            }
158        }
159    }
160}