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