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_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}