1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
//! The stdio transport's byte layer.
//!
//! # What this is
//!
//! Three adapters and a shared error type, and none of them knows what a
//! JSON-RPC message is. [`StdioSource`] turns a byte stream into whole frames,
//! [`StdioSink`] turns documents into framed bytes, and [`Diagnostics`] is the
//! side channel that must never be confused for either. The framing itself is
//! [`crate::domain::FrameDecoder`] and [`crate::domain::encode_frame`] — this
//! module supplies the stream, the ceiling, and the errors, and delegates the
//! rest, so the two halves of the transport cannot disagree about where a
//! frame ends or how long one may be.
//!
//! # The ceiling is one value, not two
//!
//! [`StdioConfig`] is handed to both halves. That is the point: a limit applied
//! only to reads protects the server from a hostile peer but not from a
//! handler that answers with more than the peer will read, and a limit applied
//! only to writes is the same argument with the roles swapped. One
//! [`MaxFrameSize`], both directions, so a reply is refused with a typed error
//! instead of being written as a line the peer's own decoder has to treat as a
//! malformed message.
//!
//! # Why the transport is generic over its stream
//!
//! [`StdioSource<R>`] and [`StdioSink<W>`] are generic rather than hard-wired to
//! a process's stdin and stdout. The transport boundary is where a
//! `tokio::process::Child` becomes a byte stream, and everything below that line
//! is identical whether the bytes arrive from a child, a pipe, a socket, or a
//! test. Naming the concrete stream here would make every one of those cases a
//! second implementation, and the framing logic — the part with the security
//! properties — would be duplicated along with it.
//!
//! # Where the conversation is
//!
//! The three adapters above are a byte layer and stay one. The loop that reads
//! a frame, routes it and answers it is [`serve::ServeLoop`], and the reply
//! document it writes is [`reply`](crate::domain::jsonrpc::reply). Both sit in this module because they are
//! still stdio: the loop's central rule is that this transport's stdout carries
//! MCP messages and nothing else, and that rule is about this process's
//! streams rather than about MCP.
//!
//! # What is deliberately not here
//!
//! No spawning and no lifecycle. The loop runs against whatever byte streams it
//! is given, and the process management that gives it a child's stdin and
//! stdout in the first place is a separate concern. A single unterminated line
//! has no framing error either, because the framing layer cannot know whether
//! the peer is mid-message or has gone away.
pub use crateReplyError;
pub use ;
pub use ;
pub use StdioSink;
pub use StdioSource;
use NonZeroUsize;
use Error;
use crateMaxFrameSize;
/// How many bytes a single read from the peer's stream is allowed to ask for.
///
/// A pipe hands over whatever happens to be in the buffer, so this is an upper
/// bound rather than a promise: a large frame is still assembled from as many
/// reads as it takes. It is sized for a pipe rather than a socket because a
/// pipe read cannot be made to wait for a full buffer, so an 8 KiB ask is
/// satisfied quickly and keeps the syscall count down without holding a frame's
/// worth of memory per connection.
const DEFAULT_READ_CHUNK: NonZeroUsize = match new ;
/// The settings both halves of the transport are built from.
///
/// Cloned into each adapter rather than shared by reference, so a
/// [`StdioSource`] and a [`StdioSink`] on the same session cannot be pointed at
/// different ceilings after the fact. The value is three words; the invariant is
/// worth more than the copy.
///
/// # Examples
///
/// ```
/// use protocol_droid::adapters::primary::stdio::StdioConfig;
/// use protocol_droid::domain::MaxFrameSize;
///
/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
/// // The default ceiling, with a smaller read buffer.
/// let config = StdioConfig::new(MaxFrameSize::new(64 * 1024)?).with_read_chunk(4096);
/// assert_eq!(config.read_chunk().get(), 4096);
/// # Ok(())
/// # }
/// ```
/// Why the stdio transport could not carry a frame.
///
/// Deliberately not [`crate::application::jsonrpc::SourceError`] or
/// [`crate::application::session::SinkError`]. Those are the *ports*, and they
/// carry no detail on purpose: the application layer is protocol-neutral and
/// must not learn that a broken pipe and an oversized reply are different
/// failures. This is the transport's own account, and it is what a caller
/// driving [`StdioSource`] or [`StdioSink`] directly actually learns.