Skip to content
516 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

API reference examples use projects1//! Workspaces and their members.
2//!
3//! A workspace owns repositories and is the first segment of their URLs.
4//! There is one kind: a person's own space and a company's differ only in
5//! how many members they have. Nothing can be created outside one.
6
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look7use g1t_contracts::access::BasePermission;
Merge main (membership, two-factor, GitHub repo roles) into tokens8use g1t_contracts::audit::Surface;
API reference examples use projects9use g1t_contracts::identity::*;
Merge main (membership, two-factor, GitHub repo roles) into tokens10use g1t_contracts::members::{LeaveWorkspaceArgs, last_owner_refusal};
Merge branch 'worktree-agent-ad7c6d88d93adc817'11use g1t_contracts::teams::TeamCreation;
API reference examples use projects12use g1t_contracts::time::rfc3339;
13use g1t_contracts::{
Merge main (membership, two-factor, GitHub repo roles) into tokens14 FailureCode, MemberPrivileges, Membership, OrgRole, Outcome, PrincipalKind, Role, User, claimable_namespace, new_id,
API reference examples use projects15};
16use g1t_kit::now_ms;
17use serde::Deserialize;
18use worker::Result;
19
20use crate::Identity;
21
22/// Enough for a person and their teams; stops one account claiming names in
23/// bulk.
24const MAX_WORKSPACES_PER_USER: usize = 10;
25
26const MAX_NAME_LENGTH: usize = 80;
27const MAX_DESCRIPTION_LENGTH: usize = 160;
28
29const WORKSPACE_COLUMNS: &str = "workspaces.id, workspaces.slug, workspaces.name,
Merge branch 'worktree-agent-ad7c6d88d93adc817'30 workspaces.description, workspaces.avatar, workspaces.created_at, workspaces.base_permission, workspaces.team_creation,
Merge main (membership, two-factor, GitHub repo roles) into tokens31 workspaces.member_privileges, workspaces.require_two_factor,
API reference examples use projects32 (SELECT count(*) FROM workspace_members
33 WHERE workspace_members.workspace_id = workspaces.id) AS member_count";
34
35#[derive(Deserialize)]
36struct WorkspaceRow {
37 id: String,
38 slug: String,
39 name: String,
40 description: Option<String>,
Workspace names and icons, and a component kit for every control41 avatar: Option<String>,
API reference examples use projects42 created_at: String,
43 member_count: u32,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look44 #[serde(default)]
45 base_permission: Option<String>,
Merge branch 'worktree-agent-ad7c6d88d93adc817'46 #[serde(default)]
47 team_creation: Option<String>,
Merge main (membership, two-factor, GitHub repo roles) into tokens48 #[serde(default)]
49 member_privileges: Option<String>,
50 #[serde(default)]
51 require_two_factor: Option<u8>,
API reference examples use projects52}
53
54impl From<WorkspaceRow> for Workspace {
55 fn from(row: WorkspaceRow) -> Self {
56 Workspace {
57 id: row.id,
58 slug: row.slug,
59 name: row.name,
60 description: row.description,
61 created_at: row.created_at,
62 member_count: row.member_count,
Workspace names and icons, and a component kit for every control63 avatar: row.avatar,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look64 base_permission: row
65 .base_permission
66 .as_deref()
67 .and_then(BasePermission::parse)
68 .unwrap_or_default(),
Merge branch 'worktree-agent-ad7c6d88d93adc817'69 team_creation: row
70 .team_creation
71 .as_deref()
72 .and_then(TeamCreation::parse)
73 .unwrap_or_default(),
Merge main (membership, two-factor, GitHub repo roles) into tokens74 privileges: MemberPrivileges::from_stored(row.member_privileges.as_deref()),
75 two_factor_requirement_enabled: row.require_two_factor.unwrap_or(0) != 0,
API reference examples use projects76 }
77 }
78}
79
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)80/// The name a new account's own workspace takes: its username, when that
81/// can name a workspace at all (g1t's own names cannot).
82pub(crate) fn own_workspace_slug(username: &str) -> Option<String> {
83 claimable_namespace(username)
84}
85
Merge main (membership, two-factor, GitHub repo roles) into tokens86/// One of a person's memberships, as stored.
API reference examples use projects87#[derive(Deserialize)]
Merge main (membership, two-factor, GitHub repo roles) into tokens88struct MembershipRow {
89 slug: String,
API reference examples use projects90 role: Role,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look91 name: Option<String>,
92 avatar: Option<String>,
Merge main (membership, two-factor, GitHub repo roles) into tokens93 base_permission: Option<String>,
94 team_creation: Option<String>,
95 member_privileges: Option<String>,
96 #[serde(default)]
97 billing_manager: u8,
98 #[serde(default)]
99 security_manager: u8,
100 #[serde(default)]
101 require_two_factor: u8,
API reference examples use projects102}
103
104impl Identity {
105 /// The workspaces a user belongs to, attached to every user resolved
Merge main (membership, two-factor, GitHub repo roles) into tokens106 /// from credentials, with what the site needs to show each one, what
107 /// members get on its repositories (access.rs), the roles the person
108 /// holds besides member (members.rs), and its member privileges.
API reference examples use projects109 pub async fn memberships(&self, user_id: &str) -> Result<Vec<Membership>> {
Merge main (membership, two-factor, GitHub repo roles) into tokens110 Ok(self.memberships_and_policies(user_id).await?.into_iter().map(|(membership, _)| membership).collect())
111 }
112
113 /// The same, each with whether its workspace requires two-factor
114 /// authentication (security.rs).
115 pub async fn memberships_and_policies(&self, user_id: &str) -> Result<Vec<(Membership, bool)>> {
116 let rows = self
117 .db
API reference examples use projects118 .prepare(
Workspace names and icons, and a component kit for every control119 "SELECT workspaces.slug, workspace_members.role, workspaces.name,
Merge main (membership, two-factor, GitHub repo roles) into tokens120 workspaces.avatar, workspaces.base_permission, workspaces.team_creation,
121 workspaces.member_privileges, workspaces.require_two_factor,
122 workspace_members.billing_manager, workspace_members.security_manager
Workspace names and icons, and a component kit for every control123 FROM workspace_members
API reference examples use projects124 JOIN workspaces ON workspaces.id = workspace_members.workspace_id
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member125 WHERE workspace_members.user_id = ? AND workspaces.deleted_at IS NULL
126 ORDER BY workspaces.slug",
API reference examples use projects127 )
128 .bind(&[user_id.into()])?
129 .all()
130 .await?
Merge main (membership, two-factor, GitHub repo roles) into tokens131 .results::<MembershipRow>()?;
132 Ok(rows
133 .into_iter()
134 .map(|row| {
135 let mut org_roles = Vec::new();
136 if row.billing_manager != 0 {
137 org_roles.push(OrgRole::BillingManager);
138 }
139 if row.security_manager != 0 {
140 org_roles.push(OrgRole::SecurityManager);
141 }
142 let membership = Membership {
143 slug: row.slug,
144 role: row.role,
145 name: row.name,
146 avatar: row.avatar,
147 base_permission: row.base_permission.as_deref().and_then(BasePermission::parse),
148 team_creation: row.team_creation.as_deref().and_then(TeamCreation::parse),
149 org_roles,
150 privileges: Some(MemberPrivileges::from_stored(row.member_privileges.as_deref())),
151 };
152 (membership, row.require_two_factor != 0)
153 })
154 .collect())
API reference examples use projects155 }
156
157 pub async fn create_workspace(&self, a: CreateWorkspaceArgs) -> Result<Outcome<Workspace>> {
158 if a.user.kind != PrincipalKind::User {
159 return Ok(Outcome::fail(
160 FailureCode::Forbidden,
161 "A workspace's access token cannot create workspaces. Sign in as a person.",
162 ));
163 }
164 if !a.user.verified {
165 return Ok(Outcome::fail(
166 FailureCode::Forbidden,
167 "Confirm your email address before creating a workspace.",
168 ));
169 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent170 let Some(slug) = claimable_namespace(&a.slug) else {
API reference examples use projects171 return Ok(Outcome::fail(
172 FailureCode::Invalid,
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent173 "Workspace names use lowercase letters, digits and single hyphens, up to 39 characters, and cannot be a reserved word.",
API reference examples use projects174 ));
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent175 };
API reference examples use projects176 if self.memberships(&a.user.id).await?.len() >= MAX_WORKSPACES_PER_USER {
177 return Ok(Outcome::fail(
178 FailureCode::Conflict,
179 "You belong to the maximum number of workspaces.",
180 ));
181 }
Merge Stripe Tax, the card fee on card payments, and one free workspace per person182 // One free workspace per person (paid.rs): a new one starts free.
183 if let Some(refused) = self.second_free_workspace(&a.user.id).await? {
184 return Ok(refused);
185 }
API reference examples use projects186 // Usernames and workspaces share one namespace: a person's username
187 // is theirs to use for a workspace, and nobody else's.
188 let someone_elses_username = self
189 .db
190 .prepare("SELECT id FROM users WHERE username = ? AND id != ?")
191 .bind(&[slug.as_str().into(), a.user.id.as_str().into()])?
192 .first::<serde_json::Value>(None)
193 .await?
194 .is_some();
195 if someone_elses_username
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member196 // A deleted workspace still holds its slug until it is purged.
197 || self.slug_in_use(&slug).await?
Agents and memory, checks and conflicts, profiles, slug renames, custom domains198 // A renamed workspace's old slug stays reserved for it a while.
199 || self.slug_held(&slug).await?
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look200 // A deleted workspace's slug is never given to anyone else; the
201 // person whose username it is may use it again.
202 || (self.slug_deleted(&slug).await?
203 && !crate::deletion::may_reclaim(&slug, &a.user.username))
API reference examples use projects204 {
205 return Ok(Outcome::fail(
206 FailureCode::Conflict,
207 "That workspace name is taken.",
208 ));
209 }
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)210 let name = match a.name.trim() {
211 "" => slug.clone(),
212 name => name.chars().take(MAX_NAME_LENGTH).collect(),
213 };
214 Ok(Outcome::Ok(self.insert_workspace(&a.user.id, slug, name).await?))
215 }
216
217 /// A new account's own workspace, named for its username, on the free
218 /// plan as every new workspace is: so nobody is left without one. Made
219 /// only when the username is free to use as a workspace's name (it
220 /// usually is: usernames and workspaces share one namespace). None
221 /// when it is not, and the site asks the person to make one.
222 pub(crate) async fn create_own_workspace(&self, user: &User) -> Result<Option<Workspace>> {
223 let Some(slug) = own_workspace_slug(&user.username) else {
224 return Ok(None);
225 };
226 if self.slug_in_use(&slug).await?
227 || self.slug_held(&slug).await?
228 || (self.slug_deleted(&slug).await? && !crate::deletion::may_reclaim(&slug, &user.username))
229 {
230 return Ok(None);
231 }
232 let made = self.insert_workspace(&user.id, slug.clone(), slug).await;
233 match made {
234 Ok(workspace) => Ok(Some(workspace)),
235 // Taken a moment ago: the person makes one themselves.
236 Err(error) if error.to_string().contains("UNIQUE") => Ok(None),
237 Err(error) => Err(error),
238 }
239 }
240
241 /// Makes a workspace `user_id` owns, once every check has passed.
242 async fn insert_workspace(&self, user_id: &str, slug: String, name: String) -> Result<Workspace> {
API reference examples use projects243 let now = now_ms();
244 let workspace = Workspace {
245 id: new_id("wsp", now),
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)246 name,
API reference examples use projects247 description: None,
248 slug,
249 created_at: rfc3339(now),
250 member_count: 1,
Workspace names and icons, and a component kit for every control251 avatar: None,
Merge main (membership, two-factor, GitHub repo roles) into tokens252 // Read, as on GitHub: an owner widens it on People.
253 base_permission: BasePermission::FOR_NEW_WORKSPACES,
Merge branch 'worktree-agent-ad7c6d88d93adc817'254 team_creation: TeamCreation::default(),
Merge main (membership, two-factor, GitHub repo roles) into tokens255 privileges: MemberPrivileges::default(),
256 two_factor_requirement_enabled: false,
API reference examples use projects257 };
258 self.db
259 .batch(vec![
260 self.db
261 .prepare(
Merge main (membership, two-factor, GitHub repo roles) into tokens262 "INSERT INTO workspaces (id, slug, name, created_by, created_at, base_permission)
263 VALUES (?, ?, ?, ?, ?, ?)",
API reference examples use projects264 )
265 .bind(&[
266 workspace.id.as_str().into(),
267 workspace.slug.as_str().into(),
268 workspace.name.as_str().into(),
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)269 user_id.into(),
API reference examples use projects270 workspace.created_at.as_str().into(),
Merge main (membership, two-factor, GitHub repo roles) into tokens271 workspace.base_permission.as_str().into(),
API reference examples use projects272 ])?,
273 self.db
274 .prepare(
275 "INSERT INTO workspace_members (workspace_id, user_id, role, created_at)
276 VALUES (?, ?, 'owner', ?)",
277 )
278 .bind(&[
279 workspace.id.as_str().into(),
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)280 user_id.into(),
API reference examples use projects281 workspace.created_at.as_str().into(),
282 ])?,
283 ])
284 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look285 self.forget_deleted(&workspace.slug).await?;
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)286 Ok(workspace)
API reference examples use projects287 }
288
289 pub async fn get_workspace(&self, a: SlugArgs) -> Result<Option<Workspace>> {
290 Ok(self
291 .db
292 .prepare(format!(
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member293 "SELECT {WORKSPACE_COLUMNS} FROM workspaces WHERE slug = ? AND deleted_at IS NULL"
API reference examples use projects294 ))
295 .bind(&[a.slug.to_lowercase().into()])?
296 .first::<WorkspaceRow>(None)
297 .await?
298 .map(Workspace::from))
299 }
300
301 pub async fn update_workspace(&self, a: UpdateWorkspaceArgs) -> Result<Outcome<Workspace>> {
302 let slug = a.slug.to_lowercase();
303 if a.actor.kind != PrincipalKind::User || a.actor.role_in(&slug) != Some(Role::Owner) {
304 return Ok(Outcome::fail(
305 FailureCode::Forbidden,
306 "Only an owner can change a workspace's details.",
307 ));
308 }
309 let name: String = match a.name.trim() {
310 "" => slug.clone(),
311 name => name.chars().take(MAX_NAME_LENGTH).collect(),
312 };
313 let description: String = a
314 .description
315 .trim()
316 .chars()
317 .take(MAX_DESCRIPTION_LENGTH)
318 .collect();
319 self.db
320 .prepare("UPDATE workspaces SET name = ?, description = ? WHERE slug = ?")
321 .bind(&[
322 name.into(),
323 if description.is_empty() {
324 worker::wasm_bindgen::JsValue::NULL
325 } else {
326 description.into()
327 },
328 slug.as_str().into(),
329 ])?
330 .run()
331 .await?;
332 Ok(match self.get_workspace(SlugArgs { slug }).await? {
333 Some(workspace) => Outcome::Ok(workspace),
334 None => Outcome::fail(FailureCode::NotFound, "Workspace not found."),
335 })
336 }
337
338 pub async fn list_members(&self, a: ListMembersArgs) -> Result<Outcome<Vec<Member>>> {
339 let slug = a.slug.to_lowercase();
Merge main (membership, two-factor, GitHub repo roles) into tokens340 let Some(viewer) = a.viewer.filter(|viewer| viewer.is_member(&slug)) else {
API reference examples use projects341 return Ok(Outcome::fail(
342 FailureCode::Forbidden,
343 "Only members can see who is in a workspace.",
344 ));
Merge main (membership, two-factor, GitHub repo roles) into tokens345 };
346 // Owners see who has two-factor authentication on, as they need to
347 // before requiring it.
348 let owner = viewer.role_in(&slug) == Some(Role::Owner);
349 #[derive(Deserialize)]
350 struct Row {
351 #[serde(flatten)]
352 member: crate::members::MemberRow,
353 #[serde(default)]
354 two_factor: u8,
API reference examples use projects355 }
356 let rows = self
357 .db
358 .prepare(
Cards you act on in chat; agents comment and review as themselves; names shown cleanly; commits on the calendar359 "SELECT workspace_members.user_id, users.username, users.display_username, workspace_members.role, users.display_name AS name, users.avatar,
Merge main (membership, two-factor, GitHub repo roles) into tokens360 workspace_members.billing_manager, workspace_members.security_manager,
361 EXISTS (SELECT 1 FROM two_factor WHERE two_factor.user_id = users.id AND two_factor.enabled_at IS NOT NULL) AS two_factor
362 FROM workspace_members
API reference examples use projects363 JOIN users ON users.id = workspace_members.user_id
364 JOIN workspaces ON workspaces.id = workspace_members.workspace_id
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member365 WHERE workspaces.slug = ? AND workspaces.deleted_at IS NULL
API reference examples use projects366 ORDER BY workspace_members.role DESC, users.username",
367 )
368 .bind(&[slug.into()])?
369 .all()
370 .await?
Merge main (membership, two-factor, GitHub repo roles) into tokens371 .results::<Row>()?;
API reference examples use projects372 Ok(Outcome::Ok(
373 rows.into_iter()
Merge main (membership, two-factor, GitHub repo roles) into tokens374 .map(|row| row.member.member(owner.then_some(row.two_factor != 0)))
API reference examples use projects375 .collect(),
376 ))
377 }
378
379 /// The ids needed to change a workspace's members, if `actor` owns it
380 /// and `username` exists.
381 async fn member_target(&self, a: &MemberArgs) -> Result<Outcome<(String, User)>> {
382 let slug = a.slug.to_lowercase();
383 if a.actor.kind != PrincipalKind::User || a.actor.role_in(&slug) != Some(Role::Owner) {
384 return Ok(Outcome::fail(
385 FailureCode::Forbidden,
386 "Only an owner can change a workspace's members.",
387 ));
388 }
389 let Some(workspace) = self.get_workspace(SlugArgs { slug }).await? else {
390 return Ok(Outcome::fail(FailureCode::NotFound, "Workspace not found."));
391 };
392 let Some(user) = self
393 .find_public_user(
394 "SELECT id, username, email_verified_at IS NOT NULL AS verified
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)395 FROM users WHERE username = ? AND deleted_at IS NULL",
API reference examples use projects396 &a.username.trim().to_lowercase(),
397 )
398 .await?
399 else {
400 return Ok(Outcome::fail(
401 FailureCode::NotFound,
402 "There is no account with that username.",
403 ));
404 };
405 Ok(Outcome::Ok((workspace.id, user)))
406 }
407
408 pub async fn add_member(&self, a: MemberArgs) -> Result<Outcome<bool>> {
409 let (workspace_id, user) = match self.member_target(&a).await? {
410 Outcome::Ok(target) => target,
411 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
412 };
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)413 // A person is never added without saying yes: they get a workspace
414 // invitation to accept or decline (invites/invitations.rs). Only
415 // g1t's own agent is added at once.
416 if !crate::paid::is_g1t(&user.username) {
417 let invited = self
418 .invite_member(InviteMemberArgs {
419 actor: a.actor,
420 slug: a.slug,
421 email: String::new(),
422 username: Some(user.username),
423 role: None,
Workspace is the workspace's settings, in one place. Its sidebar is grouped, General, Access, Money, Compute, Code, Agents, Chat, Artifacts, Security and Integrations, every page one click away with no settings inside settings, the groups folding and the owner-only pages hidden from members; what is not here yet is marked Soon with a hint. Members is a list with search and filters for role, two-factor and team, and Invite opens a dialog: who, by username, name or email, their role and a note that goes into the invitation; nothing is filled in inline any more. Invitations is its own page with All, Pending, Accepted, Declined, Expired and Revoked filters that say how many, and each row's menu can copy the link, send it again or revoke it; identity learned to send an invitation again and to carry the note. Permissions gathers every rule about who may do what, by part: Code's base permission and member privileges, who creates teams, who creates channels, and, marked Soon with what applies today, forking private repositories, adding agents to conversations, messaging agents directly, creating spaces and default sharing, creating workspace agents and raising budgets, deploying to production and publishing packages. General lost what moved. The permissions guide is new, and the workspaces, people and teams, access, chat, teams, authentication and billing guides say where things are now.424 message: None,
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)425 surface: a.surface,
426 })
427 .await?;
428 return Ok(match invited {
429 Outcome::Ok(_) => Outcome::Ok(true),
430 Outcome::Fail(failure) => Outcome::Fail(failure),
431 });
432 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look433 // What the workspace asks of its members (security.rs); nothing yet.
434 if let Some(why) = self.policy_refusal(&user.id, &a.slug.to_lowercase()).await? {
435 return Ok(Outcome::fail(FailureCode::Forbidden, why));
436 }
Merge Stripe Tax, the card fee on card payments, and one free workspace per person437 // A free workspace adds no one until it starts the plan (paid.rs);
438 // g1t's agent is never someone added.
439 if !crate::paid::is_g1t(&user.username)
440 && let Some(refused) = self.free_workspace_refusal(&a.slug).await?
441 {
442 return Ok(refused);
443 }
Merge main (membership, two-factor, GitHub repo roles) into tokens444 let added = self
445 .db
API reference examples use projects446 .prepare(
447 "INSERT OR IGNORE INTO workspace_members (workspace_id, user_id, role, created_at)
Merge main (membership, two-factor, GitHub repo roles) into tokens448 VALUES (?, ?, 'member', ?) RETURNING user_id",
API reference examples use projects449 )
450 .bind(&[
451 workspace_id.into(),
Merge main (membership, two-factor, GitHub repo roles) into tokens452 user.id.as_str().into(),
API reference examples use projects453 rfc3339(now_ms()).into(),
454 ])?
Merge main (membership, two-factor, GitHub repo roles) into tokens455 .first::<serde_json::Value>(None)
456 .await?
457 .is_some();
458 if added {
459 self.audit_workspace(
460 &a.actor,
461 "member.added",
462 &a.slug.to_lowercase(),
463 a.surface.unwrap_or(Surface::Web),
464 format!("Added {} as a member", user.username),
465 )
466 .await;
467 }
API reference examples use projects468 Ok(Outcome::Ok(true))
469 }
470
471 pub async fn remove_member(&self, a: MemberArgs) -> Result<Outcome<bool>> {
Merge main (membership, two-factor, GitHub repo roles) into tokens472 // Removing yourself is leaving, which anyone may do.
473 if a.username.trim().trim_start_matches('@').eq_ignore_ascii_case(&a.actor.username) {
474 return self
475 .leave_workspace(LeaveWorkspaceArgs { user: a.actor, slug: a.slug, surface: a.surface })
476 .await;
477 }
API reference examples use projects478 let (workspace_id, user) = match self.member_target(&a).await? {
479 Outcome::Ok(target) => target,
480 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
481 };
Merge main (membership, two-factor, GitHub repo roles) into tokens482 let slug = a.slug.to_lowercase();
483 let Some(row) = self.member_row(&workspace_id, &user.username).await? else {
484 return Ok(Outcome::Ok(true));
485 };
486 if row.role == Role::Owner
487 && let Some(why) = last_owner_refusal(self.owner_count(&workspace_id).await?, true)
488 {
489 return Ok(Outcome::fail(FailureCode::Conflict, why));
API reference examples use projects490 }
Merge main (membership, two-factor, GitHub repo roles) into tokens491 self.drop_member(&workspace_id, &user.id).await?;
492 self.audit_workspace(
493 &a.actor,
494 "member.removed",
495 &slug,
496 a.surface.unwrap_or(Surface::Web),
497 format!("Removed {} from the workspace", user.username),
498 )
499 .await;
API reference examples use projects500 Ok(Outcome::Ok(true))
501 }
502}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
509 fn no_workspace_is_created_with_g1ts_names() {
510 // What create_workspace takes the slug through, whatever its case.
511 for slug in ["g1t", "G1T", " g1t-agent ", "G1T-Agent"] {
512 assert_eq!(claimable_namespace(slug), None, "{slug}");
513 }
514 assert_eq!(claimable_namespace("Acme").as_deref(), Some("acme"));
515 }
516}

This file's history is long; its oldest lines are credited to the oldest commit read.