Skip to main content

clawft_kernel/model_manifest/
sharing.rs

1//! Model weights over the artifact exchange, redistribution fail-closed
2//! (ADR-099 section 6; ADR-101 section 3).
3//!
4//! A model's shards enter the exchange only through [`seed_model`], and only
5//! when the attested manifest says `redistributable = true` **and** the
6//! exchange's [`RedistributionPolicy`] allows every shard for
7//! [`Audience::Seed`]. The grant carries [`GrantOrigin::OptIn`] in that case
8//! and [`GrantOrigin::NotFlagged`] otherwise, so the default
9//! [`crate::mesh_swarm_state::ManifestPolicy`] vetoes an un-opted-in model
10//! for every audience. Models have no Cognitum provenance.
11//!
12//! Seeding reads the adopted files and stores their pieces in the artifact
13//! store, so it is the one place weights are duplicated on disk. It is an
14//! explicit, opted-in act; adoption itself never copies.
15
16use std::fs::File;
17
18use super::adopt::{allowed_roots, contained};
19use super::body::{ModelError, ModelPackageBody, VerifiedModel, verify_model};
20use super::locality::Sharing;
21use super::registry::{ModelRegistry, ModelTrust};
22use crate::mesh_artifact::ArtifactExchange;
23use crate::mesh_artifact_types::{ArtifactId, ExchangeError};
24use crate::mesh_swarm_state::{Audience, GrantInfo, GrantOrigin, RedistributionPolicy};
25use crate::workload_pkg::TrustAnchors;
26use crate::workload_pkg::codec::{hex_decode_exact, hex_encode};
27
28/// Licence standing of a model, from its signed manifest. Fails closed.
29pub fn grant_origin(body: &ModelPackageBody) -> GrantOrigin {
30    if body.redistributable { GrantOrigin::OptIn } else { GrantOrigin::NotFlagged }
31}
32
33fn grant_info(package_id: &str, body: &ModelPackageBody, signers: Vec<String>) -> GrantInfo {
34    GrantInfo {
35        package_id: package_id.to_string(),
36        signers,
37        origin: grant_origin(body),
38    }
39}
40
41/// The sharing stance the policy takes for this model: `Allowed` only when
42/// the policy allows every shard to be seeded.
43pub fn sharing_for(
44    policy: &dyn RedistributionPolicy,
45    package_id: &str,
46    body: &ModelPackageBody,
47) -> Sharing {
48    let grants = [grant_info(package_id, body, Vec::new())];
49    let all = body.shards.iter().all(|s| {
50        hex_decode_exact::<32>(&s.blake3)
51            .is_some_and(|h| policy.allows(&h, &grants, &Audience::Seed))
52    });
53    if all { Sharing::Allowed } else { Sharing::Refused }
54}
55
56/// A model seeded into the exchange.
57#[derive(Debug, Clone)]
58pub struct SeededModel {
59    /// Package id.
60    pub package_id: String,
61    /// BLAKE3 hex of the manifest bytes seeded (peers fetch it by this).
62    pub manifest_hash: String,
63    /// Each shard path with its artifact id.
64    pub shards: Vec<(String, ArtifactId)>,
65}
66
67/// Why a model was not seeded.
68#[derive(Debug, thiserror::Error)]
69pub enum SeedModelError {
70    /// Model state, signature or hash problem.
71    #[error(transparent)]
72    Model(#[from] ModelError),
73    /// The manifest or the exchange policy does not allow sharing.
74    #[error("model {0} is not shareable: not opted in, or vetoed by the redistribution policy")]
75    NotShareable(String),
76    /// Package, signer or manifest is on the revocation list.
77    #[error("model {0} (or its signer or manifest) is revoked")]
78    Revoked(String),
79    /// Exchange failure.
80    #[error(transparent)]
81    Exchange(#[from] ExchangeError),
82}
83
84fn signer_keys(verified: &VerifiedModel, anchors: &TrustAnchors) -> Vec<String> {
85    verified
86        .signers
87        .iter()
88        .filter_map(|s| anchors.signers.iter().find(|k| k.key_id == s.key_id))
89        .map(|k| hex_encode(&k.public_key))
90        .collect()
91}
92
93/// Seed an adopted model into `exchange`, if and only if sharing is allowed.
94///
95/// The model is resolved first (lazy hash check, so a mismatching or
96/// detached model is refused), its signature is re-verified against the
97/// current `anchors` (a since-removed operator key stops sharing), the
98/// revocation list is consulted, and the exchange's redistribution policy
99/// must allow every shard. Each shard is then streamed and verified against
100/// its attested hash while seeding.
101pub fn seed_model(
102    exchange: &ArtifactExchange,
103    registry: &ModelRegistry,
104    id_or_name: &str,
105    anchors: &TrustAnchors,
106) -> Result<SeededModel, SeedModelError> {
107    let resolved = registry.resolve_with(id_or_name, &ModelTrust::new(anchors.clone()))?;
108    let (_, entry) = registry.get(id_or_name)?;
109    let verified = verify_model(&entry.envelope, anchors)?;
110    let package_id = verified.package_id.clone();
111    let body = &verified.body;
112    if sharing_for(exchange.config().redistribution.as_ref(), &package_id, body) != Sharing::Allowed {
113        return Err(SeedModelError::NotShareable(package_id));
114    }
115    let signers = signer_keys(&verified, anchors);
116    let manifest = entry.envelope.to_pretty_json().map_err(ModelError::from)?;
117    let manifest_hash = *blake3::hash(&manifest).as_bytes();
118    let shard_hashes = body
119        .shards
120        .iter()
121        .filter_map(|s| hex_decode_exact::<32>(&s.blake3));
122    for h in std::iter::once(manifest_hash).chain(shard_hashes) {
123        if exchange.is_revoked_subject(&package_id, &signers, &h) {
124            return Err(SeedModelError::Revoked(package_id));
125        }
126    }
127    let allowed = allowed_roots(&resolved.root);
128    let origin = grant_origin(body);
129    let mut shards = Vec::new();
130    for (shard, path) in body.shards.iter().zip(&resolved.shards) {
131        let expect = (
132            hex_decode_exact::<32>(&shard.blake3).expect("validated hash"),
133            shard.size,
134        );
135        // Containment again, immediately before the open, on the resolved
136        // path: a link swapped since the check must not be read.
137        let real = contained(path, &allowed)?;
138        let mut f = File::open(real).map_err(|e| ModelError::Io {
139            path: shard.path.clone(),
140            msg: e.to_string(),
141        })?;
142        let d = exchange.seed_reader(&mut f, Some(expect))?;
143        exchange.grant_with(d.content_hash, &package_id, signers.clone(), origin.clone());
144        shards.push((shard.path.clone(), d.id()));
145    }
146    let m = exchange.seed_bytes(&manifest)?;
147    exchange.grant_with(m.content_hash, &package_id, signers, origin);
148    Ok(SeededModel {
149        package_id,
150        manifest_hash: hex_encode(&manifest_hash),
151        shards,
152    })
153}