Skip to main content

kcode_k1_loom_bootstrap/
lib.rs

1#![doc = include_str!("../Documentation.md")]
2#![forbid(unsafe_code)]
3
4use kcode_k1_access::K1Access;
5use kcode_k1_access_kmap::K1AccessKmap;
6use kcode_k1_access_launch_nodes::{ModelId, TxId, UserId};
7use kcode_k1_access_profiles::K1AccessProfiles;
8use kcode_k1_bootstrap_identity::{BootstrapIdentity, prompt};
9use kcode_k1_bootstrap_state::{
10    BeginRecord, BootstrapStatus, CompleteRecord, ImportedPackage as StatePackage, K1BootstrapState,
11};
12use kcode_k1_groups::K1Groups;
13use kcode_k1_invites::K1Invites;
14use kcode_k1_launch_nodes::LaunchNodes;
15use kcode_k1_loom_bootstrap_topology::{
16    BootstrapTopology, TopologyServices, ensure as ensure_topology,
17};
18use kcode_k1_rust_bootstrap_import::RustBootstrapImporter;
19use kcode_k1_rust_projection::K1RustProjection;
20use kcode_k1_users::{K1Users, NewUser, User};
21use kcode_k1_web_bootstrap_archive::read as read_web;
22use kcode_k1_web_bootstrap_import::{
23    ImportedPackage as WebImportedPackage, WebBootstrapImporter, write_preflight_log,
24};
25use std::path::Path;
26
27const WEB_INVENTORY_PREFIX: &str = "web:";
28const WEB_IMPORT_LOG: &str = "bootstrap/k1-web-import.log";
29const BLANK_RESTART: &str = "Loom bootstrap previously began without completing; delete the disposable blank-state data and restart";
30
31pub struct BootstrapServices<'a> {
32    pub state: &'a K1BootstrapState,
33    pub invites: &'a K1Invites,
34    pub users: &'a K1Users,
35    pub groups: &'a K1Groups,
36    pub profiles: &'a K1AccessProfiles,
37    pub access_kmap: &'a K1AccessKmap,
38    pub access_launch_nodes: &'a kcode_k1_access_launch_nodes::K1AccessLaunchNodes,
39    pub launch_nodes: &'a LaunchNodes,
40    pub rust_projection: &'a K1RustProjection,
41    pub model: ModelId,
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub struct BootstrapDeveloperGroups {
46    loom_devs: TxId,
47    kennedy_devs: TxId,
48}
49
50impl BootstrapDeveloperGroups {
51    pub const fn loom_devs(&self) -> TxId {
52        self.loom_devs
53    }
54
55    pub const fn kennedy_devs(&self) -> TxId {
56        self.kennedy_devs
57    }
58}
59
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct BootstrapResult {
62    record: CompleteRecord,
63    completed_now: bool,
64    developer_groups: BootstrapDeveloperGroups,
65}
66
67impl BootstrapResult {
68    pub fn record(&self) -> &CompleteRecord {
69        &self.record
70    }
71
72    pub const fn completed_now(&self) -> bool {
73        self.completed_now
74    }
75
76    pub const fn developer_groups(&self) -> BootstrapDeveloperGroups {
77        self.developer_groups
78    }
79}
80
81pub fn ensure_with_topology(
82    root: &Path,
83    access: &K1Access,
84    web_importer: &WebBootstrapImporter,
85    services: BootstrapServices<'_>,
86) -> Result<BootstrapResult, String> {
87    match services.state.status()? {
88        BootstrapStatus::Complete { record, .. } => {
89            require_web_complete(&record)?;
90            let first_user = UserId::from_tx_id(TxId::from_bytes(record.user_id()));
91            let topology = reconcile_topology(first_user, access, &services)?;
92            require_record_topology(&record, &topology)?;
93            return Ok(result(record, false, &topology));
94        }
95        BootstrapStatus::Begun { .. } => return Err(BLANK_RESTART.to_owned()),
96        BootstrapStatus::Empty => {}
97    }
98
99    let rust_importer = RustBootstrapImporter::open(
100        &root.join("bootstrap/k1-rust-code.zip"),
101        &root.join("bootstrap/k1-rust-import.log"),
102    )?;
103    let web = read_web(&root.join("bootstrap/k1-web-ui.zip"))?;
104    let web_import_log = root.join(WEB_IMPORT_LOG);
105    write_preflight_log(&web.archive, &web_import_log)?;
106
107    let identity = prompt(kcode_k1_terms::text().as_bytes())?;
108    let begin = BeginRecord::new(
109        identity.username().to_owned(),
110        identity.full_name().to_owned(),
111        identity.public_key(),
112    );
113    services.state.begin(begin.clone())?;
114
115    let user = reconcile_user(services.invites, services.users, &identity)?;
116    let account_user_bytes = *user.user_id().as_tx_id().as_bytes();
117    let first_user = UserId::from_tx_id(TxId::from_bytes(account_user_bytes));
118    let topology = reconcile_topology(first_user, access, &services)?;
119    let loom_authority = *topology.loom_group().txid().as_bytes();
120
121    let imported_rust = rust_importer.import_all(services.rust_projection, loom_authority, root)?;
122    let imported_web = web_importer.import_all(
123        &web.archive,
124        loom_authority,
125        account_user_bytes,
126        &web_import_log,
127    )?;
128
129    let mut inventory = rust_state_inventory(imported_rust)?;
130    inventory.extend(web_state_inventory(imported_web)?);
131    let record = CompleteRecord::new(
132        begin,
133        account_user_bytes,
134        loom_authority,
135        *topology.loom_profile().txid().as_bytes(),
136        *topology.loom_root().txid().as_bytes(),
137        inventory,
138    )?;
139    services.state.complete(record.clone())?;
140    Ok(result(record, true, &topology))
141}
142
143fn result(
144    record: CompleteRecord,
145    completed_now: bool,
146    topology: &BootstrapTopology,
147) -> BootstrapResult {
148    BootstrapResult {
149        record,
150        completed_now,
151        developer_groups: BootstrapDeveloperGroups {
152            loom_devs: TxId::from_bytes(*topology.loom_group().txid().as_bytes()),
153            kennedy_devs: TxId::from_bytes(*topology.kennedy_group().txid().as_bytes()),
154        },
155    }
156}
157
158fn reconcile_topology(
159    first_user: UserId,
160    access: &K1Access,
161    services: &BootstrapServices<'_>,
162) -> Result<BootstrapTopology, String> {
163    ensure_topology(
164        first_user,
165        TopologyServices {
166            groups: services.groups,
167            profiles: services.profiles,
168            access,
169            access_kmap: services.access_kmap,
170            access_launch_nodes: services.access_launch_nodes,
171            launch_nodes: services.launch_nodes,
172            model: services.model,
173        },
174    )
175}
176
177fn require_record_topology(
178    record: &CompleteRecord,
179    topology: &BootstrapTopology,
180) -> Result<(), String> {
181    if record.group_id() == *topology.loom_group().txid().as_bytes()
182        && record.profile_id() == *topology.loom_profile().txid().as_bytes()
183        && record.root_id() == *topology.loom_root().txid().as_bytes()
184    {
185        Ok(())
186    } else {
187        Err("canonical bootstrap Complete conflicts with reconciled Loom topology".to_owned())
188    }
189}
190
191fn require_web_complete(record: &CompleteRecord) -> Result<(), String> {
192    if record
193        .packages()
194        .iter()
195        .any(|package| package.logical_name().starts_with(WEB_INVENTORY_PREFIX))
196    {
197        Ok(())
198    } else {
199        Err("canonical bootstrap Complete predates Web bootstrap; delete the disposable state and restart blank".to_owned())
200    }
201}
202
203fn reconcile_user(
204    invites: &K1Invites,
205    users: &K1Users,
206    identity: &BootstrapIdentity,
207) -> Result<User, String> {
208    if let Some(existing) = users.find_by_username(identity.username())? {
209        require_matching_user(&existing, identity)?;
210        return Ok(existing);
211    }
212    let (_, invite) = invites.create()?;
213    let candidate = NewUser::new(
214        identity.username(),
215        identity.full_name(),
216        identity.public_key(),
217        kcode_k1_terms::REVISION,
218        kcode_k1_terms::sha256(),
219        identity.message_signature(),
220    )?;
221    let user = users.register(&invite, candidate)?;
222    require_matching_user(&user, identity)?;
223    Ok(user)
224}
225
226fn require_matching_user(user: &User, identity: &BootstrapIdentity) -> Result<(), String> {
227    if user.username() == identity.username()
228        && user.full_name() == identity.full_name()
229        && user.public_key() == identity.public_key()
230        && user.tos_revision() == kcode_k1_terms::REVISION
231        && user.tos_digest() == kcode_k1_terms::sha256()
232        && user.acceptance_signature() == identity.message_signature()
233    {
234        Ok(())
235    } else {
236        Err("existing bootstrap Account conflicts with entered identity".to_owned())
237    }
238}
239
240fn rust_state_inventory(
241    imported: Vec<kcode_k1_rust_bootstrap_import::ImportedPackage>,
242) -> Result<Vec<StatePackage>, String> {
243    imported
244        .into_iter()
245        .map(|package| {
246            StatePackage::new(package.logical_name().to_owned(), package.version().clone())
247        })
248        .collect()
249}
250
251fn web_state_inventory(imported: Vec<WebImportedPackage>) -> Result<Vec<StatePackage>, String> {
252    imported
253        .into_iter()
254        .map(|package| {
255            StatePackage::new(
256                format!("{WEB_INVENTORY_PREFIX}{}", package.name()),
257                package.version().clone(),
258            )
259        })
260        .collect()
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn web_inventory_namespace_and_import_log_are_bootstrap_owned() {
269        assert!(WEB_INVENTORY_PREFIX.contains(':'));
270        assert_eq!(WEB_IMPORT_LOG, "bootstrap/k1-web-import.log");
271        assert!(BLANK_RESTART.contains("restart"));
272    }
273}