pub struct PeerSession { /* private fields */ }Expand description
The negotiated connection a handler is serving.
Cheap to clone: a Copy revision, a Copy profile, a Copy session id and
two Arcs. A handler may hand one to a spawned task without the ceremony a
deep type would need.
§Examples
use std::sync::Arc;
use protocol_droid::application::server::context::{
CallbackExchange, ExchangeError, PeerSession,
};
use protocol_droid::domain::feature::{Feature, FeatureProfile};
use protocol_droid::domain::ir::JsonObject;
use protocol_droid::domain::session::SessionId;
use protocol_droid::domain::{ProtocolVersion, TransportKind};
struct NoExchange;
let peer = PeerSession::new(
ProtocolVersion::V2025_03_26,
FeatureProfile::empty().with(Feature::Sampling),
SessionId::new(1),
TransportKind::Stdio,
Arc::new(NoExchange),
Arc::new(NullSink),
);
// Elicitation was never negotiated; sampling was. A handler that asked the
// question up front gets an answer rather than a frame the peer may reject.
assert!(!peer.allows(Feature::Elicitation));
assert!(peer.allows(Feature::Sampling));Implementations§
Source§impl PeerSession
impl PeerSession
Sourcepub fn new(
negotiated: ProtocolVersion,
profile: FeatureProfile,
session: SessionId,
transport: TransportKind,
exchange: Arc<dyn DynExchange>,
sink: Arc<dyn DynFrameSink>,
) -> Self
pub fn new( negotiated: ProtocolVersion, profile: FeatureProfile, session: SessionId, transport: TransportKind, exchange: Arc<dyn DynExchange>, sink: Arc<dyn DynFrameSink>, ) -> Self
Records what a connection negotiated, and where its frames go.
The caller is AuthContext::anonymous, which holds no scopes. That
is a default-deny default and not a convenience: a primitive requiring
a scope is invisible to an anonymous session, and one requiring none is
visible to everybody. with_auth is how a real
principal arrives, so a transport that never calls it serves exactly
the primitives nobody had to authorise.
Sourcepub fn with_log_gate(self, gate: LogGate) -> Self
pub fn with_log_gate(self, gate: LogGate) -> Self
This peer, filtering its log records through gate.
A stateful session keeps the open gate it was built with and moves it
with logging/setLevel. A 2026-07-28 request replaces it with one
built from its own _meta — see LogGate::for_request.
Sourcepub const fn log_gate(&self) -> &LogGate
pub const fn log_gate(&self) -> &LogGate
The threshold this peer’s log records are filtered against.
Sourcepub const fn transport(&self) -> TransportKind
pub const fn transport(&self) -> TransportKind
The transport this session runs over.
Part of the session rather than of the caller because a profile is not
resolvable without one: resolve consults transport_allowed for
every transport-gated feature row, so a handshake that needed the
negotiated profile had nowhere to get the transport from until this
carried it.
Sourcepub const fn auth(&self) -> &AuthContext
pub const fn auth(&self) -> &AuthContext
The caller, for the visibility gate.
Sourcepub const fn with_profile(self, profile: FeatureProfile) -> Self
pub const fn with_profile(self, profile: FeatureProfile) -> Self
Narrows what this session may do.
Separate from the constructor because a profile can be narrowed after
the fact in a way it cannot be widened. The 2026-07-28 stateless
lifecycle is the reason: every request carries its own version and
capabilities in _meta and is served against its own profile, so the
connection settles one profile and each request may hold less.
Narrowing only, deliberately. Widening would let a caller grant a session a feature the negotiation never produced, and that is the “rules-tightening in code” hazard in its most dangerous form: the refusal it defeats is a refusal the peer was never entitled to.
Sourcepub fn with_auth(self, auth: AuthContext) -> Self
pub fn with_auth(self, auth: AuthContext) -> Self
Records who the caller is.
Per request, and the distinction is the one that goes wrong
silently. A bearer token can be refreshed, replaced, or withdrawn
between two requests on one session, so the principal belongs to the
RequestContext being served rather than to the session the peer was
negotiated into. A transport therefore overlays the context it resolved
onto the session peer per request, and what it persists carries the
anonymous default that without_auth restores.
Sourcepub fn without_auth(self) -> Self
pub fn without_auth(self) -> Self
This peer’s session state with the principal dropped.
What a transport stores when it persists a handshake result. The
returned peer keeps the negotiated revision, the profile and the session
id — everything that is the session — and answers
auth with AuthContext::anonymous.
A handshake narrows the context it was handed, so the peer it returns inherits that context’s principal. A transport that overlays the request’s own context per request overwrites it on every request and never observes the difference, which is precisely why it is worth stating rather than leaving to chance: a principal recorded against session state has the wrong lifetime for a token that can be refreshed, replaced, or withdrawn mid-session, and the next reader of the session to skip that overlay would be answered as whoever opened it.
Sourcepub fn with_exchange(self, exchange: Arc<dyn DynExchange>) -> Self
pub fn with_exchange(self, exchange: Arc<dyn DynExchange>) -> Self
This peer, opening server-initiated requests through exchange.
The partner to Self::with_sink: the sink is where a notification
goes, the exchange is how a request leaves and waits. A transport
that publishes both a tool’s log records and its sampling/createMessage
through one session has to bind both to that session, or the log records
reach one client and the sampling request waits for another.
Replaces rather than layers: a peer has one exchange, and keeping a second that silently refuses would make a mis-bound peer refuse rather than fail loudly.
Sourcepub const fn with_session(self, id: SessionId) -> Self
pub const fn with_session(self, id: SessionId) -> Self
This peer, identified as id rather than as the seed it was built from.
A transport that opens more than one session needs this, and the reason
is invisible until two sessions are live. Everything a session does is
keyed on its id: the idempotency store that makes a retried tools/call
replay its recorded answer, the in-flight map that refuses a duplicate.
Both read peer.session(). A peer seeded with one id and never rebind
therefore gives every session the same key space, so the first session’s
recorded tools/call answer is handed back to the second session’s
call as its “already completed” result — another tool’s output, served
as if it were this one’s.
The store allocates the id; this is where it reaches the peer.
Sourcepub fn with_sink(self, sink: Arc<dyn DynFrameSink>) -> Self
pub fn with_sink(self, sink: Arc<dyn DynFrameSink>) -> Self
This peer, publishing its server-initiated messages through sink.
The peer is constructed with a sink long before a transport knows where that session’s messages will be read from — a handshake happens inside a request whose response stream does not exist yet, and a transport that seeds a session with one placeholder and persists it finds later that the placeholder is still what the session writes to. Binding it here, once the session has a tail worth publishing to, is the only point where the two can be made the same object.
Replaces rather than layers: a peer has exactly one sink, and keeping a second that quietly discards would restore the failure this exists to prevent.
Sourcepub const fn negotiated(&self) -> ProtocolVersion
pub const fn negotiated(&self) -> ProtocolVersion
The revision this connection settled on.
Sourcepub const fn profile(&self) -> FeatureProfile
pub const fn profile(&self) -> FeatureProfile
The profile that revision and the peer’s capabilities produced.
Sourcepub const fn exchange(&self) -> &Arc<dyn DynExchange> ⓘ
pub const fn exchange(&self) -> &Arc<dyn DynExchange> ⓘ
The port for a server-initiated round trip.
Sourcepub const fn sink(&self) -> &Arc<dyn DynFrameSink> ⓘ
pub const fn sink(&self) -> &Arc<dyn DynFrameSink> ⓘ
The port for a one-way notification.
Trait Implementations§
Source§impl Clone for PeerSession
impl Clone for PeerSession
Source§impl Debug for PeerSession
impl Debug for PeerSession
Source§fn fmt(&self, f: &mut Formatter<'_>) -> Result
fn fmt(&self, f: &mut Formatter<'_>) -> Result
Prints the negotiation, and deliberately not the ports.
A sink holds a channel and an exchange holds a socket; neither is readable and both are large, so the derived form would print unreadable noise. The two fields a post-mortem wants are the revision and how much the session can do.