Skip to content
1,048 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;
Account deletion: from Settings and sudo, soft for 30 days, then purged to ghost (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;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar28mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look29mod throttle;
Fine-grained personal tokens, workspace token rules and approvals in identity30mod token_reach;
Agents as a team: lifecycle, merge queue, billing and a new shell31mod tokens;
Merge main (membership, two-factor, GitHub repo roles) into tokens32mod two_factor;
Workspaces own repositories33mod workspaces;
API and MCP server, Rust identity service, registration, site redesign34
35use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos36use 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-agent37use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign38use g1t_kit::{args, now_ms, reply, rpc_method};
39use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell40use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign41use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas42use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign43
RFC 3339 timestamps in identity and repos44const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
45const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign46const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning47const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
48
49/// A user as selected from the database; `verified` arrives as 0 or 1.
50#[derive(Deserialize)]
51struct Account {
52 id: String,
53 username: String,
54 verified: u8,
Workspace names and icons, and a component kit for every control55 /// Selected only where the person is being shown to themselves.
56 #[serde(default)]
57 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning58}
59
60impl From<Account> for User {
61 fn from(row: Account) -> Self {
62 User {
63 id: row.id,
64 username: row.username,
65 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control66 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell67 ..User::default()
Email verification, password reset, and Git for AI scale positioning68 }
69 }
70}
API and MCP server, Rust identity service, registration, site redesign71
72#[derive(Deserialize)]
73struct UserRow {
74 id: String,
75 username: String,
76 password_hash: String,
Email verification, password reset, and Git for AI scale positioning77 verified: u8,
API and MCP server, Rust identity service, registration, site redesign78}
79
Email verification, password reset, and Git for AI scale positioning80/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign81#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning82struct TokenOwner {
83 id: String,
84 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look85 /// The address a link was sent to; null on links from before accounts
86 /// had several, which are for the primary.
87 #[serde(default)]
88 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning89}
90
91#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign92struct KeyRow {
93 id: String,
94 title: String,
95 fingerprint: String,
RFC 3339 timestamps in identity and repos96 created_at: String,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca97 #[serde(default)]
98 last_used_at: Option<String>,
API and MCP server, Rust identity service, registration, site redesign99}
100
101impl From<KeyRow> for SshKey {
102 fn from(row: KeyRow) -> Self {
103 SshKey {
104 id: row.id,
105 title: row.title,
106 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos107 created_at: row.created_at,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca108 last_used_at: row.last_used_at,
API and MCP server, Rust identity service, registration, site redesign109 }
110 }
111}
112
113struct Identity {
114 db: D1Database,
Email verification, password reset, and Git for AI scale positioning115 env: Env,
API and MCP server, Rust identity service, registration, site redesign116}
117
118impl Identity {
Workspaces own repositories119 /// Runs a query that returns at most one user, for showing to others:
120 /// without their workspaces.
121 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning122 Ok(self
123 .db
API and MCP server, Rust identity service, registration, site redesign124 .prepare(sql)
125 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning126 .first::<Account>(None)
127 .await?
128 .map(User::from))
129 }
130
Workspaces own repositories131 /// Attaches the workspaces a user belongs to, so that any service can
132 /// authorize them without asking again.
133 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
134 let Some(mut user) = user else {
135 return Ok(None);
136 };
Merge main (membership, two-factor, GitHub repo roles) into tokens137 let memberships = self.memberships_and_policies(&user.id).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look138 // Roles on single repositories, under the same policy (access.rs).
139 let grants = self.grants_of(&user.id).await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens140 // Access to a workspace is used only within its policy; see security.rs.
141 let within = self.within_policy(&user.id, memberships, grants).await?;
142 user.workspaces = within.memberships;
143 user.grants = within.grants;
144 user.held = within.held;
Workspaces own repositories145 Ok(Some(user))
146 }
147
148 /// Runs a query that resolves credentials to at most one user.
149 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
150 let user = self.find_public_user(sql, param).await?;
151 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign152 }
153
Email verification, password reset, and Git for AI scale positioning154 /// Consumes a token of `kind`, returning its owner if it was valid.
155 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
156 let id = crypto::sha256_hex(token);
157 let owner = self
158 .db
RFC 3339 timestamps in identity and repos159 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look160 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning161 JOIN users ON users.id = email_tokens.user_id
Account deletion: from Settings and sudo, soft for 30 days, then purged to ghost (identity 0037)162 WHERE email_tokens.id = ? AND email_tokens.kind = ? AND users.deleted_at IS NULL
RFC 3339 timestamps in identity and repos163 AND email_tokens.expires_at > {SQL_NOW}"
164 ))
Email verification, password reset, and Git for AI scale positioning165 .bind(&[id.as_str().into(), kind.into()])?
166 .first::<TokenOwner>(None)
167 .await?;
168 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look169 // Every outstanding token of this kind dies with the one used:
170 // every reset link, and every confirmation link for the same
171 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning172 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look173 .prepare(
174 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
175 AND (?2 = 'reset' OR email_id IS ?3)",
176 )
177 .bind(&[
178 owner.id.as_str().into(),
179 kind.into(),
180 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
181 ])?
Email verification, password reset, and Git for AI scale positioning182 .run()
183 .await?;
184 }
185 Ok(owner)
186 }
187
188 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 look189 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
190 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
191 }
192 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning193 }
194
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)195 /// The link in a confirmation email, followed: signed in or not. It
196 /// ends the code sent with it (emails.rs).
197 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<g1t_contracts::accounts::EmailConfirmed>> {
Email verification, password reset, and Git for AI scale positioning198 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
199 return Ok(Outcome::fail(
200 FailureCode::Invalid,
201 "This confirmation link is not valid or has expired.",
202 ));
203 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)204 self.confirm_address(&owner.id, owner.email_id.as_deref()).await
Email verification, password reset, and Git for AI scale positioning205 }
206
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look207 /// Any confirmed address of an account can ask for a reset; so can the
208 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning209 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 look210 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
211 && match a.client.as_deref() {
212 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
213 None => true,
214 };
215 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 exists216 // A failure from here on happens only for a real account, so it
217 // is logged, never answered: the reply below stays the same.
218 if let Err(error) = self.send_reset(&target).await {
219 worker::console_error!("password reset for a known address failed: {error}");
220 }
221 }
222 // The same answer either way, so addresses cannot be probed.
223 Ok(true)
224 }
225
226 /// Saves a reset link for `target` and mails it, telling the account's
227 /// other addresses.
228 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
229 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look230 let token = crypto::random_hex(32);
231 self.db
232 .prepare(format!(
233 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
234 VALUES (?, ?, 'reset', {}, ?)",
235 sql_after(RESET_TTL_SECONDS)
236 ))
237 .bind(&[
238 crypto::sha256_hex(&token).into(),
239 target.user_id.as_str().into(),
240 target.email_id.as_str().into(),
241 ])?
242 .run()
Email verification, password reset, and Git for AI scale positioning243 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look244 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
245 // The primary and the backup hear of it when it went elsewhere.
246 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
247 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
248 let change = format!("A password reset was asked for through {}", target.display);
249 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
250 worker::console_error!("security notice failed: {error}");
251 }
252 }
Email verification, password reset, and Git for AI scale positioning253 }
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 exists254 Ok(())
Email verification, password reset, and Git for AI scale positioning255 }
256
257 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
258 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
259 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
260 }
261 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
262 return Ok(Outcome::fail(
263 FailureCode::Invalid,
264 "This reset link is not valid or has expired.",
265 ));
266 };
267 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look268 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning269 .bind(&[
270 crypto::hash_password(&a.password).into(),
271 owner.id.as_str().into(),
272 ])?
273 .run()
274 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look275 // Following an emailed link also proves the address it went to
276 // (unless another account confirmed it first).
277 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
278 // Anyone signed in with the old password is signed out, and nobody
279 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning280 self.db
281 .prepare("DELETE FROM sessions WHERE user_id = ?")
282 .bind(&[owner.id.as_str().into()])?
283 .run()
284 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look285 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
286 self.log_security(&owner.id, "password_changed", None, None).await;
287 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
288 let verified = self
289 .find_public_user(
290 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
291 &owner.id,
292 )
293 .await?
294 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning295 Ok(Outcome::Ok(User {
296 id: owner.id,
297 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look298 verified,
Workspaces own repositories299 ..User::default()
Email verification, password reset, and Git for AI scale positioning300 }))
301 }
302
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look303 /// The account a login names: a username, or any confirmed address.
304 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
305 let login = login.trim().to_lowercase();
306 let (column, value) = if login.contains('@') {
307 match self.user_with_verified_email(&login).await? {
308 Some(id) => ("id", id),
309 None => return Ok(None),
310 }
311 } else {
312 ("username", login)
313 };
314 self.db
315 .prepare(format!(
Account deletion: from Settings and sudo, soft for 30 days, then purged to ghost (identity 0037)316 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users
317 WHERE {column} = ? AND deleted_at IS NULL"
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look318 ))
319 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign320 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look321 .await
322 }
323
324 /// Checks a password for a login, throttled (see throttle.rs). The
325 /// refusal is one of two messages, the same for every account.
326 async fn checked_password(
327 &self,
328 login: &str,
329 password: &str,
330 client: Option<&str>,
331 ) -> Result<std::result::Result<User, &'static str>> {
332 let row = self.password_row(login).await?;
333 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
334 let (account_key, client_key) = Identity::password_keys(&subject, client);
335 if self.password_locked(&account_key, client_key.as_deref()).await? {
336 return Ok(Err(throttle::THROTTLED));
337 }
338 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
339 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
340 Some(row) => {
341 self.clear(&account_key).await?;
342 Ok(Ok(User {
343 id: row.id,
344 username: row.username,
345 verified: row.verified != 0,
346 ..User::default()
347 }))
348 }
349 None => {
350 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
351 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
352 Ok(Err("Incorrect username or password."))
353 }
354 }
355 }
356
Merge main (membership, two-factor, GitHub repo roles) into tokens357 /// Git over HTTPS with the account's password. With two-factor
358 /// authentication on, a password alone is never enough: use an access
359 /// token (two_factor.rs).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look360 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
361 let user = self.checked_password(login, password, None).await?.ok();
Merge main (membership, two-factor, GitHub repo roles) into tokens362 if let Some(user) = &user
363 && self.two_factor_enabled(&user.id).await?
364 {
365 return Ok(None);
366 }
Workspaces own repositories367 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign368 }
369
370 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
371 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-agent372 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign373 let email = a.email.trim().to_lowercase();
374 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 look375 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
376 // The invite first: without one, nothing else on the form matters.
377 if self.invites_required() && invite_code.is_none() {
378 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
379 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent380 if !claimable {
API and MCP server, Rust identity service, registration, site redesign381 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent382 "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 redesign383 );
384 }
385 let well_formed_email = email
386 .split_once('@')
387 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
388 && !email.contains(char::is_whitespace);
389 if !well_formed_email {
390 return invalid("Enter a valid email address.");
391 }
392 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning393 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign394 }
395 let taken = self
396 .db
Agents as a team: lifecycle, merge queue, billing and a new shell397 // Usernames and workspaces share one namespace, so that a name
398 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look399 // An address is taken once an account has confirmed it; an
400 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell401 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look402 "SELECT username FROM users WHERE username = ?
403 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 shell404 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
405 )
406 .bind(&[
407 username.as_str().into(),
408 email.as_str().into(),
409 username.as_str().into(),
410 ])?
API and MCP server, Rust identity service, registration, site redesign411 .first::<serde_json::Value>(None)
412 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look413 // A renamed workspace's old slug stays reserved for it a while, and
414 // a deleted workspace's for good.
415 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign416 return Ok(Outcome::fail(
417 FailureCode::Conflict,
418 "That username or email is already registered.",
419 ));
420 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look421 let password_hash = crypto::hash_password(&a.password);
422 let user = match self
423 .create_account(invites::NewAccount {
424 username: &username,
425 email: &email,
426 password_hash: &password_hash,
427 verified: false,
428 invite_code,
429 client: a.client.as_deref(),
430 })
431 .await?
432 {
433 Outcome::Ok(user) => user,
434 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign435 };
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)436 // The account exists either way; the email can be sent again from
437 // 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 quotas438 if !user.verified
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)439 && 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 quotas440 {
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)441 worker::console_error!("confirmation email failed: {error}");
Email verification, password reset, and Git for AI scale positioning442 }
API and MCP server, Rust identity service, registration, site redesign443 self.start_session(user).await
444 }
445
446 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 look447 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
448 Ok(user) => user,
449 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign450 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look451 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign452 self.start_session(user).await
453 }
454
Merge main (membership, two-factor, GitHub repo roles) into tokens455 /// Starts a session for someone who just proved their password (or
456 /// GitHub account). With two-factor authentication on, it starts none:
457 /// it returns a challenge for `two_factor_sign_in` (two_factor.rs).
API and MCP server, Rust identity service, registration, site redesign458 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
Merge main (membership, two-factor, GitHub repo roles) into tokens459 if self.two_factor_enabled(&user.id).await? {
460 let challenge = self.issue_challenge(&user.id).await?;
461 return Ok(Outcome::Ok(SignedIn {
462 user: User { workspaces: Vec::new(), grants: Vec::new(), held: Vec::new(), ..user },
463 session_token: String::new(),
464 two_factor_challenge: Some(challenge),
465 }));
466 }
467 self.session_for(user).await
468 }
469
470 /// A new session for `user`, who has proved who they are in full.
471 async fn session_for(&self, user: User) -> Result<Outcome<SignedIn>> {
Account deletion: from Settings and sudo, soft for 30 days, then purged to ghost (identity 0037)472 // Whichever way it was proved, a deleted account starts none
473 // (account_deletion.rs).
474 if !self.account_live(&user.id).await? {
475 return Ok(Outcome::fail(FailureCode::Unauthenticated, "Incorrect username or password."));
476 }
API and MCP server, Rust identity service, registration, site redesign477 let session_token = crypto::random_hex(32);
478 self.db
RFC 3339 timestamps in identity and repos479 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look480 // Signing in is proof it is the person: see security.rs.
481 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos482 sql_after(SESSION_TTL_SECONDS)
483 ))
API and MCP server, Rust identity service, registration, site redesign484 .bind(&[
485 crypto::sha256_hex(&session_token).into(),
486 user.id.as_str().into(),
487 ])?
488 .run()
489 .await?;
490 Ok(Outcome::Ok(SignedIn {
491 user,
492 session_token,
Merge main (membership, two-factor, GitHub repo roles) into tokens493 two_factor_challenge: None,
API and MCP server, Rust identity service, registration, site redesign494 }))
495 }
496
497 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
498 self.db
499 .prepare("DELETE FROM sessions WHERE id = ?")
500 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
501 .run()
502 .await?;
503 Ok(())
504 }
505
506 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
507 self.find_user(
RFC 3339 timestamps in identity and repos508 &format!(
Workspace names and icons, and a component kit for every control509 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
510 users.avatar
RFC 3339 timestamps in identity and repos511 FROM sessions JOIN users ON users.id = sessions.user_id
Account deletion: from Settings and sudo, soft for 30 days, then purged to ghost (identity 0037)512 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW} AND users.deleted_at IS NULL"
RFC 3339 timestamps in identity and repos513 ),
API and MCP server, Rust identity service, registration, site redesign514 &crypto::sha256_hex(&a.session_token),
515 )
516 .await
517 }
518
519 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
520 // Like GitHub, a token alone identifies its user.
521 if a.secret.starts_with(TOKEN_PREFIX) {
522 self.user_for_access_token(&a.secret).await
523 } else {
524 self.user_for_password(&a.username, &a.secret).await
525 }
526 }
527
528 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
529 self.find_user(
Email verification, password reset, and Git for AI scale positioning530 "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 redesign531 JOIN users ON users.id = ssh_keys.user_id
Account deletion: from Settings and sudo, soft for 30 days, then purged to ghost (identity 0037)532 WHERE fingerprint = ? AND users.deleted_at IS NULL",
API and MCP server, Rust identity service, registration, site redesign533 &a.fingerprint,
534 )
535 .await
536 }
537
538 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories539 self.find_public_user(
Account deletion: from Settings and sudo, soft for 30 days, then purged to ghost (identity 0037)540 // A deleted account is nobody's to find, mention or add.
541 "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 redesign542 &a.username.to_lowercase(),
543 )
544 .await
545 }
546
Inbox: threads, reasons, subscriptions and watching547 /// `notify_by_email`: an inbox item, emailed to the person it is for,
548 /// only at a confirmed address and only while they can still read the
549 /// repository it is about. Returns whether it was sent.
550 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
551 #[derive(Deserialize)]
552 struct Address {
553 email: Option<String>,
554 }
555 let user = self
556 .find_user(
Account deletion: from Settings and sudo, soft for 30 days, then purged to ghost (identity 0037)557 "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 watching558 &a.username.to_lowercase(),
559 )
560 .await?;
561 let Some(user) = user.filter(|user| user.verified) else {
562 return Ok(false);
563 };
564 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
565 &self.env.service("REPOS")?,
566 "readable",
567 &g1t_contracts::repos::ReadableArgs {
568 ids: vec![a.repo_id.clone()],
569 viewer: Some(user.clone()),
570 },
571 )
572 .await?;
573 if readable.is_empty() {
574 return Ok(false);
575 }
576 let address = self
577 .db
578 .prepare("SELECT email FROM users WHERE id = ?")
579 .bind(&[user.id.as_str().into()])?
580 .first::<Address>(None)
581 .await?
582 .and_then(|row| row.email)
583 .filter(|email| !email.trim().is_empty());
584 let Some(address) = address else {
585 return Ok(false);
586 };
587 email::send_notification(&self.env, &address, &a).await?;
588 Ok(true)
589 }
590
What happened across an outcome, as a feed beside its graph591 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
592 #[derive(serde::Deserialize)]
593 struct Named {
594 id: String,
595 name: String,
596 }
597 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
598 let mut names = std::collections::HashMap::new();
599 if ids.is_empty() {
600 return Ok(names);
601 }
602 let marks = vec!["?"; ids.len()].join(", ");
603 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
604 for sql in [
605 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
606 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
607 ] {
608 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
609 names.insert(row.id, row.name);
610 }
611 }
612 Ok(names)
613 }
614
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97615 /// `accounts`: the accounts behind these ids (at most 200), each with
616 /// its username and avatar, for lists that keep ids, such as who
617 /// starred a repository. Ids of no account are left out.
618 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
619 #[derive(serde::Deserialize)]
620 struct Row {
621 id: String,
622 username: String,
623 avatar: Option<String>,
624 }
625 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
626 let mut found = std::collections::HashMap::new();
627 if ids.is_empty() {
628 return Ok(found);
629 }
630 let marks = vec!["?"; ids.len()].join(", ");
631 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
632 let rows = self
633 .db
634 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
635 .bind(&bind)?
636 .all()
637 .await?
638 .results::<Row>()?;
639 for row in rows {
640 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
641 }
642 Ok(found)
643 }
644
API and MCP server, Rust identity service, registration, site redesign645 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
646 let rows = self
647 .db
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca648 .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 redesign649 .bind(&[a.user.id.into()])?
650 .all()
651 .await?
652 .results::<KeyRow>()?;
653 Ok(rows.into_iter().map(SshKey::from).collect())
654 }
655
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge656 /// The account (user id) that registered each key, by fingerprint
657 /// (`SHA256:…`). At most 100; unknown keys are left out.
658 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
659 #[derive(serde::Deserialize)]
660 struct Row {
661 fingerprint: String,
662 user_id: String,
663 }
664 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
665 if fingerprints.is_empty() {
666 return Ok(std::collections::HashMap::new());
667 }
668 let marks = vec!["?"; fingerprints.len()].join(", ");
669 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
670 Ok(self
671 .db
672 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
673 .bind(&binds)?
674 .all()
675 .await?
676 .results::<Row>()?
677 .into_iter()
678 .map(|row| (row.fingerprint, row.user_id))
679 .collect())
680 }
681
API and MCP server, Rust identity service, registration, site redesign682 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
683 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
684 return Ok(Outcome::fail(
685 FailureCode::Invalid,
686 "That is not a valid OpenSSH public key.",
687 ));
688 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca689 // Someone's SSH key, or a repository's deploy key (deploy_keys.rs).
690 if self.key_in_use(&key.fingerprint).await? {
691 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
API and MCP server, Rust identity service, registration, site redesign692 }
693 let now = now_ms();
694 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
695 .into_iter()
696 .find(|candidate| !candidate.is_empty())
697 .unwrap_or_default()
698 .to_owned();
699 let row = KeyRow {
700 id: new_id("key", now),
701 title,
702 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos703 created_at: rfc3339(now),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca704 last_used_at: None,
API and MCP server, Rust identity service, registration, site redesign705 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca706 let inserted = self.db
API and MCP server, Rust identity service, registration, site redesign707 .prepare(
708 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
709 VALUES (?, ?, ?, ?, ?, ?)",
710 )
711 .bind(&[
712 row.id.as_str().into(),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca713 a.user.id.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign714 row.title.as_str().into(),
715 key.public_key.into(),
716 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos717 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign718 ])?
719 .run()
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca720 .await;
721 // Added at the same moment elsewhere: the trigger or the unique
722 // index refused it.
723 if let Err(error) = inserted {
724 if self.key_in_use(&row.fingerprint).await? {
725 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
726 }
727 return Err(error);
728 }
Merge main (membership, two-factor, GitHub repo roles) into tokens729 let shown = format!("{} ({})", row.title, row.fingerprint);
730 self.log_security(&a.user.id, "ssh_key_added", Some(&shown), None).await;
731 self.audit_account(&a.user, "ssh_key.added", &format!("Added SSH key {shown}")).await;
API and MCP server, Rust identity service, registration, site redesign732 Ok(Outcome::Ok(row.into()))
733 }
734
Merge main (membership, two-factor, GitHub repo roles) into tokens735 /// Deletes one of the person's SSH keys.
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca736 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
Merge main (membership, two-factor, GitHub repo roles) into tokens737 #[derive(Deserialize)]
738 struct Removed {
739 title: String,
740 fingerprint: String,
741 }
742 let removed = self
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca743 .db
Merge main (membership, two-factor, GitHub repo roles) into tokens744 .prepare("DELETE FROM ssh_keys WHERE id = ? AND user_id = ? RETURNING title, fingerprint")
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca745 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
Merge main (membership, two-factor, GitHub repo roles) into tokens746 .first::<Removed>(None)
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca747 .await?;
Merge main (membership, two-factor, GitHub repo roles) into tokens748 if let Some(removed) = removed {
749 let shown = format!("{} ({})", removed.title, removed.fingerprint);
750 self.log_security(&a.user.id, "ssh_key_removed", Some(&shown), None).await;
751 self.audit_account(&a.user, "ssh_key.removed", &format!("Removed SSH key {shown}")).await;
752 }
API and MCP server, Rust identity service, registration, site redesign753 Ok(())
754 }
755}
756
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas757/// 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 member758/// the last summary's window was still open, so none waits on a later one;
Account deletion: from Settings and sudo, soft for 30 days, then purged to ghost (identity 0037)759/// and deleted workspaces and accounts past their restore window are purged
760/// (deletion.rs, account_deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas761#[event(scheduled)]
762async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
763 let Ok(db) = env.d1("DB") else { return };
764 let identity = Identity { db, env };
765 if let Err(error) = identity.notify_staff_of_requests().await {
766 worker::console_error!("waitlist summary: {error}");
767 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member768 if let Err(error) = identity.purge_due_workspaces().await {
769 worker::console_error!("workspace purge: {error}");
770 }
Account deletion: from Settings and sudo, soft for 30 days, then purged to ghost (identity 0037)771 // And deleted accounts past theirs (account_deletion.rs).
772 if let Err(error) = identity.purge_due_accounts().await {
773 worker::console_error!("account purge: {error}");
774 }
Merge main (membership, two-factor, GitHub repo roles) into tokens775 // Once: creators of repositories made before they got Admin (members.rs).
776 if let Err(error) = identity.backfill_creator_grants().await {
777 worker::console_error!("creator grants: {error}");
778 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas779}
780
API and MCP server, Rust identity service, registration, site redesign781#[event(fetch)]
782async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
783 let Some(method) = rpc_method(&request) else {
784 return Response::error("Not found", 404);
785 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents786 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
787 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign788 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents789 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign790
Fast pages, required checks on the branch, self-hosted runners, honest incidents791 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette792 "register" => {
793 let outcome = identity.register(args(body)?).await?;
794 if let Outcome::Ok(signed_in) = &outcome {
795 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
796 }
797 reply(&outcome)
798 }
API and MCP server, Rust identity service, registration, site redesign799 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette800 "create_workspace" => {
801 let outcome = identity.create_workspace(args(body)?).await?;
802 if let Outcome::Ok(workspace) = &outcome {
803 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
804 }
805 reply(&outcome)
806 }
Workspaces own repositories807 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
808 "list_members" => reply(&identity.list_members(args(body)?).await?),
809 "add_member" => reply(&identity.add_member(args(body)?).await?),
810 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens811 // Owners, roles, leaving and member privileges; see members.rs.
812 "update_member" => reply(&identity.update_member(args(body)?).await?),
813 "transfer_ownership" => reply(&identity.transfer_ownership(args(body)?).await?),
814 "leave_workspace" => reply(&identity.leave_workspace(args(body)?).await?),
815 "set_member_privileges" => reply(&identity.set_member_privileges(args(body)?).await?),
816 "set_two_factor_requirement" => reply(&identity.set_two_factor_requirement(args(body)?).await?),
817 "grant_creator" => reply(&identity.grant_creator(args(body)?).await?),
Search across all of g1t, Explore, and a command palette818 "update_workspace" => {
819 let outcome = identity.update_workspace(args(body)?).await?;
820 if let Outcome::Ok(workspace) = &outcome {
821 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
822 }
823 reply(&outcome)
824 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains825 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
826 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
827 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'828 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look829 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
830 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
831 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette832 "set_workspace_avatar" => {
833 let outcome = identity.set_workspace_avatar(args(body)?).await?;
834 if let Outcome::Ok(workspace) = &outcome {
835 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
836 }
837 reply(&outcome)
838 }
839 "set_user_avatar" => {
840 let a: SetUserAvatarArgs = args(body)?;
841 let (username, id) = (a.user.username.clone(), a.user.id.clone());
842 let outcome = identity.set_user_avatar(a).await?;
843 if matches!(outcome, Outcome::Ok(_)) {
844 identity.announce_user(&username, Some(&id)).await;
845 }
846 reply(&outcome)
847 }
Agents as a team: lifecycle, merge queue, billing and a new shell848 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
849 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
850 "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 look851 // Signing in with GitHub; see github.rs.
852 "github_enabled" => reply(&identity.github_enabled()),
853 "github_start" => reply(&identity.github_start(args(body)?).await?),
854 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
855 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
856 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
857 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
858 "github_account" => reply(&identity.github_account(args(body)?).await?),
859 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
860 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
861 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
862 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications863 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
864 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
865 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
866 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
867 "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 step868 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API869 "device_start" => reply(&identity.device_start(args(body)?).await?),
870 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
871 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
872 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning873 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
874 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)875 "confirm_email_code" => reply(&identity.confirm_email_code(args(body)?).await?),
876 "change_pending_email" => reply(&identity.change_pending_email(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning877 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
878 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look879 // A person's email addresses; see emails.rs and security.rs.
880 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
881 "add_email" => reply(&identity.add_email(args(body)?).await?),
882 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
883 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
884 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
885 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens886 // Two-factor authentication; see two_factor.rs.
887 "two_factor_status" => reply(&identity.two_factor_status(args(body)?).await?),
888 "two_factor_start" => reply(&identity.two_factor_start(args(body)?).await?),
889 "two_factor_enable" => reply(&identity.two_factor_enable(args(body)?).await?),
890 "two_factor_disable" => reply(&identity.two_factor_disable(args(body)?).await?),
891 "two_factor_recovery_codes" => reply(&identity.two_factor_recovery_codes(args(body)?).await?),
892 "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 look893 "security_log" => reply(&identity.security_log(args(body)?).await?),
894 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
895 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
896 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
897 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
898 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign899 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
900 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
901 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
902 "user_for_access_token" => {
903 let a: TokenArgs = args(body)?;
904 reply(&identity.user_for_access_token(&a.token).await?)
905 }
906 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
907 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph908 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97909 "accounts" => reply(&identity.accounts(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching910 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains911 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette912 "update_profile" => {
913 let outcome = identity.update_profile(args(body)?).await?;
914 if let Outcome::Ok(profile) = &outcome {
915 identity.announce_user(&profile.username, None).await;
916 }
917 reply(&outcome)
918 }
919 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains920 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign921 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge922 // Services only: who registered each key, for verifying commit
923 // signatures (repos' signatures.rs).
924 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign925 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca926 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
927 // A repository's deploy keys, and who an SSH key signs in as; see
928 // deploy_keys.rs.
929 "list_deploy_keys" => reply(&identity.list_deploy_keys(args(body)?).await?),
930 "get_deploy_key" => reply(&identity.get_deploy_key(args(body)?).await?),
931 "add_deploy_key" => reply(&identity.add_deploy_key(args(body)?).await?),
932 "remove_deploy_key" => reply(&identity.remove_deploy_key(args(body)?).await?),
933 "principal_for_ssh_key" => reply(&identity.principal_for_ssh_key(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign934 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
935 "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 step936 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Fine-grained personal tokens, workspace token rules and approvals in identity937 // Fine-grained tokens and workspaces' rules for tokens; see token_reach.rs.
938 "create_fine_grained_token" => reply(&identity.create_fine_grained_token(args(body)?).await?),
939 "update_fine_grained_token" => reply(&identity.update_fine_grained_token(args(body)?).await?),
940 "get_token_policy" => reply(&identity.get_token_policy(args(body)?).await?),
941 "set_token_policy" => reply(&identity.set_token_policy(args(body)?).await?),
942 "list_member_tokens" => reply(&identity.list_member_tokens(args(body)?).await?),
943 "review_token_request" => reply(&identity.review_token_request(args(body)?).await?),
944 "revoke_member_token" => reply(&identity.revoke_member_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell945 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
946 "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 API947 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
948 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
949 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'950 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
951 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
Merge main (membership, two-factor, GitHub repo roles) into tokens952 "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 look953 // Invites and the waitlist; see invites.rs.
954 "registration" => reply(&identity.registration_mode()),
955 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
956 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
957 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
958 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
959 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
960 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
961 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
962 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
963 "request_access" => reply(&identity.request_access(args(body)?).await?),
964 // Who has access to a repository; see access.rs.
965 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
966 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
967 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
968 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
969 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
970 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
971 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
972 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
973 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'974 // Where a workspace keeps its repositories' git data (EU residency).
975 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
976 "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 look977 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca978 "forget_repo_access" => {
979 let a: g1t_contracts::access::ForgetRepoAccessArgs = args(body)?;
980 // A purged repository's deploy keys go with its access.
981 identity.forget_deploy_keys(&a.repo_id).await?;
982 reply(&identity.forget_repo_access(a).await?)
983 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar984 // Teams (teams.rs).
985 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
986 "get_team" => reply(&identity.get_team(args(body)?).await?),
987 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'988 "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 bar989 "update_team" => reply(&identity.update_team(args(body)?).await?),
990 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
991 "team_members" => reply(&identity.team_members(args(body)?).await?),
992 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
993 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
994 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
995 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
996 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
997 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
998 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
999 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
1000 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
1001 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace1002 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
1003 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
1004 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
1005 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1006 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
1007 "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 quotas1008 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1009 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
1010 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
1011 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
1012 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
1013 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
1014 "admin_workspace_invites" => reply(&identity.admin_workspace_invites(args(body)?).await?),
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1015 // Deleted workspaces, restored or purged by staff; see deletion.rs.
1016 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
1017 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
1018 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Account deletion: from Settings and sudo, soft for 30 days, then purged to ghost (identity 0037)1019 // Deleting accounts (account_deletion.rs): the person from the
1020 // site, staff from sudo. There is no API route for it.
1021 "check_account_deletion" => reply(&identity.check_account_deletion(args(body)?).await?),
1022 "delete_account" => reply(&identity.delete_account(args(body)?).await?),
1023 "admin_delete_account" => reply(&identity.admin_delete_account(args(body)?).await?),
1024 "admin_deleted_accounts" => reply(&identity.admin_deleted_accounts().await?),
1025 "admin_restore_account" => reply(&identity.admin_restore_account(args(body)?).await?),
1026 "admin_purge_account" => reply(&identity.admin_purge_account(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'1027 // Workspace aliases, set by staff only; see aliases.rs.
1028 "admin_aliases" => reply(&identity.admin_aliases().await?),
1029 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
1030 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign1031 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1032 };
1033 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign1034}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1035
1036#[cfg(test)]
1037mod register_tests {
1038 use super::*;
1039
1040 #[test]
1041 fn nobody_registers_as_g1t() {
1042 // What register checks the username with, whatever its case.
1043 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
1044 assert_eq!(claimable_namespace(username), None, "{username}");
1045 }
1046 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
1047 }
1048}

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