1#![doc = include_str!("../Documentation.md")]
2#![forbid(unsafe_code)]
3
4use kcode_k1_access_kmap::K1AccessKmap;
5use kcode_k1_access_launch_nodes::{
6 AccessContext, AccessId, AccessPolicy, Authority, FilteredAuthorities, K1AccessLaunchNodes,
7 ModelId, ProfileId, TargetId, TargetName, TxId, UserId, ViewerSubject,
8};
9use kcode_k1_access_profiles::{K1AccessProfiles, ProfileColor, ProfileName};
10use kcode_k1_bootstrap_identity::{BootstrapIdentity, prompt};
11use kcode_k1_bootstrap_state::{
12 BeginRecord, BootstrapStatus, CompleteRecord, ImportedPackage as StatePackage, K1BootstrapState,
13};
14use kcode_k1_groups::{
15 ALL_MODELS, ALL_MODELS_MEMBER, ALL_USERS, Group, GroupId, GroupName, GroupRole, K1Groups,
16};
17use kcode_k1_invites::K1Invites;
18use kcode_k1_launch_nodes::LaunchNodes;
19use kcode_k1_rust_bootstrap_archive::{LoadedArchive, read};
20use kcode_k1_rust_bootstrap_import::{PreparedPackage, import_all, prepare};
21use kcode_k1_rust_projection::K1RustProjection;
22use kcode_k1_users::{K1Users, NewUser, User};
23use std::path::Path;
24
25const GROUP_NAME: &str = "loom-devs";
26const PROFILE_NAME: &str = "loom-devs";
27const ROOT_TITLE: &str = "loom-devs Kmap Root";
28const ROOT_HINT: &str = "The public starting point for the loom-devs group Kmap.";
29const ROOT_NARRATIVE: &str = "This is the public root of the loom-devs group Kmap.";
30const LAUNCH_TARGET: &str = "KmapLaunchNode";
31
32pub struct BootstrapServices<'a> {
33 pub state: &'a K1BootstrapState,
34 pub invites: &'a K1Invites,
35 pub users: &'a K1Users,
36 pub groups: &'a K1Groups,
37 pub profiles: &'a K1AccessProfiles,
38 pub access_kmap: &'a K1AccessKmap,
39 pub access_launch_nodes: &'a K1AccessLaunchNodes,
40 pub launch_nodes: &'a LaunchNodes,
41 pub rust_projection: &'a K1RustProjection,
42 pub model: ModelId,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct BootstrapResult {
47 record: CompleteRecord,
48 completed_now: bool,
49}
50
51impl BootstrapResult {
52 pub fn record(&self) -> &CompleteRecord {
53 &self.record
54 }
55
56 pub const fn completed_now(&self) -> bool {
57 self.completed_now
58 }
59}
60
61pub fn ensure(root: &Path, services: BootstrapServices<'_>) -> Result<BootstrapResult, String> {
62 if let BootstrapStatus::Complete { record, .. } = services.state.status()? {
63 return Ok(BootstrapResult {
64 record,
65 completed_now: false,
66 });
67 }
68
69 let loaded = load_archive(root)?;
70 let identity = prompt(kcode_k1_terms::text().as_bytes())?;
71 let begin = BeginRecord::new(
72 loaded.sha256,
73 identity.username().to_owned(),
74 identity.full_name().to_owned(),
75 identity.public_key(),
76 );
77 services.state.begin(begin.clone())?;
78
79 let user = reconcile_user(services.invites, services.users, &identity)?;
80 let account_user_bytes = *user.user_id().as_tx_id().as_bytes();
81 let user_id = UserId::from_tx_id(TxId::from_bytes(account_user_bytes));
82
83 let group = reconcile_group(services.groups, user_id)?;
84 let group_id = group.id();
85 services
86 .groups
87 .set_model_membership(user_id, group_id, ALL_MODELS_MEMBER, true)?;
88
89 let policy = public_policy(group_id)?;
90 let profile_id = reconcile_profile(services.profiles, user_id, group_id, &policy)?;
91 let prepared = prepare(&loaded.archive, *group_id.txid().as_bytes())?;
92 let context = AccessContext::new(user_id, services.model, FilteredAuthorities::empty())?;
93 let root_id = reconcile_root(&services, &context, profile_id, &policy, group_id)?;
94 let imported = import_all(services.rust_projection, &prepared)?;
95 verify_inventory(&prepared, &imported)?;
96
97 let inventory = imported
98 .into_iter()
99 .map(|package| {
100 StatePackage::new(
101 package.logical_name().to_owned(),
102 package.version().clone(),
103 package.source_sha256(),
104 )
105 })
106 .collect::<Result<Vec<_>, _>>()?;
107
108 let record = CompleteRecord::new(
109 begin,
110 account_user_bytes,
111 *group_id.txid().as_bytes(),
112 *profile_id.txid().as_bytes(),
113 *root_id.txid().as_bytes(),
114 inventory,
115 )?;
116 services.state.complete(record.clone())?;
117
118 Ok(BootstrapResult {
119 record,
120 completed_now: true,
121 })
122}
123
124fn load_archive(root: &Path) -> Result<LoadedArchive, String> {
125 let loaded = read(&root.join("bootstrap/k1-rust-code.zip"))?;
126 loaded.archive.require_complete()?;
127 if loaded.archive.root_library != "loom" {
128 return Err("bootstrap archive root library is not loom".to_owned());
129 }
130 Ok(loaded)
131}
132
133fn reconcile_user(
134 invites: &K1Invites,
135 users: &K1Users,
136 identity: &BootstrapIdentity,
137) -> Result<User, String> {
138 if let Some(existing) = users.find_by_username(identity.username())? {
139 require_matching_user(&existing, identity)?;
140 return Ok(existing);
141 }
142
143 let (_, invite) = invites.create()?;
144 let candidate = NewUser::new(
145 identity.username(),
146 identity.full_name(),
147 identity.public_key(),
148 kcode_k1_terms::REVISION,
149 kcode_k1_terms::sha256(),
150 identity.message_signature(),
151 )?;
152 let user = users.register(&invite, candidate)?;
153 require_matching_user(&user, identity)?;
154 Ok(user)
155}
156
157fn require_matching_user(user: &User, identity: &BootstrapIdentity) -> Result<(), String> {
158 let matches = user.username() == identity.username()
159 && user.full_name() == identity.full_name()
160 && user.public_key() == identity.public_key()
161 && user.tos_revision() == kcode_k1_terms::REVISION
162 && user.tos_digest() == kcode_k1_terms::sha256()
163 && user.acceptance_signature() == identity.message_signature();
164
165 if matches {
166 Ok(())
167 } else {
168 Err("existing bootstrap Account conflicts with entered identity".to_owned())
169 }
170}
171
172fn reconcile_group(groups: &K1Groups, user: UserId) -> Result<Group, String> {
173 let mut matches = Vec::new();
174 for id in groups.groups_for_user(user)? {
175 if id.sentinel().is_none()
176 && let Some(group) = groups.get(id)?
177 && group.name().as_str() == GROUP_NAME
178 {
179 matches.push(group);
180 }
181 }
182
183 let group = match matches.len() {
184 0 => {
185 let revision = groups.create(user, GroupName::new(GROUP_NAME.to_owned())?)?;
186 groups
187 .get(revision.group_id())?
188 .ok_or_else(|| "created loom-devs group is unavailable".to_owned())?
189 }
190 1 => matches.pop().expect("one group"),
191 _ => {
192 return Err("multiple loom-devs groups are visible to the bootstrap user".to_owned());
193 }
194 };
195
196 if !group
197 .users()
198 .iter()
199 .any(|entry| entry.user_id() == user && entry.role() == GroupRole::Owner)
200 {
201 return Err("bootstrap user is not an Owner of loom-devs".to_owned());
202 }
203
204 Ok(group)
205}
206
207fn public_policy(group: GroupId) -> Result<AccessPolicy, String> {
208 AccessPolicy::new(
209 Authority::Group(group),
210 Vec::new(),
211 vec![
212 ViewerSubject::Group(ALL_USERS),
213 ViewerSubject::Group(ALL_MODELS),
214 ],
215 )
216}
217
218fn reconcile_profile(
219 profiles: &K1AccessProfiles,
220 user: UserId,
221 group: GroupId,
222 policy: &AccessPolicy,
223) -> Result<ProfileId, String> {
224 let mut exact = Vec::new();
225 let mut conflicting = false;
226
227 for profile in profiles.list_for_user(user)? {
228 if profile.name().as_str() != PROFILE_NAME {
229 continue;
230 }
231
232 let matches = profile.owner() == user
233 && !profile.archived()
234 && profile
235 .color()
236 .is_some_and(|color| color.as_str() == "amber")
237 && profile.policy() == policy;
238
239 if matches {
240 exact.push(profile.profile_id());
241 } else if profile.policy().authority() == Authority::Group(group) {
242 conflicting = true;
243 }
244 }
245
246 if conflicting || exact.len() > 1 {
247 return Err("loom-devs Profile is ambiguous or conflicts with bootstrap policy".to_owned());
248 }
249 if let Some(profile) = exact.pop() {
250 return Ok(profile);
251 }
252
253 let revision = profiles.create(
254 user,
255 ProfileName::new(PROFILE_NAME.to_owned())?,
256 Some(ProfileColor::new("amber".to_owned())?),
257 policy.clone(),
258 )?;
259 Ok(revision.profile_id())
260}
261
262fn reconcile_root(
263 services: &BootstrapServices<'_>,
264 context: &AccessContext,
265 profile: ProfileId,
266 policy: &AccessPolicy,
267 group: GroupId,
268) -> Result<AccessId, String> {
269 let target = TargetId::new(
270 Authority::Group(group),
271 TargetName::new(LAUNCH_TARGET.to_owned())?,
272 );
273
274 let root = if services.launch_nodes.get(&target)?.is_some() {
275 services.access_launch_nodes.lookup(context, &target)?
276 } else {
277 let revision = services.access_kmap.create_node(
278 context,
279 profile,
280 policy.clone(),
281 ROOT_TITLE.to_owned(),
282 ROOT_HINT.to_owned(),
283 ROOT_NARRATIVE.to_owned(),
284 Vec::new(),
285 )?;
286 let root = revision.access_id();
287
288 match services.access_launch_nodes.create(
289 context,
290 profile,
291 policy.clone(),
292 target.clone(),
293 root,
294 ) {
295 Ok(()) => root,
296 Err(error) => {
297 let found = services
298 .access_launch_nodes
299 .lookup(context, &target)
300 .map_err(|_| error)?;
301 if found != root {
302 return Err(
303 "loom-devs launch-node binding conflicts with newly created root"
304 .to_owned(),
305 );
306 }
307 root
308 }
309 }
310 };
311
312 let node = services.access_kmap.get_node(context, root)?;
313 if node.title != ROOT_TITLE
314 || node.navigation_hint != ROOT_HINT
315 || node.narrative != ROOT_NARRATIVE
316 || !node.connections.is_empty()
317 {
318 return Err("existing loom-devs Kmap root conflicts with bootstrap root".to_owned());
319 }
320
321 Ok(root)
322}
323
324fn verify_inventory(
325 prepared: &[PreparedPackage],
326 imported: &[kcode_k1_rust_bootstrap_import::ImportedPackage],
327) -> Result<(), String> {
328 if prepared.len() != imported.len()
329 || !prepared.iter().zip(imported).all(|(left, right)| {
330 left.logical_name() == right.logical_name()
331 && left.version() == right.version()
332 && left.source_sha256() == right.source_sha256()
333 })
334 {
335 return Err("imported Rust package inventory differs from prepared closure".to_owned());
336 }
337
338 Ok(())
339}