systemprompt_loader/bundle/bootstrap/
baked.rs1use std::fs;
14use std::path::Path;
15
16use chrono::Utc;
17use systemprompt_models::services::bundle::{
18 BUNDLE_ALLOWED_DIRS, BUNDLE_MANIFEST_FILE, BundleSourceInfo, BundleSourceState,
19 ServicesBundleManifest, SignedBundleManifest,
20};
21
22use super::fetch::ResolvedSource;
23use crate::bundle::cache::{BundleCache, discard_staging};
24use crate::bundle::compose::copy_tree;
25use crate::bundle::error::{BundleError, BundleResult};
26use crate::bundle::pack::build_manifest;
27
28pub const BASE_SOURCE_NAME: &str = "base";
29const BAKED_VERSION: &str = "baked";
30
31#[must_use]
32pub fn is_base(manifest: &ServicesBundleManifest) -> bool {
33 manifest.owns.dirs.iter().any(|d| d == "config")
34}
35
36pub(super) fn stage_baked_base(
37 cache: &BundleCache,
38 services_root: &Path,
39 core_version: &str,
40) -> BundleResult<ResolvedSource> {
41 if !services_root.join("config/config.yaml").is_file() {
42 return Err(BundleError::policy(format!(
43 "no pinned source is a base and no baked services tree at {}",
44 services_root.display()
45 )));
46 }
47 let manifest = build_manifest(
48 services_root,
49 BAKED_VERSION,
50 &format!(">={core_version}"),
51 BundleSourceInfo::default(),
52 )?;
53 let content_hash = manifest.content_hash.clone();
54 let signed = SignedBundleManifest {
55 manifest,
56 signature: None,
57 };
58
59 let target = cache.bundle_dir(BASE_SOURCE_NAME, &content_hash);
60 if !target.is_dir() {
61 cache.prepare()?;
62 let staging = cache.bundle_dir(
63 BASE_SOURCE_NAME,
64 &format!("{content_hash}.tmp-{}", std::process::id()),
65 );
66 if staging.exists() {
67 fs::remove_dir_all(&staging)?;
68 }
69 fs::create_dir_all(&staging)?;
70 for dir in BUNDLE_ALLOWED_DIRS {
71 let from = services_root.join(dir);
72 if from.is_dir() {
73 let to = staging.join(dir);
74 fs::create_dir_all(&to)?;
75 copy_tree(&from, &to, false)?;
76 }
77 }
78 let raw = serde_json::to_vec_pretty(&signed)
79 .map_err(|e| BundleError::policy(format!("baked base manifest: {e}")))?;
80 fs::write(staging.join(BUNDLE_MANIFEST_FILE), raw)?;
81 if let Err(e) = fs::rename(&staging, &target) {
82 discard_staging(&staging);
83 if !target.is_dir() {
84 return Err(BundleError::extract(&target, e));
85 }
86 }
87 }
88
89 Ok(ResolvedSource {
90 name: BASE_SOURCE_NAME.to_owned(),
91 content_hash: content_hash.clone(),
92 signed,
93 state: BundleSourceState {
94 digest: format!("tree:{content_hash}"),
95 version: BAKED_VERSION.to_owned(),
96 content_hash,
97 fetched_at: Utc::now(),
98 },
99 })
100}