fastmcp-rust 0.12.0

Asupersync-based MCP framework for Rust
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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
//! FND-01 A: enforcement for the authoritative source and toolchain freeze.
//!
//! This module is the shipped, non-`cfg(test)` public surface that makes a
//! declared closed-child binding *checkable*. Each binding in
//! `evidence/fnd-01/dependency-verification.toml` names an owned source path,
//! the scope that closed over it, and the byte length and SHA-256 that were
//! authoritative when it was recorded.
//!
//! # Why this exists
//!
//! Those rows were, until this module, read by no code anywhere in the
//! workspace. A freeze that nothing checks is not a freeze: a declared binding
//! can drift arbitrarily far from the file it claims to bind and no evaluator
//! notices. Enforcement is the missing capability, not fresher numbers —
//! recomputing the recorded digests to make them agree would destroy the only
//! evidence that drift occurred.
//!
//! # What this module does and does not decide
//!
//! It answers one question per row: *does this declared binding match these
//! actual bytes?* It is a pure function of `(declaration, bytes)`. It performs
//! no filesystem access, reads no ambient state, and takes no position on what
//! a drifted binding should cost — repairing or re-issuing a binding belongs
//! to the integration scope that owns the receipt, and deciding whether a
//! drifted campaign may proceed belongs to independent verification.
//!
//! Both checked fields are reported independently and neither short-circuits
//! the other, because the two drift shapes are diagnostically different. A
//! same-length, different-content edit is exactly the case a length-only
//! comparison misses, so the digest is always evaluated even when the length
//! already disagrees.

//! # Two declared shapes, one invariant
//!
//! This module enforces two kinds of declaration, both resting on the same
//! invariant: **the authoritative evidence document describes this
//! repository.**
//!
//! - A [`ClosedChildBinding`] declares the byte length and digest of an owned
//!   source file. It is checked against that file's bytes.
//! - A [`DeclaredFact`] declares a value the document asserts about the
//!   repository — the toolchain channel it pins, the `rust-version` its
//!   documentation states. It is checked against what the repository actually
//!   contains.
//!
//! The second shape is deliberately a *consistency* check rather than a
//! literal-value assertion. Asserting the document's literals directly would
//! encode whichever values happened to be recorded, so a document that had
//! fallen behind a deliberate project decision would force the repository to
//! match the stale document. Comparing declaration against reality stays
//! correct in both directions and keeps its meaning after a divergence is
//! resolved, whichever side moves.

use fastmcp_core::sha256_bounded;

/// Maximum bytes this module will digest for one bound source file.
///
/// Bounded before hashing so an oversized or truncated input is a typed
/// refusal rather than unbounded work. The largest currently bound source is
/// well under this ceiling.
pub const MAX_BOUND_SOURCE_BYTES: usize = 8 * 1024 * 1024;

/// SHA-256 digests are recorded as 64 lowercase hex characters.
pub const SHA256_HEX_LENGTH: usize = 64;

/// A closed-child binding exactly as the evidence document declares it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClosedChildBinding {
    path: String,
    owner_scope: String,
    byte_length: usize,
    sha256: String,
}

/// A malformed declaration, refused before any comparison is attempted.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingDeclarationError {
    /// The bound path was empty.
    EmptyPath,
    /// The owning scope was empty.
    EmptyOwnerScope,
    /// The recorded digest was not exactly 64 characters.
    DigestLength,
    /// The recorded digest contained a character outside `0-9a-f`.
    ///
    /// Uppercase is refused deliberately: the digest is compared as recorded,
    /// so admitting two spellings of one value would make equality depend on
    /// how the document happened to be written.
    DigestNotLowercaseHex,
}

impl std::fmt::Display for BindingDeclarationError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::EmptyPath => formatter.write_str("closed-child binding path must be nonempty"),
            Self::EmptyOwnerScope => {
                formatter.write_str("closed-child binding owner scope must be nonempty")
            }
            Self::DigestLength => {
                formatter.write_str("closed-child binding digest must be 64 hex characters")
            }
            Self::DigestNotLowercaseHex => {
                formatter.write_str("closed-child binding digest must be lowercase hex")
            }
        }
    }
}

impl std::error::Error for BindingDeclarationError {}

/// Why a digest could not be computed for the supplied bytes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoundSourceError {
    /// The supplied bytes exceeded [`MAX_BOUND_SOURCE_BYTES`].
    TooLarge {
        /// Bytes supplied.
        supplied: usize,
        /// The configured ceiling.
        ceiling: usize,
    },
}

