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}