Skip to main content

st/collab/
mod.rs

1//! Collaboration Module - Humans + AIs working together
2//!
3//! Each user (human or AI) brings their own skills to the project.
4//! Users connect via i1.is or GitHub identity, get their own space
5//! (optionally containerized), and collaborate in real-time.
6//!
7//! ## Philosophy
8//! - **Work by default** - Don't block people, enable them
9//! - **Bring your skills** - Each collaborator has unique abilities
10//! - **Any machine** - Connect to any std daemon you have access to
11
12pub mod identity;
13pub mod permissions;
14pub mod space;
15pub mod templates;
16
17pub use identity::{Identity, IdentityProvider};
18pub use permissions::{AccessLevel, Permission, ProjectAccess};
19pub use space::{UserSpace, SpaceConfig};
20pub use templates::Template;
21
22use anyhow::Result;
23use serde::{Deserialize, Serialize};
24use std::path::PathBuf;
25
26/// A collaborator - human or AI
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Collaborator {
29    /// Their identity (i1.is, GitHub, or local)
30    pub identity: Identity,
31
32    /// What kind of collaborator
33    pub kind: CollaboratorKind,
34
35    /// Their preferred template/environment
36    pub template: Option<String>,
37
38    /// Skills they bring (for AI: tools they can use, for humans: expertise)
39    pub skills: Vec<String>,
40
41    /// Current status
42    pub status: CollaboratorStatus,
43}
44
45/// Type of collaborator
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
47pub enum CollaboratorKind {
48    /// Human developer
49    Human,
50    /// AI assistant (Claude, etc.)
51    Ai { model: String },
52    /// Automated system (CI, bots)
53    System,
54}
55
56/// Current status of a collaborator
57#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
58pub enum CollaboratorStatus {
59    /// Currently active in the space
60    Active,
61    /// Connected but idle
62    Idle,
63    /// Not currently connected
64    Offline,
65    /// Invited but hasn't joined yet
66    Invited,
67}
68
69/// A collaborative project/workspace
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct Project {
72    /// Project identifier (e.g., "hue@i1.is/smart-tree")
73    pub id: String,
74
75    /// Display name
76    pub name: String,
77
78    /// Owner identity
79    pub owner: Identity,
80
81    /// Path on the host machine
82    pub path: PathBuf,
83
84    /// Collaborators with their access levels
85    pub collaborators: Vec<(Collaborator, AccessLevel)>,
86
87    /// Project-level settings
88    pub settings: ProjectSettings,
89}
90
91/// Project settings
92#[derive(Debug, Clone, Serialize, Deserialize, Default)]
93pub struct ProjectSettings {
94    /// Allow container isolation for users
95    pub allow_containers: bool,
96
97    /// Default template for new collaborators
98    pub default_template: Option<String>,
99
100    /// Sync memories to i1.is cloud
101    pub cloud_sync: bool,
102
103    /// Specific denies (not allows - work by default!)
104    pub deny_patterns: Vec<String>,
105}
106
107impl Project {
108    /// Create a new project
109    pub fn new(name: &str, owner: Identity, path: PathBuf) -> Self {
110        let id = format!("{}/{}", owner, name);
111        Project {
112            id,
113            name: name.to_string(),
114            owner,
115            path,
116            collaborators: Vec::new(),
117            settings: ProjectSettings::default(),
118        }
119    }
120
121    /// Add a collaborator
122    pub fn add_collaborator(&mut self, collab: Collaborator, access: AccessLevel) {
123        self.collaborators.push((collab, access));
124    }
125
126    /// Check if an identity has access
127    pub fn can_access(&self, identity: &Identity) -> bool {
128        // Owner always has access
129        if &self.owner == identity {
130            return true;
131        }
132
133        // Check collaborators
134        self.collaborators.iter().any(|(c, _)| &c.identity == identity)
135    }
136
137    /// Get access level for an identity
138    pub fn access_level(&self, identity: &Identity) -> AccessLevel {
139        if &self.owner == identity {
140            return AccessLevel::Owner;
141        }
142
143        self.collaborators
144            .iter()
145            .find(|(c, _)| &c.identity == identity)
146            .map(|(_, level)| level.clone())
147            .unwrap_or(AccessLevel::None)
148    }
149}
150
151/// Join a remote project
152pub async fn join_project(project_id: &str) -> Result<Project> {
153    // Parse project ID: "user@i1.is/project" or "github:user/project"
154    let (owner_str, project_name) = project_id
155        .rsplit_once('/')
156        .ok_or_else(|| anyhow::anyhow!("Invalid project ID format"))?;
157
158    let owner = Identity::parse(owner_str)?;
159
160    // TODO: Connect to remote daemon via STUN/relay
161    // For now, return a placeholder
162    Ok(Project::new(project_name, owner, PathBuf::from(".")))
163}