impl std::fmt::Display for BoundSourceError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::TooLarge { supplied, ceiling } => write!(
                formatter,
                "bound source of {supplied} bytes exceeds the {ceiling}-byte hashing bound"
            ),
        }
    }
}

impl std::error::Error for BoundSourceError {}

impl ClosedChildBinding {
    /// Records a declared binding, refusing a malformed one.
    ///
    /// # Errors
    ///
    /// Returns [`BindingDeclarationError`] when a field is empty or the digest
    /// is not exactly 64 lowercase hex characters.
    pub fn declare(
        path: &str,
        owner_scope: &str,
        byte_length: usize,
        sha256: &str,
    ) -> Result<Self, BindingDeclarationError> {
        if path.is_empty() {
            return Err(BindingDeclarationError::EmptyPath);
        }
        if owner_scope.is_empty() {
            return Err(BindingDeclarationError::EmptyOwnerScope);
        }
        if sha256.len() != SHA256_HEX_LENGTH {
            return Err(BindingDeclarationError::DigestLength);
        }
        if !sha256
            .bytes()
            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
        {
            return Err(BindingDeclarationError::DigestNotLowercaseHex);
        }
        Ok(Self {
            path: path.to_owned(),
            owner_scope: owner_scope.to_owned(),
            byte_length,
            sha256: sha256.to_owned(),
        })
    }

    /// The bound source path, exactly as declared.
    #[must_use]
    pub fn path(&self) -> &str {
        &self.path
    }

    /// The scope that closed over this source.
    #[must_use]
    pub fn owner_scope(&self) -> &str {
        &self.owner_scope
    }

    /// The authoritative byte length recorded for this source.
    #[must_use]
    pub const fn byte_length(&self) -> usize {
        self.byte_length
    }

    /// The authoritative SHA-256, as 64 lowercase hex characters.
    #[must_use]
    pub fn sha256(&self) -> &str {
        &self.sha256
    }

    /// Evaluates this declaration against the actual bytes of the bound file.
    ///
    /// # Errors
    ///
    /// Returns [`BoundSourceError::TooLarge`] when the supplied bytes exceed
    /// [`MAX_BOUND_SOURCE_BYTES`]; the declaration is neither accepted nor
    /// refused in that case, because no digest was computed.
    pub fn evaluate(&self, actual: &[u8]) -> Result<BindingOutcome, BoundSourceError> {
        let digest = sha256_bounded(actual, MAX_BOUND_SOURCE_BYTES).map_err(|_| {
            BoundSourceError::TooLarge {
                supplied: actual.len(),
                ceiling: MAX_BOUND_SOURCE_BYTES,
            }
        })?;
        let actual_sha256 = lowercase_hex(digest.as_bytes());
        // Both fields are always evaluated. A same-length, different-content
        // edit is precisely the drift a length-only comparison misses.
        Ok(BindingOutcome {
            declared_byte_length: self.byte_length,
            actual_byte_length: actual.len(),
            digest_matches: actual_sha256 == self.sha256,
            actual_sha256,
        })
    }
}

/// Renders a digest as 64 lowercase hex characters.
fn lowercase_hex(bytes: &[u8; 32]) -> String {
    const DIGITS: &[u8; 16] = b"0123456789abcdef";
    let mut rendered = String::with_capacity(SHA256_HEX_LENGTH);
    for &byte in bytes {
        rendered.push(char::from(DIGITS[usize::from(byte >> 4)]));
        rendered.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
    }
    rendered
}

/// The observed relationship between one declaration and one file's bytes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindingOutcome {
    declared_byte_length: usize,
    actual_byte_length: usize,
    actual_sha256: String,
    digest_matches: bool,
}

impl BindingOutcome {
    /// The byte length the declaration recorded.
    #[must_use]
    pub const fn declared_byte_length(&self) -> usize {
        self.declared_byte_length
    }

    /// The byte length actually supplied.
    #[must_use]
    pub const fn actual_byte_length(&self) -> usize {
        self.actual_byte_length
    }

    /// The digest actually computed, as 64 lowercase hex characters.
    #[must_use]
    pub fn actual_sha256(&self) -> &str {
        &self.actual_sha256
    }

    /// Whether the recorded length matches the supplied bytes.
    #[must_use]
    pub const fn length_matches(&self) -> bool {
        self.declared_byte_length == self.actual_byte_length
    }

