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