khive_runtime/preference_verification.rs
1//! Legacy moodboard preference-model verification, inverted behind a trait.
2//!
3//! `khive-mcp`'s V21 attachment cutover (ADR-121/ADR-160) must be able to
4//! authenticate legacy `khive-pack-moodboard` preference-model bundles
5//! without depending on that pack directly — the pack is opt-in and its
6//! crate dependency is feature-gated. `khive-runtime` already sits below
7//! both `khive-mcp` and `khive-pack-moodboard` in the dependency graph and
8//! already owns [`BlobHydrator`], so the trait lives here.
9
10use async_trait::async_trait;
11use khive_storage::{ContentRef, SqlAccess};
12use uuid::Uuid;
13
14use crate::{BlobHydrator, RuntimeError};
15
16/// One authenticated network role for the attachment cutover coordinator.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct VerifiedModelNetworkAttachment {
19 pub model_id: Uuid,
20 pub network_content_ref: ContentRef,
21 pub size_bytes: u64,
22}
23
24/// Restricts implementations of [`LegacyPreferenceVerifier`].
25///
26/// The verifier trait is `pub` because its one implementor lives in another
27/// crate, `khive-pack-moodboard`. It is not an extension point: the public
28/// commitment is the method shape the cutover calls, not open implementation.
29/// This supertrait is the marker that says so.
30///
31/// Note on what this does and does not enforce. Rust has no notion of
32/// "workspace-visible", so a marker a sibling crate can name is a marker any
33/// crate can name. This is the conventional sealing idiom — it prevents
34/// accidental implementation and documents intent — not a barrier a determined
35/// downstream cannot cross. Hard enforcement is unavailable while the
36/// implementor is a separate crate.
37#[doc(hidden)]
38pub mod sealed {
39 pub trait Sealed {}
40}
41
42/// Authenticates legacy moodboard preference-model bundle/event/FANN
43/// evidence for the V21 attachment cutover.
44///
45/// Implemented by `khive-pack-moodboard` when that pack is compiled in. When
46/// no implementation is installed and legacy rows exist, the cutover fails
47/// closed rather than silently dropping the legacy column with unmigrated
48/// models still on disk.
49///
50/// Sealed via [`sealed::Sealed`]; see that module for the scope of the seal.
51#[async_trait]
52pub trait LegacyPreferenceVerifier: sealed::Sealed + Send + Sync {
53 async fn verify_legacy_preference_attachments(
54 &self,
55 sql: &dyn SqlAccess,
56 hydrator: &BlobHydrator,
57 ) -> Result<Vec<VerifiedModelNetworkAttachment>, RuntimeError>;
58}