    /// Whether the recorded digest matches the supplied bytes.
    #[must_use]
    pub const fn digest_matches(&self) -> bool {
        self.digest_matches
    }

    /// Whether the binding holds: both recorded fields match.
    #[must_use]
    pub const fn is_bound(&self) -> bool {
        self.length_matches() && self.digest_matches()
    }

    /// The drift classification for this row.
    #[must_use]
    pub const fn drift(&self) -> BindingDrift {
        match (self.length_matches(), self.digest_matches()) {
            (true, true) => BindingDrift::Bound,
            (true, false) => BindingDrift::ContentOnly,
            (false, true) => BindingDrift::LengthOnly,
            (false, false) => BindingDrift::LengthAndContent,
        }
    }
}

/// How a declared binding relates to the bytes it claims to bind.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingDrift {
    /// Length and digest both match; the freeze holds for this row.
    Bound,
    /// The length matches but the content changed.
    ///
    /// This is the case a length-only comparison cannot see, which is why the
    /// digest is evaluated unconditionally.
    ContentOnly,
    /// The digest matches but the recorded length does not.
    ///
    /// Only reachable from a mis-recorded declaration, since equal content
    /// implies equal length.
    LengthOnly,
    /// Neither recorded field matches.
    LengthAndContent,
}

impl BindingDrift {
    /// Whether this classification means the binding holds.
    #[must_use]
    pub const fn is_bound(self) -> bool {
        matches!(self, Self::Bound)
    }
}

/// A refused fact declaration.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FactDeclarationError {
    /// The subject naming what is declared was empty.
    EmptySubject,
    /// The declared value was empty.
    ///
    /// An empty declaration is refused rather than treated as "no claim",
    /// because a silently absent value would let a consistency check pass
    /// while comparing nothing.
    EmptyDeclared,
}

impl std::fmt::Display for FactDeclarationError {
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::EmptySubject => formatter.write_str("declared fact subject must be nonempty"),
            Self::EmptyDeclared => formatter.write_str("declared fact value must be nonempty"),
        }
    }
}

impl std::error::Error for FactDeclarationError {}

/// One fact the evidence document declares about this repository.
///
/// The document is authoritative about what it *claims*; the repository is
/// authoritative about what is *true*. This type carries the claim so it can
/// be confronted with the truth.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclaredFact {
    subject: String,
    declared: String,
}

impl DeclaredFact {
    /// Records a declared fact, refusing an empty subject or value.
    ///
    /// # Errors
    ///
    /// Returns [`FactDeclarationError`] when either field is empty.
    pub fn declare(subject: &str, declared: &str) -> Result<Self, FactDeclarationError> {
        if subject.is_empty() {
            return Err(FactDeclarationError::EmptySubject);
        }
        if declared.is_empty() {
            return Err(FactDeclarationError::EmptyDeclared);
        }
        Ok(Self {
            subject: subject.to_owned(),
            declared: declared.to_owned(),
        })
    }

    /// What this fact is about.
    #[must_use]
    pub fn subject(&self) -> &str {
        &self.subject
    }

    /// The value the evidence document declares.
    #[must_use]
    pub fn declared(&self) -> &str {
        &self.declared
    }

    /// Confronts the declaration with what the repository actually contains.
    #[must_use]
    pub fn compare(&self, observed: &str) -> FactOutcome {
        FactOutcome {
            subject: self.subject.clone(),
            declared: self.declared.clone(),
            observed: observed.to_owned(),
        }
    }
}

/// The result of confronting one declaration with reality.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FactOutcome {
    subject: String,
    declared: String,
    observed: String,
}

impl FactOutcome {
    /// What this fact is about.
    #[must_use]
    pub fn subject(&self) -> &str {
        &self.subject
    }

    /// The value the evidence document declares.
    #[must_use]
    pub fn declared(&self) -> &str {
        &self.declared
    }

    /// The value the repository actually contains.
    #[must_use]
    pub fn observed(&self) -> &str {
        &self.observed
    }

    /// Whether the document describes the repository for this fact.
    #[must_use]
    pub fn describes_repository(&self) -> bool {
        self.declared == self.observed
    }
}

impl std::fmt::Display for FactOutcome {
    /// Names both sides, so a divergence report is actionable without
    /// re-deriving either value.
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            formatter,
            "{}: evidence declares {:?}, repository has {:?}",
            self.subject, self.declared, self.observed
        )
    }
}