Skip to main content

systemprompt_loader/bundle/source/
mod.rs

1//! Remote bundle transports.
2//!
3//! [`BundleFetcher`] is deliberately two calls: `head` is the cheap identity
4//! probe the boot path uses to decide whether anything changed, and `fetch`
5//! is the streaming download. `head` returns an empty digest when the remote
6//! offers no cheap identity (an HTTPS endpoint without an `ETag`), which the
7//! boot path reads as "unknown" and therefore as changed — never as
8//! unchanged, which would pin an instance to a stale bundle forever.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13pub mod https;
14pub mod oci;
15mod stream;
16
17use std::path::{Path, PathBuf};
18
19use systemprompt_models::profile::ServicesSource;
20
21use super::error::{BundleError, BundleResult};
22
23pub use https::HttpsFetcher;
24pub use oci::{OciFetcher, push_bundle};
25
26pub const MAX_BUNDLE_BYTES: u64 = 512 * 1024 * 1024;
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct RemoteRef {
30    pub digest: String,
31}
32
33impl RemoteRef {
34    #[must_use]
35    pub const fn is_unknown(&self) -> bool {
36        self.digest.is_empty()
37    }
38}
39
40#[derive(Debug, Clone)]
41pub struct FetchedBundle {
42    pub archive: PathBuf,
43    pub digest: String,
44}
45
46pub trait BundleFetcher {
47    fn head(&self) -> impl Future<Output = BundleResult<RemoteRef>> + Send;
48
49    fn fetch(&self, into: &Path) -> impl Future<Output = BundleResult<FetchedBundle>> + Send;
50}
51
52#[derive(Debug)]
53pub enum AnyFetcher {
54    Https(HttpsFetcher),
55    Oci(OciFetcher),
56}
57
58impl AnyFetcher {
59    pub fn from_source(
60        source: &ServicesSource,
61        auth: Option<String>,
62        client: &reqwest::Client,
63    ) -> BundleResult<Self> {
64        if !source.is_exactly_one() {
65            return Err(BundleError::policy(format!(
66                "source {} must declare exactly one of https: or oci:",
67                source.name
68            )));
69        }
70        if let Some(https) = source.https.as_ref() {
71            return Ok(Self::Https(HttpsFetcher::new(
72                &source.name,
73                &https.url,
74                auth,
75                client.clone(),
76            )));
77        }
78        let oci = source
79            .oci
80            .as_ref()
81            .ok_or_else(|| BundleError::policy("source declares no transport"))?;
82        Ok(Self::Oci(OciFetcher::new(
83            &source.name,
84            &oci.reference,
85            auth,
86            client.clone(),
87        )?))
88    }
89}
90
91impl BundleFetcher for AnyFetcher {
92    async fn head(&self) -> BundleResult<RemoteRef> {
93        match self {
94            Self::Https(f) => f.head().await,
95            Self::Oci(f) => f.head().await,
96        }
97    }
98
99    async fn fetch(&self, into: &Path) -> BundleResult<FetchedBundle> {
100        match self {
101            Self::Https(f) => f.fetch(into).await,
102            Self::Oci(f) => f.fetch(into).await,
103        }
104    }
105}