Skip to content
1,017 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;
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace7mod admin;
Merge branch 'worktree-agent-a8385d293d42c913a'8mod aliases;
Workspace names and icons, and a component kit for every control9mod avatars;
API and MCP server, Rust identity service, registration, site redesign10mod crypto;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look11mod deletion;
Deploy keys: SSH keys that reach one repository12mod deploy_keys;
Device sign-in replaces registering and minting tokens over the API13mod device;
Search across all of g1t, Explore, and a command palette14mod directory;
Email verification, password reset, and Git for AI scale positioning15mod email;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look16mod emails;
17mod github;
18mod invites;
OAuth 2.1 sign-in for MCP clients and other applications19mod oauth;
Merge Stripe Tax, the card fee on card payments, and one free workspace per person20mod paid;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains21mod profiles;
22mod rename;
Merge branch 'worktree-agent-a3abfcce648e87dca'23mod job_tokens;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API24mod run_credentials;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25mod security;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar26mod teams;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look27mod throttle;
Agents as a team: lifecycle, merge queue, billing and a new shell28mod tokens;
Workspaces own repositories29mod workspaces;
API and MCP server, Rust identity service, registration, site redesign30
31use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos32use 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-agent33use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign34use g1t_kit::{args, now_ms, reply, rpc_method};
35use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell36use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign37use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas38use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign39
RFC 3339 timestamps in identity and repos40const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
41const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
42const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign43const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning44const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
45
46/// A user as selected from the database; `verified` arrives as 0 or 1.
47#[derive(Deserialize)]
48struct Account {
49 id: String,
50 username: String,
51 verified: u8,
Workspace names and icons, and a component kit for every control52 /// Selected only where the person is being shown to themselves.
53 #[serde(default)]
54 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning55}
56
57impl From<Account> for User {
58 fn from(row: Account) -> Self {
59 User {
60 id: row.id,
61 username: row.username,
62 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control63 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell64 ..User::default()
Email verification, password reset, and Git for AI scale positioning65 }
66 }
67}
API and MCP server, Rust identity service, registration, site redesign68
69#[derive(Deserialize)]
70struct UserRow {
71 id: String,
72 username: String,
73 password_hash: String,
Email verification, password reset, and Git for AI scale positioning74 verified: u8,
API and MCP server, Rust identity service, registration, site redesign75}
76
Email verification, password reset, and Git for AI scale positioning77/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign78#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning79struct TokenOwner {
80 id: String,
81 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look82 /// The address a link was sent to; null on links from before accounts
83 /// had several, which are for the primary.
84 #[serde(default)]
85 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning86}
87
88#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign89struct KeyRow {
90 id: String,
91 title: String,
92 fingerprint: String,
RFC 3339 timestamps in identity and repos93 created_at: String,
Deploy keys: SSH keys that reach one repository94 #[serde(default)]
95 last_used_at: Option<String>,
API and MCP server, Rust identity service, registration, site redesign96}
97
98impl From<KeyRow> for SshKey {
99 fn from(row: KeyRow) -> Self {
100 SshKey {
101 id: row.id,
102 title: row.title,
103 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos104 created_at: row.created_at,
Deploy keys: SSH keys that reach one repository105 last_used_at: row.last_used_at,
API and MCP server, Rust identity service, registration, site redesign106 }
107 }
108}
109
110struct Identity {
111 db: D1Database,
Email verification, password reset, and Git for AI scale positioning112 env: Env,
API and MCP server, Rust identity service, registration, site redesign113}
114
115impl Identity {
Workspaces own repositories116 /// Runs a query that returns at most one user, for showing to others:
117 /// without their workspaces.
118 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning119 Ok(self
120 .db
API and MCP server, Rust identity service, registration, site redesign121 .prepare(sql)
122 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning123 .first::<Account>(None)
124 .await?
125 .map(User::from))
126 }
127
Workspaces own repositories128 /// Attaches the workspaces a user belongs to, so that any service can
129 /// authorize them without asking again.
130 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
131 let Some(mut user) = user else {
132 return Ok(None);
133 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look134 let memberships = self.memberships(&user.id).await?;
135 // Access to a workspace is used only within its policy; see security.rs.
136 user.workspaces = self.within_policy(&user.id, memberships).await?;
137 // Roles on single repositories, under the same policy (access.rs).
138 let grants = self.grants_of(&user.id).await?;
139 user.grants = self.grants_within_policy(&user.id, grants).await?;
Workspaces own repositories140 Ok(Some(user))
141 }
142
143 /// Runs a query that resolves credentials to at most one user.
144 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
145 let user = self.find_public_user(sql, param).await?;
146 self.with_workspaces(user).await
147 }
148
Email verification, password reset, and Git for AI scale positioning149 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos150 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning151 let token = crypto::random_hex(32);
152 self.db
RFC 3339 timestamps in identity and repos153 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning154 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos155 VALUES (?, ?, ?, {})",
156 sql_after(ttl)
157 ))
Email verification, password reset, and Git for AI scale positioning158 .bind(&[
159 crypto::sha256_hex(&token).into(),
160 user_id.into(),
161 kind.into(),
162 ])?
163 .run()
164 .await?;
165 Ok(token)
API and MCP server, Rust identity service, registration, site redesign166 }
167
Email verification, password reset, and Git for AI scale positioning168 /// Consumes a token of `kind`, returning its owner if it was valid.
169 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
170 let id = crypto::sha256_hex(token);
171 let owner = self
172 .db
RFC 3339 timestamps in identity and repos173 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look174 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning175 JOIN users ON users.id = email_tokens.user_id
176 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos177 AND email_tokens.expires_at > {SQL_NOW}"
178 ))
Email verification, password reset, and Git for AI scale positioning179 .bind(&[id.as_str().into(), kind.into()])?
180 .first::<TokenOwner>(None)
181 .await?;
182 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look183 // Every outstanding token of this kind dies with the one used:
184 // every reset link, and every confirmation link for the same
185 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning186 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look187 .prepare(
188 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
189 AND (?2 = 'reset' OR email_id IS ?3)",
190 )
191 .bind(&[
192 owner.id.as_str().into(),
193 kind.into(),
194 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
195 ])?
Email verification, password reset, and Git for AI scale positioning196 .run()
197 .await?;
198 }
199 Ok(owner)
200 }
201
202 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
203 let token = self
204 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
205 .await?;
206 email::send_verification(&self.env, email, &user.username, &token).await
207 }
208
209 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 look210 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
211 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
212 }
213 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning214 }
215
216 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
217 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
218 return Ok(Outcome::fail(
219 FailureCode::Invalid,
220 "This confirmation link is not valid or has expired.",
221 ));
222 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look223 if let Outcome::Fail(failure) = self.confirm_address(&owner.id, owner.email_id.as_deref()).await? {
224 return Ok(Outcome::Fail(failure));
225 }
226 // Whether the account is confirmed: whether its primary is.
227 let verified = self
228 .find_public_user(
229 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
230 &owner.id,
231 )
232 .await?
233 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning234 Ok(Outcome::Ok(User {
235 id: owner.id,
236 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look237 verified,
Workspaces own repositories238 ..User::default()
Email verification, password reset, and Git for AI scale positioning239 }))
240 }
241
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look242 /// Any confirmed address of an account can ask for a reset; so can the
243 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning244 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 look245 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
246 && match a.client.as_deref() {
247 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
248 None => true,
249 };
250 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 exists251 // A failure from here on happens only for a real account, so it
252 // is logged, never answered: the reply below stays the same.
253 if let Err(error) = self.send_reset(&target).await {
254 worker::console_error!("password reset for a known address failed: {error}");
255 }
256 }
257 // The same answer either way, so addresses cannot be probed.
258 Ok(true)
259 }
260
261 /// Saves a reset link for `target` and mails it, telling the account's
262 /// other addresses.
263 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
264 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look265 let token = crypto::random_hex(32);
266 self.db
267 .prepare(format!(
268 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
269 VALUES (?, ?, 'reset', {}, ?)",
270 sql_after(RESET_TTL_SECONDS)
271 ))
272 .bind(&[
273 crypto::sha256_hex(&token).into(),
274 target.user_id.as_str().into(),
275 target.email_id.as_str().into(),
276 ])?
277 .run()
Email verification, password reset, and Git for AI scale positioning278 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look279 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
280 // The primary and the backup hear of it when it went elsewhere.
281 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
282 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
283 let change = format!("A password reset was asked for through {}", target.display);
284 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
285 worker::console_error!("security notice failed: {error}");
286 }
287 }
Email verification, password reset, and Git for AI scale positioning288 }
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 exists289 Ok(())
Email verification, password reset, and Git for AI scale positioning290 }
291
292 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
293 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
294 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
295 }
296 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
297 return Ok(Outcome::fail(
298 FailureCode::Invalid,
299 "This reset link is not valid or has expired.",
300 ));
301 };
302 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look303 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning304 .bind(&[
305 crypto::hash_password(&a.password).into(),
306 owner.id.as_str().into(),
307 ])?
308 .run()
309 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look310 // Following an emailed link also proves the address it went to
311 // (unless another account confirmed it first).
312 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
313 // Anyone signed in with the old password is signed out, and nobody
314 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning315 self.db
316 .prepare("DELETE FROM sessions WHERE user_id = ?")
317 .bind(&[owner.id.as_str().into()])?
318 .run()
319 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look320 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
321 self.log_security(&owner.id, "password_changed", None, None).await;
322 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
323 let verified = self
324 .find_public_user(
325 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
326 &owner.id,
327 )
328 .await?
329 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning330 Ok(Outcome::Ok(User {
331 id: owner.id,
332 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look333 verified,
Workspaces own repositories334 ..User::default()
Email verification, password reset, and Git for AI scale positioning335 }))
336 }
337
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look338 /// The account a login names: a username, or any confirmed address.
339 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
340 let login = login.trim().to_lowercase();
341 let (column, value) = if login.contains('@') {
342 match self.user_with_verified_email(&login).await? {
343 Some(id) => ("id", id),
344 None => return Ok(None),
345 }
346 } else {
347 ("username", login)
348 };
349 self.db
350 .prepare(format!(
351 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
352 ))
353 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign354 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look355 .await
356 }
357
358 /// Checks a password for a login, throttled (see throttle.rs). The
359 /// refusal is one of two messages, the same for every account.
360 async fn checked_password(
361 &self,
362 login: &str,
363 password: &str,
364 client: Option<&str>,
365 ) -> Result<std::result::Result<User, &'static str>> {
366 let row = self.password_row(login).await?;
367 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
368 let (account_key, client_key) = Identity::password_keys(&subject, client);
369 if self.password_locked(&account_key, client_key.as_deref()).await? {
370 return Ok(Err(throttle::THROTTLED));
371 }
372 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
373 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
374 Some(row) => {
375 self.clear(&account_key).await?;
376 Ok(Ok(User {
377 id: row.id,
378 username: row.username,
379 verified: row.verified != 0,
380 ..User::default()
381 }))
382 }
383 None => {
384 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
385 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
386 Ok(Err("Incorrect username or password."))
387 }
388 }
389 }
390
391 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
392 let user = self.checked_password(login, password, None).await?.ok();
Workspaces own repositories393 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign394 }
395
396 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
397 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-agent398 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign399 let email = a.email.trim().to_lowercase();
400 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 look401 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
402 // The invite first: without one, nothing else on the form matters.
403 if self.invites_required() && invite_code.is_none() {
404 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
405 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent406 if !claimable {
API and MCP server, Rust identity service, registration, site redesign407 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent408 "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 redesign409 );
410 }
411 let well_formed_email = email
412 .split_once('@')
413 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
414 && !email.contains(char::is_whitespace);
415 if !well_formed_email {
416 return invalid("Enter a valid email address.");
417 }
418 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning419 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign420 }
421 let taken = self
422 .db
Agents as a team: lifecycle, merge queue, billing and a new shell423 // Usernames and workspaces share one namespace, so that a name
424 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look425 // An address is taken once an account has confirmed it; an
426 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell427 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look428 "SELECT username FROM users WHERE username = ?
429 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 shell430 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
431 )
432 .bind(&[
433 username.as_str().into(),
434 email.as_str().into(),
435 username.as_str().into(),
436 ])?
API and MCP server, Rust identity service, registration, site redesign437 .first::<serde_json::Value>(None)
438 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look439 // A renamed workspace's old slug stays reserved for it a while, and
440 // a deleted workspace's for good.
441 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign442 return Ok(Outcome::fail(
443 FailureCode::Conflict,
444 "That username or email is already registered.",
445 ));
446 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look447 let password_hash = crypto::hash_password(&a.password);
448 let user = match self
449 .create_account(invites::NewAccount {
450 username: &username,
451 email: &email,
452 password_hash: &password_hash,
453 verified: false,
454 invite_code,
455 client: a.client.as_deref(),
456 })
457 .await?
458 {
459 Outcome::Ok(user) => user,
460 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign461 };
Email verification, password reset, and Git for AI scale positioning462 // The account exists either way; the email can be sent again later.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas463 // An invite sent to this address confirmed it already (invites.rs).
464 if !user.verified
465 && let Err(error) = self.send_verification(&user, &email).await
466 {
Email verification, password reset, and Git for AI scale positioning467 worker::console_error!("verification email failed: {error}");
468 }
API and MCP server, Rust identity service, registration, site redesign469 self.start_session(user).await
470 }
471
472 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 look473 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
474 Ok(user) => user,
475 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign476 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look477 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign478 self.start_session(user).await
479 }
480
481 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
482 let session_token = crypto::random_hex(32);
483 self.db
RFC 3339 timestamps in identity and repos484 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look485 // Signing in is proof it is the person: see security.rs.
486 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos487 sql_after(SESSION_TTL_SECONDS)
488 ))
API and MCP server, Rust identity service, registration, site redesign489 .bind(&[
490 crypto::sha256_hex(&session_token).into(),
491 user.id.as_str().into(),
492 ])?
493 .run()
494 .await?;
495 Ok(Outcome::Ok(SignedIn {
496 user,
497 session_token,
498 }))
499 }
500
501 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
502 self.db
503 .prepare("DELETE FROM sessions WHERE id = ?")
504 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
505 .run()
506 .await?;
507 Ok(())
508 }
509
510 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
511 self.find_user(
RFC 3339 timestamps in identity and repos512 &format!(
Workspace names and icons, and a component kit for every control513 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
514 users.avatar
RFC 3339 timestamps in identity and repos515 FROM sessions JOIN users ON users.id = sessions.user_id
516 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
517 ),
API and MCP server, Rust identity service, registration, site redesign518 &crypto::sha256_hex(&a.session_token),
519 )
520 .await
521 }
522
523 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
524 // Like GitHub, a token alone identifies its user.
525 if a.secret.starts_with(TOKEN_PREFIX) {
526 self.user_for_access_token(&a.secret).await
527 } else {
528 self.user_for_password(&a.username, &a.secret).await
529 }
530 }
531
532 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
533 self.find_user(
Email verification, password reset, and Git for AI scale positioning534 "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 redesign535 JOIN users ON users.id = ssh_keys.user_id
536 WHERE fingerprint = ?",
537 &a.fingerprint,
538 )
539 .await
540 }
541
542 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories543 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning544 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign545 &a.username.to_lowercase(),
546 )
547 .await
548 }
549
Inbox: threads, reasons, subscriptions and watching550 /// `notify_by_email`: an inbox item, emailed to the person it is for,
551 /// only at a confirmed address and only while they can still read the
552 /// repository it is about. Returns whether it was sent.
553 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
554 #[derive(Deserialize)]
555 struct Address {
556 email: Option<String>,
557 }
558 let user = self
559 .find_user(
560 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
561 &a.username.to_lowercase(),
562 )
563 .await?;
564 let Some(user) = user.filter(|user| user.verified) else {
565 return Ok(false);
566 };
567 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
568 &self.env.service("REPOS")?,
569 "readable",
570 &g1t_contracts::repos::ReadableArgs {
571 ids: vec![a.repo_id.clone()],
572 viewer: Some(user.clone()),
573 },
574 )
575 .await?;
576 if readable.is_empty() {
577 return Ok(false);
578 }
579 let address = self
580 .db
581 .prepare("SELECT email FROM users WHERE id = ?")
582 .bind(&[user.id.as_str().into()])?
583 .first::<Address>(None)
584 .await?
585 .and_then(|row| row.email)
586 .filter(|email| !email.trim().is_empty());
587 let Some(address) = address else {
588 return Ok(false);
589 };
590 email::send_notification(&self.env, &address, &a).await?;
591 Ok(true)
592 }
593
What happened across an outcome, as a feed beside its graph594 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
595 #[derive(serde::Deserialize)]
596 struct Named {
597 id: String,
598 name: String,
599 }
600 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
601 let mut names = std::collections::HashMap::new();
602 if ids.is_empty() {
603 return Ok(names);
604 }
605 let marks = vec!["?"; ids.len()].join(", ");
606 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
607 for sql in [
608 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
609 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
610 ] {
611 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
612 names.insert(row.id, row.name);
613 }
614 }
615 Ok(names)
616 }
617
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97618 /// `accounts`: the accounts behind these ids (at most 200), each with
619 /// its username and avatar, for lists that keep ids, such as who
620 /// starred a repository. Ids of no account are left out.
621 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
622 #[derive(serde::Deserialize)]
623 struct Row {
624 id: String,
625 username: String,
626 avatar: Option<String>,
627 }
628 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
629 let mut found = std::collections::HashMap::new();
630 if ids.is_empty() {
631 return Ok(found);
632 }
633 let marks = vec!["?"; ids.len()].join(", ");
634 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
635 let rows = self
636 .db
637 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
638 .bind(&bind)?
639 .all()
640 .await?
641 .results::<Row>()?;
642 for row in rows {
643 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
644 }
645 Ok(found)
646 }
647
API and MCP server, Rust identity service, registration, site redesign648 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
649 let rows = self
650 .db
Deploy keys: SSH keys that reach one repository651 .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 redesign652 .bind(&[a.user.id.into()])?
653 .all()
654 .await?
655 .results::<KeyRow>()?;
656 Ok(rows.into_iter().map(SshKey::from).collect())
657 }
658
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge659 /// The account (user id) that registered each key, by fingerprint
660 /// (`SHA256:…`). At most 100; unknown keys are left out.
661 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
662 #[derive(serde::Deserialize)]
663 struct Row {
664 fingerprint: String,
665 user_id: String,
666 }
667 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
668 if fingerprints.is_empty() {
669 return Ok(std::collections::HashMap::new());
670 }
671 let marks = vec!["?"; fingerprints.len()].join(", ");
672 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
673 Ok(self
674 .db
675 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
676 .bind(&binds)?
677 .all()
678 .await?
679 .results::<Row>()?
680 .into_iter()
681 .map(|row| (row.fingerprint, row.user_id))
682 .collect())
683 }
684
API and MCP server, Rust identity service, registration, site redesign685 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
686 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
687 return Ok(Outcome::fail(
688 FailureCode::Invalid,
689 "That is not a valid OpenSSH public key.",
690 ));
691 };
Deploy keys: SSH keys that reach one repository692 // Someone's SSH key, or a repository's deploy key (deploy_keys.rs).
693 if self.key_in_use(&key.fingerprint).await? {
694 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
API and MCP server, Rust identity service, registration, site redesign695 }
696 let now = now_ms();
697 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
698 .into_iter()
699 .find(|candidate| !candidate.is_empty())
700 .unwrap_or_default()
701 .to_owned();
702 let row = KeyRow {
703 id: new_id("key", now),
704 title,
705 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos706 created_at: rfc3339(now),
Deploy keys: SSH keys that reach one repository707 last_used_at: None,
API and MCP server, Rust identity service, registration, site redesign708 };
Deploy keys: SSH keys that reach one repository709 let inserted = self.db
API and MCP server, Rust identity service, registration, site redesign710 .prepare(
711 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
712 VALUES (?, ?, ?, ?, ?, ?)",
713 )
714 .bind(&[
715 row.id.as_str().into(),
Deploy keys: SSH keys that reach one repository716 a.user.id.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign717 row.title.as_str().into(),
718 key.public_key.into(),
719 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos720 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign721 ])?
722 .run()
Deploy keys: SSH keys that reach one repository723 .await;
724 // Added at the same moment elsewhere: the trigger or the unique
725 // index refused it.
726 if let Err(error) = inserted {
727 if self.key_in_use(&row.fingerprint).await? {
728 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
729 }
730 return Err(error);
731 }
732 let added = format!("{} ({})", row.title, row.fingerprint);
733 self.log_security(&a.user.id, "ssh_key_added", Some(&added), None).await;
API and MCP server, Rust identity service, registration, site redesign734 Ok(Outcome::Ok(row.into()))
735 }
736
Deploy keys: SSH keys that reach one repository737 /// Deletes one of the person's SSH keys, and says so in their security log.
738 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
739 let key = self
740 .db
741 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE id = ? AND user_id = ?")
742 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
743 .first::<KeyRow>(None)
744 .await?;
745 let Some(key) = key else {
746 return Ok(());
747 };
748 let user_id = a.user.id.clone();
749 self.remove("ssh_keys", a).await?;
750 let removed = format!("{} ({})", key.title, key.fingerprint);
751 self.log_security(&user_id, "ssh_key_removed", Some(&removed), None).await;
752 Ok(())
753 }
754
API and MCP server, Rust identity service, registration, site redesign755 /// Deletes a row the user owns from `table`.
756 async fn remove(&self, table: &str, a: RemoveArgs) -> Result<()> {
757 self.db
758 .prepare(format!("DELETE FROM {table} WHERE id = ? AND user_id = ?"))
759 .bind(&[a.id.into(), a.user.id.into()])?
760 .run()
761 .await?;
762 Ok(())
763 }
764}
765
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas766/// 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 member767/// the last summary's window was still open, so none waits on a later one;
768/// and deleted workspaces past their restore window are purged
769/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas770#[event(scheduled)]
771async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
772 let Ok(db) = env.d1("DB") else { return };
773 let identity = Identity { db, env };
774 if let Err(error) = identity.notify_staff_of_requests().await {
775 worker::console_error!("waitlist summary: {error}");
776 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member777 if let Err(error) = identity.purge_due_workspaces().await {
778 worker::console_error!("workspace purge: {error}");
779 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas780}
781
API and MCP server, Rust identity service, registration, site redesign782#[event(fetch)]
783async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
784 let Some(method) = rpc_method(&request) else {
785 return Response::error("Not found", 404);
786 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents787 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
788 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign789 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents790 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign791
Fast pages, required checks on the branch, self-hosted runners, honest incidents792 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette793 "register" => {
794 let outcome = identity.register(args(body)?).await?;
795 if let Outcome::Ok(signed_in) = &outcome {
796 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
797 }
798 reply(&outcome)
799 }
API and MCP server, Rust identity service, registration, site redesign800 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette801 "create_workspace" => {
802 let outcome = identity.create_workspace(args(body)?).await?;
803 if let Outcome::Ok(workspace) = &outcome {
804 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
805 }
806 reply(&outcome)
807 }
Workspaces own repositories808 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
809 "list_members" => reply(&identity.list_members(args(body)?).await?),
810 "add_member" => reply(&identity.add_member(args(body)?).await?),
811 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Search across all of g1t, Explore, and a command palette812 "update_workspace" => {
813 let outcome = identity.update_workspace(args(body)?).await?;
814 if let Outcome::Ok(workspace) = &outcome {
815 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
816 }
817 reply(&outcome)
818 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains819 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
820 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
821 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'822 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look823 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
824 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
825 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette826 "set_workspace_avatar" => {
827 let outcome = identity.set_workspace_avatar(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 }
833 "set_user_avatar" => {
834 let a: SetUserAvatarArgs = args(body)?;
835 let (username, id) = (a.user.username.clone(), a.user.id.clone());
836 let outcome = identity.set_user_avatar(a).await?;
837 if matches!(outcome, Outcome::Ok(_)) {
838 identity.announce_user(&username, Some(&id)).await;
839 }
840 reply(&outcome)
841 }
Agents as a team: lifecycle, merge queue, billing and a new shell842 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
843 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
844 "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 look845 // Signing in with GitHub; see github.rs.
846 "github_enabled" => reply(&identity.github_enabled()),
847 "github_start" => reply(&identity.github_start(args(body)?).await?),
848 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
849 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
850 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
851 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
852 "github_account" => reply(&identity.github_account(args(body)?).await?),
853 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
854 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
855 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
856 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications857 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
858 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
859 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
860 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
861 "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 step862 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API863 "device_start" => reply(&identity.device_start(args(body)?).await?),
864 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
865 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
866 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning867 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
868 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
869 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
870 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look871 // A person's email addresses; see emails.rs and security.rs.
872 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
873 "add_email" => reply(&identity.add_email(args(body)?).await?),
874 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
875 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
876 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
877 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
878 "security_log" => reply(&identity.security_log(args(body)?).await?),
879 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
880 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
881 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
882 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
883 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign884 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
885 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
886 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
887 "user_for_access_token" => {
888 let a: TokenArgs = args(body)?;
889 reply(&identity.user_for_access_token(&a.token).await?)
890 }
891 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
892 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph893 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97894 "accounts" => reply(&identity.accounts(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching895 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains896 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette897 "update_profile" => {
898 let outcome = identity.update_profile(args(body)?).await?;
899 if let Outcome::Ok(profile) = &outcome {
900 identity.announce_user(&profile.username, None).await;
901 }
902 reply(&outcome)
903 }
904 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains905 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign906 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge907 // Services only: who registered each key, for verifying commit
908 // signatures (repos' signatures.rs).
909 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign910 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Deploy keys: SSH keys that reach one repository911 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
912 // A repository's deploy keys, and who an SSH key signs in as; see
913 // deploy_keys.rs.
914 "list_deploy_keys" => reply(&identity.list_deploy_keys(args(body)?).await?),
915 "get_deploy_key" => reply(&identity.get_deploy_key(args(body)?).await?),
916 "add_deploy_key" => reply(&identity.add_deploy_key(args(body)?).await?),
917 "remove_deploy_key" => reply(&identity.remove_deploy_key(args(body)?).await?),
918 "principal_for_ssh_key" => reply(&identity.principal_for_ssh_key(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign919 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
920 "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 step921 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell922 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
923 "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 API924 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
925 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
926 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'927 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
928 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign929 "remove_access_token" => reply(&identity.remove("access_tokens", args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look930 // Invites and the waitlist; see invites.rs.
931 "registration" => reply(&identity.registration_mode()),
932 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
933 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
934 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
935 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
936 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
937 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
938 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
939 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
940 "request_access" => reply(&identity.request_access(args(body)?).await?),
941 // Who has access to a repository; see access.rs.
942 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
943 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
944 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
945 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
946 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
947 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
948 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
949 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
950 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'951 // Where a workspace keeps its repositories' git data (EU residency).
952 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
953 "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 look954 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
Deploy keys: SSH keys that reach one repository955 "forget_repo_access" => {
956 let a: g1t_contracts::access::ForgetRepoAccessArgs = args(body)?;
957 // A purged repository's deploy keys go with its access.
958 identity.forget_deploy_keys(&a.repo_id).await?;
959 reply(&identity.forget_repo_access(a).await?)
960 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar961 // Teams (teams.rs).
962 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
963 "get_team" => reply(&identity.get_team(args(body)?).await?),
964 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'965 "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 bar966 "update_team" => reply(&identity.update_team(args(body)?).await?),
967 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
968 "team_members" => reply(&identity.team_members(args(body)?).await?),
969 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
970 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
971 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
972 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
973 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
974 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
975 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
976 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
977 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
978 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace979 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
980 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
981 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
982 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look983 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
984 "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 quotas985 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look986 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
987 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
988 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
989 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
990 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
991 "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 member992 // Deleted workspaces, restored or purged by staff; see deletion.rs.
993 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
994 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
995 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'996 // Workspace aliases, set by staff only; see aliases.rs.
997 "admin_aliases" => reply(&identity.admin_aliases().await?),
998 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
999 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign1000 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1001 };
1002 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign1003}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1004
1005#[cfg(test)]
1006mod register_tests {
1007 use super::*;
1008
1009 #[test]
1010 fn nobody_registers_as_g1t() {
1011 // What register checks the username with, whatever its case.
1012 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
1013 assert_eq!(claimable_namespace(username), None, "{username}");
1014 }
1015 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
1016 }
1017}

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