Skip to content
1,083 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 and MCP server, Rust identity service, registration, site redesign1//! The identity service: accounts, sessions, SSH keys and access tokens.
2//!
3//! Reached only through service bindings; see `g1t_contracts::identity` for
4//! the methods and their arguments.
5
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look6mod access;
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)7mod account_deletion;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace8mod admin;
Merge branch 'worktree-agent-a8385d293d42c913a'9mod aliases;
Workspace names and icons, and a component kit for every control10mod avatars;
API and MCP server, Rust identity service, registration, site redesign11mod crypto;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look12mod deletion;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca13mod deploy_keys;
Device sign-in replaces registering and minting tokens over the API14mod device;
Search across all of g1t, Explore, and a command palette15mod directory;
Email verification, password reset, and Git for AI scale positioning16mod email;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look17mod emails;
18mod github;
19mod invites;
Merge main (membership, two-factor, GitHub repo roles) into tokens20mod members;
OAuth 2.1 sign-in for MCP clients and other applications21mod oauth;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person22mod paid;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains23mod profiles;
24mod rename;
Merge branch 'worktree-agent-a3abfcce648e87dca'25mod job_tokens;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API26mod run_credentials;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look27mod security;
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)28mod shared_invites;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar29mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look30mod throttle;
Fine-grained personal tokens, workspace token rules and approvals in identity31mod token_reach;
Agents as a team: lifecycle, merge queue, billing and a new shell32mod tokens;
Merge main (membership, two-factor, GitHub repo roles) into tokens33mod two_factor;
Workspaces own repositories34mod workspaces;
API and MCP server, Rust identity service, registration, site redesign35
36use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos37use g1t_contracts::time::{SQL_NOW, rfc3339, sql_after};
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent38use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign39use g1t_kit::{args, now_ms, reply, rpc_method};
40use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell41use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign42use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas43use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign44
RFC 3339 timestamps in identity and repos45const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
46const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign47const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning48const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
49
50/// A user as selected from the database; `verified` arrives as 0 or 1.
51#[derive(Deserialize)]
52struct Account {
53 id: String,
54 username: String,
55 verified: u8,
Workspace names and icons, and a component kit for every control56 /// Selected only where the person is being shown to themselves.
57 #[serde(default)]
58 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning59}
60
61impl From<Account> for User {
62 fn from(row: Account) -> Self {
63 User {
64 id: row.id,
65 username: row.username,
66 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control67 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell68 ..User::default()
Email verification, password reset, and Git for AI scale positioning69 }
70 }
71}
API and MCP server, Rust identity service, registration, site redesign72
73#[derive(Deserialize)]
74struct UserRow {
75 id: String,
76 username: String,
77 password_hash: String,
Email verification, password reset, and Git for AI scale positioning78 verified: u8,
API and MCP server, Rust identity service, registration, site redesign79}
80
Email verification, password reset, and Git for AI scale positioning81/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign82#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning83struct TokenOwner {
84 id: String,
85 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look86 /// The address a link was sent to; null on links from before accounts
87 /// had several, which are for the primary.
88 #[serde(default)]
89 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning90}
91
92#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign93struct KeyRow {
94 id: String,
95 title: String,
96 fingerprint: String,
RFC 3339 timestamps in identity and repos97 created_at: String,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca98 #[serde(default)]
99 last_used_at: Option<String>,
API and MCP server, Rust identity service, registration, site redesign100}
101
102impl From<KeyRow> for SshKey {
103 fn from(row: KeyRow) -> Self {
104 SshKey {
105 id: row.id,
106 title: row.title,
107 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos108 created_at: row.created_at,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca109 last_used_at: row.last_used_at,
API and MCP server, Rust identity service, registration, site redesign110 }
111 }
112}
113
114struct Identity {
115 db: D1Database,
Email verification, password reset, and Git for AI scale positioning116 env: Env,
API and MCP server, Rust identity service, registration, site redesign117}
118
119impl Identity {
Workspaces own repositories120 /// Runs a query that returns at most one user, for showing to others:
121 /// without their workspaces.
122 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning123 Ok(self
124 .db
API and MCP server, Rust identity service, registration, site redesign125 .prepare(sql)
126 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning127 .first::<Account>(None)
128 .await?
129 .map(User::from))
130 }
131
Workspaces own repositories132 /// Attaches the workspaces a user belongs to, so that any service can
133 /// authorize them without asking again.
134 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
135 let Some(mut user) = user else {
136 return Ok(None);
137 };
Merge main (membership, two-factor, GitHub repo roles) into tokens138 let memberships = self.memberships_and_policies(&user.id).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look139 // Roles on single repositories, under the same policy (access.rs).
140 let grants = self.grants_of(&user.id).await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens141 // Access to a workspace is used only within its policy; see security.rs.
142 let within = self.within_policy(&user.id, memberships, grants).await?;
143 user.workspaces = within.memberships;
144 user.grants = within.grants;
145 user.held = within.held;
Workspaces own repositories146 Ok(Some(user))
147 }
148
149 /// Runs a query that resolves credentials to at most one user.
150 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
151 let user = self.find_public_user(sql, param).await?;
152 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign153 }
154
Email verification, password reset, and Git for AI scale positioning155 /// Consumes a token of `kind`, returning its owner if it was valid.
156 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
157 let id = crypto::sha256_hex(token);
158 let owner = self
159 .db
RFC 3339 timestamps in identity and repos160 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look161 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning162 JOIN users ON users.id = email_tokens.user_id
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)163 WHERE email_tokens.id = ? AND email_tokens.kind = ? AND users.deleted_at IS NULL
RFC 3339 timestamps in identity and repos164 AND email_tokens.expires_at > {SQL_NOW}"
165 ))
Email verification, password reset, and Git for AI scale positioning166 .bind(&[id.as_str().into(), kind.into()])?
167 .first::<TokenOwner>(None)
168 .await?;
169 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look170 // Every outstanding token of this kind dies with the one used:
171 // every reset link, and every confirmation link for the same
172 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning173 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look174 .prepare(
175 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
176 AND (?2 = 'reset' OR email_id IS ?3)",
177 )
178 .bind(&[
179 owner.id.as_str().into(),
180 kind.into(),
181 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
182 ])?
Email verification, password reset, and Git for AI scale positioning183 .run()
184 .await?;
185 }
186 Ok(owner)
187 }
188
189 async fn resend_verification(&self, a: UserArgs) -> Result<Outcome<bool>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look190 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
191 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
192 }
193 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning194 }
195
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)196 /// The link in a confirmation email, followed: signed in or not. It
197 /// ends the code sent with it (emails.rs).
198 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<g1t_contracts::accounts::EmailConfirmed>> {
Email verification, password reset, and Git for AI scale positioning199 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
200 return Ok(Outcome::fail(
201 FailureCode::Invalid,
202 "This confirmation link is not valid or has expired.",
203 ));
204 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)205 self.confirm_address(&owner.id, owner.email_id.as_deref()).await
Email verification, password reset, and Git for AI scale positioning206 }
207
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look208 /// Any confirmed address of an account can ask for a reset; so can the
209 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning210 async fn request_password_reset(&self, a: EmailArgs) -> Result<bool> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look211 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
212 && match a.client.as_deref() {
213 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
214 None => true,
215 };
216 if allowed && let Some(target) = self.reset_target(&a.email).await? {
Identity: a password reset for a known address that fails to save or send is logged, never answered, so the reply never says an account exists217 // A failure from here on happens only for a real account, so it
218 // is logged, never answered: the reply below stays the same.
219 if let Err(error) = self.send_reset(&target).await {
220 worker::console_error!("password reset for a known address failed: {error}");
221 }
222 }
223 // The same answer either way, so addresses cannot be probed.
224 Ok(true)
225 }
226
227 /// Saves a reset link for `target` and mails it, telling the account's
228 /// other addresses.
229 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
230 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look231 let token = crypto::random_hex(32);
232 self.db
233 .prepare(format!(
234 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
235 VALUES (?, ?, 'reset', {}, ?)",
236 sql_after(RESET_TTL_SECONDS)
237 ))
238 .bind(&[
239 crypto::sha256_hex(&token).into(),
240 target.user_id.as_str().into(),
241 target.email_id.as_str().into(),
242 ])?
243 .run()
Email verification, password reset, and Git for AI scale positioning244 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look245 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
246 // The primary and the backup hear of it when it went elsewhere.
247 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
248 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
249 let change = format!("A password reset was asked for through {}", target.display);
250 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
251 worker::console_error!("security notice failed: {error}");
252 }
253 }
Email verification, password reset, and Git for AI scale positioning254 }
Identity: a password reset for a known address that fails to save or send is logged, never answered, so the reply never says an account exists255 Ok(())
Email verification, password reset, and Git for AI scale positioning256 }
257
258 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
259 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
260 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
261 }
262 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
263 return Ok(Outcome::fail(
264 FailureCode::Invalid,
265 "This reset link is not valid or has expired.",
266 ));
267 };
268 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look269 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning270 .bind(&[
271 crypto::hash_password(&a.password).into(),
272 owner.id.as_str().into(),
273 ])?
274 .run()
275 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look276 // Following an emailed link also proves the address it went to
277 // (unless another account confirmed it first).
278 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
279 // Anyone signed in with the old password is signed out, and nobody
280 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning281 self.db
282 .prepare("DELETE FROM sessions WHERE user_id = ?")
283 .bind(&[owner.id.as_str().into()])?
284 .run()
285 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look286 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
287 self.log_security(&owner.id, "password_changed", None, None).await;
288 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
289 let verified = self
290 .find_public_user(
291 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
292 &owner.id,
293 )
294 .await?
295 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning296 Ok(Outcome::Ok(User {
297 id: owner.id,
298 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look299 verified,
Workspaces own repositories300 ..User::default()
Email verification, password reset, and Git for AI scale positioning301 }))
302 }
303
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look304 /// The account a login names: a username, or any confirmed address.
305 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
306 let login = login.trim().to_lowercase();
307 let (column, value) = if login.contains('@') {
308 match self.user_with_verified_email(&login).await? {
309 Some(id) => ("id", id),
310 None => return Ok(None),
311 }
312 } else {
313 ("username", login)
314 };
315 self.db
316 .prepare(format!(
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)317 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users
318 WHERE {column} = ? AND deleted_at IS NULL"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look319 ))
320 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign321 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look322 .await
323 }
324
325 /// Checks a password for a login, throttled (see throttle.rs). The
326 /// refusal is one of two messages, the same for every account.
327 async fn checked_password(
328 &self,
329 login: &str,
330 password: &str,
331 client: Option<&str>,
332 ) -> Result<std::result::Result<User, &'static str>> {
333 let row = self.password_row(login).await?;
334 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
335 let (account_key, client_key) = Identity::password_keys(&subject, client);
336 if self.password_locked(&account_key, client_key.as_deref()).await? {
337 return Ok(Err(throttle::THROTTLED));
338 }
339 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
340 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
341 Some(row) => {
342 self.clear(&account_key).await?;
343 Ok(Ok(User {
344 id: row.id,
345 username: row.username,
346 verified: row.verified != 0,
347 ..User::default()
348 }))
349 }
350 None => {
351 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
352 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
353 Ok(Err("Incorrect username or password."))
354 }
355 }
356 }
357
Merge main (membership, two-factor, GitHub repo roles) into tokens358 /// Git over HTTPS with the account's password. With two-factor
359 /// authentication on, a password alone is never enough: use an access
360 /// token (two_factor.rs).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look361 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
362 let user = self.checked_password(login, password, None).await?.ok();
Merge main (membership, two-factor, GitHub repo roles) into tokens363 if let Some(user) = &user
364 && self.two_factor_enabled(&user.id).await?
365 {
366 return Ok(None);
367 }
Workspaces own repositories368 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign369 }
370
371 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
372 let username = a.username.trim().to_lowercase();
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent373 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign374 let email = a.email.trim().to_lowercase();
375 let invalid = |message: &str| Ok(Outcome::fail(FailureCode::Invalid, message));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look376 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
377 // The invite first: without one, nothing else on the form matters.
378 if self.invites_required() && invite_code.is_none() {
379 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
380 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent381 if !claimable {
API and MCP server, Rust identity service, registration, site redesign382 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent383 "Usernames use lowercase letters, digits and single hyphens, up to 39 characters, and cannot be a reserved word.",
API and MCP server, Rust identity service, registration, site redesign384 );
385 }
386 let well_formed_email = email
387 .split_once('@')
388 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
389 && !email.contains(char::is_whitespace);
390 if !well_formed_email {
391 return invalid("Enter a valid email address.");
392 }
393 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning394 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign395 }
396 let taken = self
397 .db
Agents as a team: lifecycle, merge queue, billing and a new shell398 // Usernames and workspaces share one namespace, so that a name
399 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look400 // An address is taken once an account has confirmed it; an
401 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell402 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look403 "SELECT username FROM users WHERE username = ?
404 UNION ALL SELECT email FROM user_emails WHERE email = ? AND verified_at IS NOT NULL
Agents as a team: lifecycle, merge queue, billing and a new shell405 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
406 )
407 .bind(&[
408 username.as_str().into(),
409 email.as_str().into(),
410 username.as_str().into(),
411 ])?
API and MCP server, Rust identity service, registration, site redesign412 .first::<serde_json::Value>(None)
413 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look414 // A renamed workspace's old slug stays reserved for it a while, and
415 // a deleted workspace's for good.
416 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign417 return Ok(Outcome::fail(
418 FailureCode::Conflict,
419 "That username or email is already registered.",
420 ));
421 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look422 let password_hash = crypto::hash_password(&a.password);
423 let user = match self
424 .create_account(invites::NewAccount {
425 username: &username,
426 email: &email,
427 password_hash: &password_hash,
428 verified: false,
429 invite_code,
Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm430 email_proof: a.email_proof.as_deref(),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look431 client: a.client.as_deref(),
432 })
433 .await?
434 {
435 Outcome::Ok(user) => user,
436 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign437 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)438 // The account exists either way; the email can be sent again from
439 // the confirmation page. It carries a code and a link (emails.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas440 if !user.verified
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)441 && let Err(error) = self.send_primary_confirmation(&user.id, &user.username).await
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas442 {
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)443 worker::console_error!("confirmation email failed: {error}");
Email verification, password reset, and Git for AI scale positioning444 }
API and MCP server, Rust identity service, registration, site redesign445 self.start_session(user).await
446 }
447
448 async fn sign_in(&self, a: SignInArgs) -> Result<Outcome<SignedIn>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look449 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
450 Ok(user) => user,
451 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign452 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look453 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign454 self.start_session(user).await
455 }
456
Merge main (membership, two-factor, GitHub repo roles) into tokens457 /// Starts a session for someone who just proved their password (or
458 /// GitHub account). With two-factor authentication on, it starts none:
459 /// it returns a challenge for `two_factor_sign_in` (two_factor.rs).
API and MCP server, Rust identity service, registration, site redesign460 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
Merge main (membership, two-factor, GitHub repo roles) into tokens461 if self.two_factor_enabled(&user.id).await? {
462 let challenge = self.issue_challenge(&user.id).await?;
463 return Ok(Outcome::Ok(SignedIn {
464 user: User { workspaces: Vec::new(), grants: Vec::new(), held: Vec::new(), ..user },
465 session_token: String::new(),
466 two_factor_challenge: Some(challenge),
467 }));
468 }
469 self.session_for(user).await
470 }
471
472 /// A new session for `user`, who has proved who they are in full.
473 async fn session_for(&self, user: User) -> Result<Outcome<SignedIn>> {
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)474 // Whichever way it was proved, a deleted account starts none
475 // (account_deletion.rs).
476 if !self.account_live(&user.id).await? {
477 return Ok(Outcome::fail(FailureCode::Unauthenticated, "Incorrect username or password."));
478 }
API and MCP server, Rust identity service, registration, site redesign479 let session_token = crypto::random_hex(32);
480 self.db
RFC 3339 timestamps in identity and repos481 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look482 // Signing in is proof it is the person: see security.rs.
483 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos484 sql_after(SESSION_TTL_SECONDS)
485 ))
API and MCP server, Rust identity service, registration, site redesign486 .bind(&[
487 crypto::sha256_hex(&session_token).into(),
488 user.id.as_str().into(),
489 ])?
490 .run()
491 .await?;
492 Ok(Outcome::Ok(SignedIn {
493 user,
494 session_token,
Merge main (membership, two-factor, GitHub repo roles) into tokens495 two_factor_challenge: None,
API and MCP server, Rust identity service, registration, site redesign496 }))
497 }
498
499 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
500 self.db
501 .prepare("DELETE FROM sessions WHERE id = ?")
502 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
503 .run()
504 .await?;
505 Ok(())
506 }
507
508 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
509 self.find_user(
RFC 3339 timestamps in identity and repos510 &format!(
Workspace names and icons, and a component kit for every control511 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
512 users.avatar
RFC 3339 timestamps in identity and repos513 FROM sessions JOIN users ON users.id = sessions.user_id
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)514 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW} AND users.deleted_at IS NULL"
RFC 3339 timestamps in identity and repos515 ),
API and MCP server, Rust identity service, registration, site redesign516 &crypto::sha256_hex(&a.session_token),
517 )
518 .await
519 }
520
521 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
522 // Like GitHub, a token alone identifies its user.
523 if a.secret.starts_with(TOKEN_PREFIX) {
524 self.user_for_access_token(&a.secret).await
525 } else {
526 self.user_for_password(&a.username, &a.secret).await
527 }
528 }
529
530 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
531 self.find_user(
Email verification, password reset, and Git for AI scale positioning532 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified FROM ssh_keys
API and MCP server, Rust identity service, registration, site redesign533 JOIN users ON users.id = ssh_keys.user_id
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)534 WHERE fingerprint = ? AND users.deleted_at IS NULL",
API and MCP server, Rust identity service, registration, site redesign535 &a.fingerprint,
536 )
537 .await
538 }
539
540 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories541 self.find_public_user(
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)542 // A deleted account is nobody's to find, mention or add.
543 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ? AND deleted_at IS NULL",
API and MCP server, Rust identity service, registration, site redesign544 &a.username.to_lowercase(),
545 )
546 .await
547 }
548
Inbox: threads, reasons, subscriptions and watching549 /// `notify_by_email`: an inbox item, emailed to the person it is for,
550 /// only at a confirmed address and only while they can still read the
551 /// repository it is about. Returns whether it was sent.
552 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
553 #[derive(Deserialize)]
554 struct Address {
555 email: Option<String>,
556 }
557 let user = self
558 .find_user(
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)559 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ? AND deleted_at IS NULL",
Inbox: threads, reasons, subscriptions and watching560 &a.username.to_lowercase(),
561 )
562 .await?;
563 let Some(user) = user.filter(|user| user.verified) else {
564 return Ok(false);
565 };
566 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
567 &self.env.service("REPOS")?,
568 "readable",
569 &g1t_contracts::repos::ReadableArgs {
570 ids: vec![a.repo_id.clone()],
571 viewer: Some(user.clone()),
572 },
573 )
574 .await?;
575 if readable.is_empty() {
576 return Ok(false);
577 }
578 let address = self
579 .db
580 .prepare("SELECT email FROM users WHERE id = ?")
581 .bind(&[user.id.as_str().into()])?
582 .first::<Address>(None)
583 .await?
584 .and_then(|row| row.email)
585 .filter(|email| !email.trim().is_empty());
586 let Some(address) = address else {
587 return Ok(false);
588 };
589 email::send_notification(&self.env, &address, &a).await?;
590 Ok(true)
591 }
592
What happened across an outcome, as a feed beside its graph593 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
594 #[derive(serde::Deserialize)]
595 struct Named {
596 id: String,
597 name: String,
598 }
599 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
600 let mut names = std::collections::HashMap::new();
601 if ids.is_empty() {
602 return Ok(names);
603 }
604 let marks = vec!["?"; ids.len()].join(", ");
605 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
606 for sql in [
607 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
608 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
609 ] {
610 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
611 names.insert(row.id, row.name);
612 }
613 }
614 Ok(names)
615 }
616
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)617 /// `users_for_audience`: the people behind these ids (at most 50), each
618 /// with their workspaces, roles, base permissions and repository grants,
619 /// under each workspace's policy, as a signed-in viewer would have them.
620 /// For the agents service, which answers only with what every person
621 /// who will read the answer may see (docs/WORKSPACE.md, "What an agent
622 /// can and can't know"). Ids of no live account are left out, so the
623 /// caller can tell someone it could not resolve. Reached only by service
624 /// binding.
625 async fn users_for_audience(&self, a: UsernamesArgs) -> Result<Vec<User>> {
626 let mut found = Vec::new();
627 for id in a.ids.iter().take(50) {
628 let user = self
629 .find_user(
630 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ? AND deleted_at IS NULL",
631 id,
632 )
633 .await?;
634 if let Some(user) = user {
635 found.push(user);
636 }
637 }
638 Ok(found)
639 }
640
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97641 /// `accounts`: the accounts behind these ids (at most 200), each with
642 /// its username and avatar, for lists that keep ids, such as who
643 /// starred a repository. Ids of no account are left out.
644 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
645 #[derive(serde::Deserialize)]
646 struct Row {
647 id: String,
648 username: String,
649 avatar: Option<String>,
650 }
651 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
652 let mut found = std::collections::HashMap::new();
653 if ids.is_empty() {
654 return Ok(found);
655 }
656 let marks = vec!["?"; ids.len()].join(", ");
657 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
658 let rows = self
659 .db
660 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
661 .bind(&bind)?
662 .all()
663 .await?
664 .results::<Row>()?;
665 for row in rows {
666 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
667 }
668 Ok(found)
669 }
670
API and MCP server, Rust identity service, registration, site redesign671 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
672 let rows = self
673 .db
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca674 .prepare("SELECT id, title, fingerprint, created_at, last_used_at FROM ssh_keys WHERE user_id = ? ORDER BY id")
API and MCP server, Rust identity service, registration, site redesign675 .bind(&[a.user.id.into()])?
676 .all()
677 .await?
678 .results::<KeyRow>()?;
679 Ok(rows.into_iter().map(SshKey::from).collect())
680 }
681
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge682 /// The account (user id) that registered each key, by fingerprint
683 /// (`SHA256:…`). At most 100; unknown keys are left out.
684 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
685 #[derive(serde::Deserialize)]
686 struct Row {
687 fingerprint: String,
688 user_id: String,
689 }
690 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
691 if fingerprints.is_empty() {
692 return Ok(std::collections::HashMap::new());
693 }
694 let marks = vec!["?"; fingerprints.len()].join(", ");
695 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
696 Ok(self
697 .db
698 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
699 .bind(&binds)?
700 .all()
701 .await?
702 .results::<Row>()?
703 .into_iter()
704 .map(|row| (row.fingerprint, row.user_id))
705 .collect())
706 }
707
API and MCP server, Rust identity service, registration, site redesign708 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
709 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
710 return Ok(Outcome::fail(
711 FailureCode::Invalid,
712 "That is not a valid OpenSSH public key.",
713 ));
714 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca715 // Someone's SSH key, or a repository's deploy key (deploy_keys.rs).
716 if self.key_in_use(&key.fingerprint).await? {
717 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
API and MCP server, Rust identity service, registration, site redesign718 }
719 let now = now_ms();
720 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
721 .into_iter()
722 .find(|candidate| !candidate.is_empty())
723 .unwrap_or_default()
724 .to_owned();
725 let row = KeyRow {
726 id: new_id("key", now),
727 title,
728 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos729 created_at: rfc3339(now),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca730 last_used_at: None,
API and MCP server, Rust identity service, registration, site redesign731 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca732 let inserted = self.db
API and MCP server, Rust identity service, registration, site redesign733 .prepare(
734 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
735 VALUES (?, ?, ?, ?, ?, ?)",
736 )
737 .bind(&[
738 row.id.as_str().into(),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca739 a.user.id.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign740 row.title.as_str().into(),
741 key.public_key.into(),
742 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos743 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign744 ])?
745 .run()
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca746 .await;
747 // Added at the same moment elsewhere: the trigger or the unique
748 // index refused it.
749 if let Err(error) = inserted {
750 if self.key_in_use(&row.fingerprint).await? {
751 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
752 }
753 return Err(error);
754 }
Merge main (membership, two-factor, GitHub repo roles) into tokens755 let shown = format!("{} ({})", row.title, row.fingerprint);
756 self.log_security(&a.user.id, "ssh_key_added", Some(&shown), None).await;
757 self.audit_account(&a.user, "ssh_key.added", &format!("Added SSH key {shown}")).await;
API and MCP server, Rust identity service, registration, site redesign758 Ok(Outcome::Ok(row.into()))
759 }
760
Merge main (membership, two-factor, GitHub repo roles) into tokens761 /// Deletes one of the person's SSH keys.
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca762 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
Merge main (membership, two-factor, GitHub repo roles) into tokens763 #[derive(Deserialize)]
764 struct Removed {
765 title: String,
766 fingerprint: String,
767 }
768 let removed = self
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca769 .db
Merge main (membership, two-factor, GitHub repo roles) into tokens770 .prepare("DELETE FROM ssh_keys WHERE id = ? AND user_id = ? RETURNING title, fingerprint")
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca771 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
Merge main (membership, two-factor, GitHub repo roles) into tokens772 .first::<Removed>(None)
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca773 .await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens774 if let Some(removed) = removed {
775 let shown = format!("{} ({})", removed.title, removed.fingerprint);
776 self.log_security(&a.user.id, "ssh_key_removed", Some(&shown), None).await;
777 self.audit_account(&a.user, "ssh_key.removed", &format!("Removed SSH key {shown}")).await;
778 }
API and MCP server, Rust identity service, registration, site redesign779 Ok(())
780 }
781}
782
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas783/// Every 15 minutes: staff hear about waitlist requests that arrived while
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member784/// the last summary's window was still open, so none waits on a later one;
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)785/// and deleted workspaces and accounts past their restore window are purged
786/// (deletion.rs, account_deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas787#[event(scheduled)]
788async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
789 let Ok(db) = env.d1("DB") else { return };
790 let identity = Identity { db, env };
791 if let Err(error) = identity.notify_staff_of_requests().await {
792 worker::console_error!("waitlist summary: {error}");
793 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member794 if let Err(error) = identity.purge_due_workspaces().await {
795 worker::console_error!("workspace purge: {error}");
796 }
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)797 // And deleted accounts past theirs (account_deletion.rs).
798 if let Err(error) = identity.purge_due_accounts().await {
799 worker::console_error!("account purge: {error}");
800 }
Merge main (membership, two-factor, GitHub repo roles) into tokens801 // Once: creators of repositories made before they got Admin (members.rs).
802 if let Err(error) = identity.backfill_creator_grants().await {
803 worker::console_error!("creator grants: {error}");
804 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas805}
806
API and MCP server, Rust identity service, registration, site redesign807#[event(fetch)]
808async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
809 let Some(method) = rpc_method(&request) else {
810 return Response::error("Not found", 404);
811 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents812 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
813 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign814 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents815 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign816
Fast pages, required checks on the branch, self-hosted runners, honest incidents817 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette818 "register" => {
819 let outcome = identity.register(args(body)?).await?;
820 if let Outcome::Ok(signed_in) = &outcome {
821 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
822 }
823 reply(&outcome)
824 }
API and MCP server, Rust identity service, registration, site redesign825 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette826 "create_workspace" => {
827 let outcome = identity.create_workspace(args(body)?).await?;
828 if let Outcome::Ok(workspace) = &outcome {
829 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
830 }
831 reply(&outcome)
832 }
Workspaces own repositories833 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
834 "list_members" => reply(&identity.list_members(args(body)?).await?),
835 "add_member" => reply(&identity.add_member(args(body)?).await?),
836 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens837 // Owners, roles, leaving and member privileges; see members.rs.
838 "update_member" => reply(&identity.update_member(args(body)?).await?),
839 "transfer_ownership" => reply(&identity.transfer_ownership(args(body)?).await?),
840 "leave_workspace" => reply(&identity.leave_workspace(args(body)?).await?),
841 "set_member_privileges" => reply(&identity.set_member_privileges(args(body)?).await?),
842 "set_two_factor_requirement" => reply(&identity.set_two_factor_requirement(args(body)?).await?),
843 "grant_creator" => reply(&identity.grant_creator(args(body)?).await?),
Search across all of g1t, Explore, and a command palette844 "update_workspace" => {
845 let outcome = identity.update_workspace(args(body)?).await?;
846 if let Outcome::Ok(workspace) = &outcome {
847 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
848 }
849 reply(&outcome)
850 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains851 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
852 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
853 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'854 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look855 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
856 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
857 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette858 "set_workspace_avatar" => {
859 let outcome = identity.set_workspace_avatar(args(body)?).await?;
860 if let Outcome::Ok(workspace) = &outcome {
861 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
862 }
863 reply(&outcome)
864 }
865 "set_user_avatar" => {
866 let a: SetUserAvatarArgs = args(body)?;
867 let (username, id) = (a.user.username.clone(), a.user.id.clone());
868 let outcome = identity.set_user_avatar(a).await?;
869 if matches!(outcome, Outcome::Ok(_)) {
870 identity.announce_user(&username, Some(&id)).await;
871 }
872 reply(&outcome)
873 }
Agents as a team: lifecycle, merge queue, billing and a new shell874 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
875 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
876 "remove_workspace_token" => reply(&identity.remove_workspace_token(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look877 // Signing in with GitHub; see github.rs.
878 "github_enabled" => reply(&identity.github_enabled()),
879 "github_start" => reply(&identity.github_start(args(body)?).await?),
880 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
881 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
882 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
883 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
884 "github_account" => reply(&identity.github_account(args(body)?).await?),
885 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
886 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
887 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
888 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications889 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
890 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
891 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
892 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
893 "revoke_oauth_grant" => reply(&identity.revoke_oauth_grant(args(body)?).await?),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step894 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API895 "device_start" => reply(&identity.device_start(args(body)?).await?),
896 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
897 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
898 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning899 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
900 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)901 "confirm_email_code" => reply(&identity.confirm_email_code(args(body)?).await?),
902 "change_pending_email" => reply(&identity.change_pending_email(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning903 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
904 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look905 // A person's email addresses; see emails.rs and security.rs.
906 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
907 "add_email" => reply(&identity.add_email(args(body)?).await?),
908 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
909 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
910 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
911 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens912 // Two-factor authentication; see two_factor.rs.
913 "two_factor_status" => reply(&identity.two_factor_status(args(body)?).await?),
914 "two_factor_start" => reply(&identity.two_factor_start(args(body)?).await?),
915 "two_factor_enable" => reply(&identity.two_factor_enable(args(body)?).await?),
916 "two_factor_disable" => reply(&identity.two_factor_disable(args(body)?).await?),
917 "two_factor_recovery_codes" => reply(&identity.two_factor_recovery_codes(args(body)?).await?),
918 "two_factor_sign_in" => reply(&identity.two_factor_sign_in(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look919 "security_log" => reply(&identity.security_log(args(body)?).await?),
920 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
921 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
922 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
923 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
924 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign925 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
926 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
927 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
928 "user_for_access_token" => {
929 let a: TokenArgs = args(body)?;
930 reply(&identity.user_for_access_token(&a.token).await?)
931 }
932 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
933 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph934 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97935 "accounts" => reply(&identity.accounts(args(body)?).await?),
Merge the workspace shell: navigation and phone shell, g1t as orchestrator, agents in roles with audience-checked reads, reactions and custom emoji, live notifications and browser push, the homepage tour (agents 0002, chat 0002)936 "users_for_audience" => reply(&identity.users_for_audience(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching937 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains938 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette939 "update_profile" => {
940 let outcome = identity.update_profile(args(body)?).await?;
941 if let Outcome::Ok(profile) = &outcome {
942 identity.announce_user(&profile.username, None).await;
943 }
944 reply(&outcome)
945 }
946 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains947 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign948 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge949 // Services only: who registered each key, for verifying commit
950 // signatures (repos' signatures.rs).
951 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign952 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca953 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
954 // A repository's deploy keys, and who an SSH key signs in as; see
955 // deploy_keys.rs.
956 "list_deploy_keys" => reply(&identity.list_deploy_keys(args(body)?).await?),
957 "get_deploy_key" => reply(&identity.get_deploy_key(args(body)?).await?),
958 "add_deploy_key" => reply(&identity.add_deploy_key(args(body)?).await?),
959 "remove_deploy_key" => reply(&identity.remove_deploy_key(args(body)?).await?),
960 "principal_for_ssh_key" => reply(&identity.principal_for_ssh_key(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign961 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
962 "create_access_token" => reply(&identity.create_access_token(args(body)?).await?),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step963 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Fine-grained personal tokens, workspace token rules and approvals in identity964 // Fine-grained tokens and workspaces' rules for tokens; see token_reach.rs.
965 "create_fine_grained_token" => reply(&identity.create_fine_grained_token(args(body)?).await?),
966 "update_fine_grained_token" => reply(&identity.update_fine_grained_token(args(body)?).await?),
967 "get_token_policy" => reply(&identity.get_token_policy(args(body)?).await?),
968 "set_token_policy" => reply(&identity.set_token_policy(args(body)?).await?),
969 "list_member_tokens" => reply(&identity.list_member_tokens(args(body)?).await?),
970 "review_token_request" => reply(&identity.review_token_request(args(body)?).await?),
971 "revoke_member_token" => reply(&identity.revoke_member_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell972 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
973 "agent_scope" => reply(&identity.agent_scope(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API974 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
975 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
976 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'977 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
978 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens979 "remove_access_token" => reply(&identity.remove_access_token(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look980 // Invites and the waitlist; see invites.rs.
981 "registration" => reply(&identity.registration_mode()),
982 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
983 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
984 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
985 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
986 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
987 "invite_member" => reply(&identity.invite_member(args(body)?).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)988 "list_invitations" => reply(&identity.list_invitations(args(body)?).await?),
989 "accept_invitation" => reply(&identity.accept_invitation(args(body)?).await?),
990 "decline_invitation" => reply(&identity.decline_invitation(args(body)?).await?),
991 "find_people" => reply(&identity.find_people(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look992 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
993 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
994 "request_access" => reply(&identity.request_access(args(body)?).await?),
995 // Who has access to a repository; see access.rs.
996 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
997 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
998 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
999 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
1000 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
1001 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
1002 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
1003 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
1004 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'1005 // Where a workspace keeps its repositories' git data (EU residency).
1006 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
1007 "set_workspace_residency" => reply(&identity.set_workspace_residency(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1008 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1009 "forget_repo_access" => {
1010 let a: g1t_contracts::access::ForgetRepoAccessArgs = args(body)?;
1011 // A purged repository's deploy keys go with its access.
1012 identity.forget_deploy_keys(&a.repo_id).await?;
1013 reply(&identity.forget_repo_access(a).await?)
1014 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1015 // Teams (teams.rs).
1016 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
1017 "get_team" => reply(&identity.get_team(args(body)?).await?),
1018 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'1019 "set_team_creation" => reply(&identity.set_team_creation(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1020 "update_team" => reply(&identity.update_team(args(body)?).await?),
1021 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
1022 "team_members" => reply(&identity.team_members(args(body)?).await?),
1023 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
1024 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
1025 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
1026 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
1027 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
1028 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
1029 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
1030 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
1031 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
1032 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace1033 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
1034 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
1035 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
1036 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1037 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
1038 "admin_decide_waitlist" => reply(&identity.admin_decide_waitlist(args(body)?).await?),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1039 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1040 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
1041 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
1042 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
1043 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
1044 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
1045 "admin_workspace_invites" => reply(&identity.admin_workspace_invites(args(body)?).await?),
Merge shared invite links: label, uses, expiry, domains; joined through recorded (identity 0038)1046 // Shared invite links for a group; see shared_invites.rs.
1047 "admin_shared_invites" => reply(&identity.admin_shared_invites().await?),
1048 "admin_create_shared_invite" => reply(&identity.admin_create_shared_invite(args(body)?).await?),
1049 "admin_revoke_shared_invite" => reply(&identity.admin_revoke_shared_invite(args(body)?).await?),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1050 // Deleted workspaces, restored or purged by staff; see deletion.rs.
1051 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
1052 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
1053 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge account deletion: soft delete for 30 days, staff restore and purge, ghost for what remains (identity 0037)1054 // Deleting accounts (account_deletion.rs): the person from the
1055 // site, staff from sudo. There is no API route for it.
1056 "check_account_deletion" => reply(&identity.check_account_deletion(args(body)?).await?),
1057 "delete_account" => reply(&identity.delete_account(args(body)?).await?),
1058 "admin_delete_account" => reply(&identity.admin_delete_account(args(body)?).await?),
1059 "admin_deleted_accounts" => reply(&identity.admin_deleted_accounts().await?),
1060 "admin_restore_account" => reply(&identity.admin_restore_account(args(body)?).await?),
1061 "admin_purge_account" => reply(&identity.admin_purge_account(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'1062 // Workspace aliases, set by staff only; see aliases.rs.
1063 "admin_aliases" => reply(&identity.admin_aliases().await?),
1064 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
1065 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign1066 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1067 };
1068 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign1069}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1070
1071#[cfg(test)]
1072mod register_tests {
1073 use super::*;
1074
1075 #[test]
1076 fn nobody_registers_as_g1t() {
1077 // What register checks the username with, whatever its case.
1078 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
1079 assert_eq!(claimable_namespace(username), None, "{username}");
1080 }
1081 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
1082 }
1083}

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