Skip to content
1,026 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;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca12mod 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;
Fine-grained personal tokens, workspace token rules and approvals in identity28mod token_reach;
Agents as a team: lifecycle, merge queue, billing and a new shell29mod tokens;
Workspaces own repositories30mod workspaces;
API and MCP server, Rust identity service, registration, site redesign31
32use g1t_contracts::identity::*;
RFC 3339 timestamps in identity and repos33use 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-agent34use g1t_contracts::{FailureCode, Outcome, User, Viewer, claimable_namespace, new_id};
API and MCP server, Rust identity service, registration, site redesign35use g1t_kit::{args, now_ms, reply, rpc_method};
36use serde::Deserialize;
Agents as a team: lifecycle, merge queue, billing and a new shell37use tokens::TOKEN_PREFIX;
API and MCP server, Rust identity service, registration, site redesign38use worker::wasm_bindgen::JsValue;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas39use worker::{Context, D1Database, Env, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
API and MCP server, Rust identity service, registration, site redesign40
RFC 3339 timestamps in identity and repos41const SESSION_TTL_SECONDS: u64 = 30 * 24 * 60 * 60;
42const VERIFY_TTL_SECONDS: u64 = 24 * 60 * 60;
43const RESET_TTL_SECONDS: u64 = 60 * 60;
API and MCP server, Rust identity service, registration, site redesign44const MIN_PASSWORD_LENGTH: usize = 10;
Email verification, password reset, and Git for AI scale positioning45const PASSWORD_TOO_SHORT: &str = "Use a password of at least 10 characters.";
46
47/// A user as selected from the database; `verified` arrives as 0 or 1.
48#[derive(Deserialize)]
49struct Account {
50 id: String,
51 username: String,
52 verified: u8,
Workspace names and icons, and a component kit for every control53 /// Selected only where the person is being shown to themselves.
54 #[serde(default)]
55 avatar: Option<String>,
Email verification, password reset, and Git for AI scale positioning56}
57
58impl From<Account> for User {
59 fn from(row: Account) -> Self {
60 User {
61 id: row.id,
62 username: row.username,
63 verified: row.verified != 0,
Workspace names and icons, and a component kit for every control64 avatar: row.avatar,
Agents as a team: lifecycle, merge queue, billing and a new shell65 ..User::default()
Email verification, password reset, and Git for AI scale positioning66 }
67 }
68}
API and MCP server, Rust identity service, registration, site redesign69
70#[derive(Deserialize)]
71struct UserRow {
72 id: String,
73 username: String,
74 password_hash: String,
Email verification, password reset, and Git for AI scale positioning75 verified: u8,
API and MCP server, Rust identity service, registration, site redesign76}
77
Email verification, password reset, and Git for AI scale positioning78/// The owner of an emailed token.
API and MCP server, Rust identity service, registration, site redesign79#[derive(Deserialize)]
Email verification, password reset, and Git for AI scale positioning80struct TokenOwner {
81 id: String,
82 username: String,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look83 /// The address a link was sent to; null on links from before accounts
84 /// had several, which are for the primary.
85 #[serde(default)]
86 email_id: Option<String>,
Email verification, password reset, and Git for AI scale positioning87}
88
89#[derive(Deserialize)]
API and MCP server, Rust identity service, registration, site redesign90struct KeyRow {
91 id: String,
92 title: String,
93 fingerprint: String,
RFC 3339 timestamps in identity and repos94 created_at: String,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca95 #[serde(default)]
96 last_used_at: Option<String>,
API and MCP server, Rust identity service, registration, site redesign97}
98
99impl From<KeyRow> for SshKey {
100 fn from(row: KeyRow) -> Self {
101 SshKey {
102 id: row.id,
103 title: row.title,
104 fingerprint: row.fingerprint,
RFC 3339 timestamps in identity and repos105 created_at: row.created_at,
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca106 last_used_at: row.last_used_at,
API and MCP server, Rust identity service, registration, site redesign107 }
108 }
109}
110
111struct Identity {
112 db: D1Database,
Email verification, password reset, and Git for AI scale positioning113 env: Env,
API and MCP server, Rust identity service, registration, site redesign114}
115
116impl Identity {
Workspaces own repositories117 /// Runs a query that returns at most one user, for showing to others:
118 /// without their workspaces.
119 async fn find_public_user(&self, sql: &str, param: &str) -> Result<Viewer> {
Email verification, password reset, and Git for AI scale positioning120 Ok(self
121 .db
API and MCP server, Rust identity service, registration, site redesign122 .prepare(sql)
123 .bind(&[JsValue::from(param)])?
Email verification, password reset, and Git for AI scale positioning124 .first::<Account>(None)
125 .await?
126 .map(User::from))
127 }
128
Workspaces own repositories129 /// Attaches the workspaces a user belongs to, so that any service can
130 /// authorize them without asking again.
131 async fn with_workspaces(&self, user: Viewer) -> Result<Viewer> {
132 let Some(mut user) = user else {
133 return Ok(None);
134 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look135 let memberships = self.memberships(&user.id).await?;
136 // Access to a workspace is used only within its policy; see security.rs.
137 user.workspaces = self.within_policy(&user.id, memberships).await?;
138 // Roles on single repositories, under the same policy (access.rs).
139 let grants = self.grants_of(&user.id).await?;
140 user.grants = self.grants_within_policy(&user.id, grants).await?;
Workspaces own repositories141 Ok(Some(user))
142 }
143
144 /// Runs a query that resolves credentials to at most one user.
145 async fn find_user(&self, sql: &str, param: &str) -> Result<Viewer> {
146 let user = self.find_public_user(sql, param).await?;
147 self.with_workspaces(user).await
148 }
149
Email verification, password reset, and Git for AI scale positioning150 /// Stores a one-time token of `kind` for the user and returns it.
RFC 3339 timestamps in identity and repos151 async fn issue_email_token(&self, user_id: &str, kind: &str, ttl: u64) -> Result<String> {
Email verification, password reset, and Git for AI scale positioning152 let token = crypto::random_hex(32);
153 self.db
RFC 3339 timestamps in identity and repos154 .prepare(format!(
Email verification, password reset, and Git for AI scale positioning155 "INSERT INTO email_tokens (id, user_id, kind, expires_at)
RFC 3339 timestamps in identity and repos156 VALUES (?, ?, ?, {})",
157 sql_after(ttl)
158 ))
Email verification, password reset, and Git for AI scale positioning159 .bind(&[
160 crypto::sha256_hex(&token).into(),
161 user_id.into(),
162 kind.into(),
163 ])?
164 .run()
165 .await?;
166 Ok(token)
API and MCP server, Rust identity service, registration, site redesign167 }
168
Email verification, password reset, and Git for AI scale positioning169 /// Consumes a token of `kind`, returning its owner if it was valid.
170 async fn redeem_email_token(&self, token: &str, kind: &str) -> Result<Option<TokenOwner>> {
171 let id = crypto::sha256_hex(token);
172 let owner = self
173 .db
RFC 3339 timestamps in identity and repos174 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look175 "SELECT users.id, users.username, email_tokens.email_id FROM email_tokens
Email verification, password reset, and Git for AI scale positioning176 JOIN users ON users.id = email_tokens.user_id
177 WHERE email_tokens.id = ? AND email_tokens.kind = ?
RFC 3339 timestamps in identity and repos178 AND email_tokens.expires_at > {SQL_NOW}"
179 ))
Email verification, password reset, and Git for AI scale positioning180 .bind(&[id.as_str().into(), kind.into()])?
181 .first::<TokenOwner>(None)
182 .await?;
183 if let Some(owner) = &owner {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look184 // Every outstanding token of this kind dies with the one used:
185 // every reset link, and every confirmation link for the same
186 // address (another address's links still work).
Email verification, password reset, and Git for AI scale positioning187 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look188 .prepare(
189 "DELETE FROM email_tokens WHERE user_id = ?1 AND kind = ?2
190 AND (?2 = 'reset' OR email_id IS ?3)",
191 )
192 .bind(&[
193 owner.id.as_str().into(),
194 kind.into(),
195 owner.email_id.as_deref().map_or(JsValue::NULL, Into::into),
196 ])?
Email verification, password reset, and Git for AI scale positioning197 .run()
198 .await?;
199 }
200 Ok(owner)
201 }
202
203 async fn send_verification(&self, user: &User, email: &str) -> Result<()> {
204 let token = self
205 .issue_email_token(&user.id, "verify", VERIFY_TTL_SECONDS)
206 .await?;
207 email::send_verification(&self.env, email, &user.username, &token).await
208 }
209
210 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 look211 if !self.allow(throttle::CONFIRM_ACCOUNT, &a.user.id).await? {
212 return Ok(Outcome::fail(FailureCode::Conflict, "Too many confirmation emails this hour. Check your inbox, or try again later."));
213 }
214 self.resend_primary(&a.user).await
Email verification, password reset, and Git for AI scale positioning215 }
216
217 async fn verify_email(&self, a: EmailTokenArgs) -> Result<Outcome<User>> {
218 let Some(owner) = self.redeem_email_token(&a.token, "verify").await? else {
219 return Ok(Outcome::fail(
220 FailureCode::Invalid,
221 "This confirmation link is not valid or has expired.",
222 ));
223 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look224 if let Outcome::Fail(failure) = self.confirm_address(&owner.id, owner.email_id.as_deref()).await? {
225 return Ok(Outcome::Fail(failure));
226 }
227 // Whether the account is confirmed: whether its primary is.
228 let verified = self
229 .find_public_user(
230 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
231 &owner.id,
232 )
233 .await?
234 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning235 Ok(Outcome::Ok(User {
236 id: owner.id,
237 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look238 verified,
Workspaces own repositories239 ..User::default()
Email verification, password reset, and Git for AI scale positioning240 }))
241 }
242
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look243 /// Any confirmed address of an account can ask for a reset; so can the
244 /// unconfirmed address a new account signed up with. See emails.rs.
Email verification, password reset, and Git for AI scale positioning245 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 look246 let allowed = self.allow(throttle::RESET_EMAIL, &a.email).await?
247 && match a.client.as_deref() {
248 Some(client) => self.allow(throttle::RESET_CLIENT, client).await?,
249 None => true,
250 };
251 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 exists252 // A failure from here on happens only for a real account, so it
253 // is logged, never answered: the reply below stays the same.
254 if let Err(error) = self.send_reset(&target).await {
255 worker::console_error!("password reset for a known address failed: {error}");
256 }
257 }
258 // The same answer either way, so addresses cannot be probed.
259 Ok(true)
260 }
261
262 /// Saves a reset link for `target` and mails it, telling the account's
263 /// other addresses.
264 async fn send_reset(&self, target: &emails::ResetTarget) -> Result<()> {
265 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look266 let token = crypto::random_hex(32);
267 self.db
268 .prepare(format!(
269 "INSERT INTO email_tokens (id, user_id, kind, expires_at, email_id)
270 VALUES (?, ?, 'reset', {}, ?)",
271 sql_after(RESET_TTL_SECONDS)
272 ))
273 .bind(&[
274 crypto::sha256_hex(&token).into(),
275 target.user_id.as_str().into(),
276 target.email_id.as_str().into(),
277 ])?
278 .run()
Email verification, password reset, and Git for AI scale positioning279 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look280 email::send_password_reset(&self.env, &target.display, &target.username, &token).await?;
281 // The primary and the backup hear of it when it went elsewhere.
282 let elsewhere = self.notice_recipients(&target.user_id, false).await?;
283 for address in elsewhere.iter().filter(|address| !address.eq_ignore_ascii_case(&target.display)) {
284 let change = format!("A password reset was asked for through {}", target.display);
285 if let Err(error) = email::send_security_notice(&self.env, address, &target.username, &change).await {
286 worker::console_error!("security notice failed: {error}");
287 }
288 }
Email verification, password reset, and Git for AI scale positioning289 }
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 exists290 Ok(())
Email verification, password reset, and Git for AI scale positioning291 }
292
293 async fn reset_password(&self, a: ResetPasswordArgs) -> Result<Outcome<User>> {
294 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
295 return Ok(Outcome::fail(FailureCode::Invalid, PASSWORD_TOO_SHORT));
296 }
297 let Some(owner) = self.redeem_email_token(&a.token, "reset").await? else {
298 return Ok(Outcome::fail(
299 FailureCode::Invalid,
300 "This reset link is not valid or has expired.",
301 ));
302 };
303 self.db
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look304 .prepare("UPDATE users SET password_hash = ? WHERE id = ?")
Email verification, password reset, and Git for AI scale positioning305 .bind(&[
306 crypto::hash_password(&a.password).into(),
307 owner.id.as_str().into(),
308 ])?
309 .run()
310 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look311 // Following an emailed link also proves the address it went to
312 // (unless another account confirmed it first).
313 let _ = self.confirm_address(&owner.id, owner.email_id.as_deref()).await?;
314 // Anyone signed in with the old password is signed out, and nobody
315 // stays locked out by the wrong guesses before it.
Email verification, password reset, and Git for AI scale positioning316 self.db
317 .prepare("DELETE FROM sessions WHERE user_id = ?")
318 .bind(&[owner.id.as_str().into()])?
319 .run()
320 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look321 self.clear(&throttle::key(throttle::PASSWORD_ACCOUNT, &owner.id)).await?;
322 self.log_security(&owner.id, "password_changed", None, None).await;
323 self.tell_primary_and_backup(&owner.id, &owner.username, "Your password was changed").await;
324 let verified = self
325 .find_public_user(
326 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE id = ?",
327 &owner.id,
328 )
329 .await?
330 .is_some_and(|user| user.verified);
Email verification, password reset, and Git for AI scale positioning331 Ok(Outcome::Ok(User {
332 id: owner.id,
333 username: owner.username,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look334 verified,
Workspaces own repositories335 ..User::default()
Email verification, password reset, and Git for AI scale positioning336 }))
337 }
338
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look339 /// The account a login names: a username, or any confirmed address.
340 async fn password_row(&self, login: &str) -> Result<Option<UserRow>> {
341 let login = login.trim().to_lowercase();
342 let (column, value) = if login.contains('@') {
343 match self.user_with_verified_email(&login).await? {
344 Some(id) => ("id", id),
345 None => return Ok(None),
346 }
347 } else {
348 ("username", login)
349 };
350 self.db
351 .prepare(format!(
352 "SELECT id, username, password_hash, email_verified_at IS NOT NULL AS verified FROM users WHERE {column} = ?"
353 ))
354 .bind(&[JsValue::from(value)])?
API and MCP server, Rust identity service, registration, site redesign355 .first::<UserRow>(None)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look356 .await
357 }
358
359 /// Checks a password for a login, throttled (see throttle.rs). The
360 /// refusal is one of two messages, the same for every account.
361 async fn checked_password(
362 &self,
363 login: &str,
364 password: &str,
365 client: Option<&str>,
366 ) -> Result<std::result::Result<User, &'static str>> {
367 let row = self.password_row(login).await?;
368 let subject = row.as_ref().map_or_else(|| login.trim().to_lowercase(), |row| row.id.clone());
369 let (account_key, client_key) = Identity::password_keys(&subject, client);
370 if self.password_locked(&account_key, client_key.as_deref()).await? {
371 return Ok(Err(throttle::THROTTLED));
372 }
373 let owner = row.as_ref().map(|row| (row.id.clone(), row.username.clone()));
374 match row.filter(|row| !row.password_hash.is_empty() && crypto::verify_password(password, &row.password_hash)) {
375 Some(row) => {
376 self.clear(&account_key).await?;
377 Ok(Ok(User {
378 id: row.id,
379 username: row.username,
380 verified: row.verified != 0,
381 ..User::default()
382 }))
383 }
384 None => {
385 let owner = owner.as_ref().map(|(id, name)| (id.as_str(), name.as_str()));
386 self.password_failed(&account_key, client_key.as_deref(), owner).await?;
387 Ok(Err("Incorrect username or password."))
388 }
389 }
390 }
391
392 async fn user_for_password(&self, login: &str, password: &str) -> Result<Viewer> {
393 let user = self.checked_password(login, password, None).await?.ok();
Workspaces own repositories394 self.with_workspaces(user).await
API and MCP server, Rust identity service, registration, site redesign395 }
396
397 async fn register(&self, a: RegisterArgs) -> Result<Outcome<SignedIn>> {
398 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-agent399 let claimable = claimable_namespace(&username).is_some();
API and MCP server, Rust identity service, registration, site redesign400 let email = a.email.trim().to_lowercase();
401 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 look402 let invite_code = a.invite_code.as_deref().map(str::trim).filter(|code| !code.is_empty());
403 // The invite first: without one, nothing else on the form matters.
404 if self.invites_required() && invite_code.is_none() {
405 return Ok(Outcome::fail(FailureCode::Forbidden, invites::MISSING));
406 }
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent407 if !claimable {
API and MCP server, Rust identity service, registration, site redesign408 return invalid(
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent409 "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 redesign410 );
411 }
412 let well_formed_email = email
413 .split_once('@')
414 .is_some_and(|(local, domain)| !local.is_empty() && domain.contains('.'))
415 && !email.contains(char::is_whitespace);
416 if !well_formed_email {
417 return invalid("Enter a valid email address.");
418 }
419 if a.password.chars().count() < MIN_PASSWORD_LENGTH {
Email verification, password reset, and Git for AI scale positioning420 return invalid(PASSWORD_TOO_SHORT);
API and MCP server, Rust identity service, registration, site redesign421 }
422 let taken = self
423 .db
Agents as a team: lifecycle, merge queue, billing and a new shell424 // Usernames and workspaces share one namespace, so that a name
425 // means the same thing wherever it appears.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look426 // An address is taken once an account has confirmed it; an
427 // unconfirmed one goes to whoever confirms it first (emails.rs).
Agents as a team: lifecycle, merge queue, billing and a new shell428 .prepare(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look429 "SELECT username FROM users WHERE username = ?
430 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 shell431 UNION ALL SELECT slug FROM workspaces WHERE slug = ?",
432 )
433 .bind(&[
434 username.as_str().into(),
435 email.as_str().into(),
436 username.as_str().into(),
437 ])?
API and MCP server, Rust identity service, registration, site redesign438 .first::<serde_json::Value>(None)
439 .await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look440 // A renamed workspace's old slug stays reserved for it a while, and
441 // a deleted workspace's for good.
442 if taken.is_some() || self.slug_held(&username).await? || self.slug_deleted(&username).await? {
API and MCP server, Rust identity service, registration, site redesign443 return Ok(Outcome::fail(
444 FailureCode::Conflict,
445 "That username or email is already registered.",
446 ));
447 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look448 let password_hash = crypto::hash_password(&a.password);
449 let user = match self
450 .create_account(invites::NewAccount {
451 username: &username,
452 email: &email,
453 password_hash: &password_hash,
454 verified: false,
455 invite_code,
456 client: a.client.as_deref(),
457 })
458 .await?
459 {
460 Outcome::Ok(user) => user,
461 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
API and MCP server, Rust identity service, registration, site redesign462 };
Email verification, password reset, and Git for AI scale positioning463 // 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 quotas464 // An invite sent to this address confirmed it already (invites.rs).
465 if !user.verified
466 && let Err(error) = self.send_verification(&user, &email).await
467 {
Email verification, password reset, and Git for AI scale positioning468 worker::console_error!("verification email failed: {error}");
469 }
API and MCP server, Rust identity service, registration, site redesign470 self.start_session(user).await
471 }
472
473 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 look474 let user = match self.checked_password(&a.username, &a.password, a.client.as_deref()).await? {
475 Ok(user) => user,
476 Err(message) => return Ok(Outcome::fail(FailureCode::Unauthenticated, message)),
API and MCP server, Rust identity service, registration, site redesign477 };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look478 let user = self.with_workspaces(Some(user)).await?.unwrap_or_default();
API and MCP server, Rust identity service, registration, site redesign479 self.start_session(user).await
480 }
481
482 async fn start_session(&self, user: User) -> Result<Outcome<SignedIn>> {
483 let session_token = crypto::random_hex(32);
484 self.db
RFC 3339 timestamps in identity and repos485 .prepare(format!(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look486 // Signing in is proof it is the person: see security.rs.
487 "INSERT INTO sessions (id, user_id, expires_at, authenticated_at) VALUES (?, ?, {}, {SQL_NOW})",
RFC 3339 timestamps in identity and repos488 sql_after(SESSION_TTL_SECONDS)
489 ))
API and MCP server, Rust identity service, registration, site redesign490 .bind(&[
491 crypto::sha256_hex(&session_token).into(),
492 user.id.as_str().into(),
493 ])?
494 .run()
495 .await?;
496 Ok(Outcome::Ok(SignedIn {
497 user,
498 session_token,
499 }))
500 }
501
502 async fn sign_out(&self, a: SessionArgs) -> Result<()> {
503 self.db
504 .prepare("DELETE FROM sessions WHERE id = ?")
505 .bind(&[crypto::sha256_hex(&a.session_token).into()])?
506 .run()
507 .await?;
508 Ok(())
509 }
510
511 async fn user_for_session(&self, a: SessionArgs) -> Result<Viewer> {
512 self.find_user(
RFC 3339 timestamps in identity and repos513 &format!(
Workspace names and icons, and a component kit for every control514 "SELECT users.id, users.username, users.email_verified_at IS NOT NULL AS verified,
515 users.avatar
RFC 3339 timestamps in identity and repos516 FROM sessions JOIN users ON users.id = sessions.user_id
517 WHERE sessions.id = ? AND sessions.expires_at > {SQL_NOW}"
518 ),
API and MCP server, Rust identity service, registration, site redesign519 &crypto::sha256_hex(&a.session_token),
520 )
521 .await
522 }
523
524 async fn user_for_git_credentials(&self, a: GitCredentialsArgs) -> Result<Viewer> {
525 // Like GitHub, a token alone identifies its user.
526 if a.secret.starts_with(TOKEN_PREFIX) {
527 self.user_for_access_token(&a.secret).await
528 } else {
529 self.user_for_password(&a.username, &a.secret).await
530 }
531 }
532
533 async fn user_for_ssh_key(&self, a: FingerprintArgs) -> Result<Viewer> {
534 self.find_user(
Email verification, password reset, and Git for AI scale positioning535 "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 redesign536 JOIN users ON users.id = ssh_keys.user_id
537 WHERE fingerprint = ?",
538 &a.fingerprint,
539 )
540 .await
541 }
542
543 async fn user_by_username(&self, a: UsernameArgs) -> Result<Viewer> {
Workspaces own repositories544 self.find_public_user(
Email verification, password reset, and Git for AI scale positioning545 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
API and MCP server, Rust identity service, registration, site redesign546 &a.username.to_lowercase(),
547 )
548 .await
549 }
550
Inbox: threads, reasons, subscriptions and watching551 /// `notify_by_email`: an inbox item, emailed to the person it is for,
552 /// only at a confirmed address and only while they can still read the
553 /// repository it is about. Returns whether it was sent.
554 async fn notify_by_email(&self, a: g1t_contracts::inbox::NotifyByEmailArgs) -> Result<bool> {
555 #[derive(Deserialize)]
556 struct Address {
557 email: Option<String>,
558 }
559 let user = self
560 .find_user(
561 "SELECT id, username, email_verified_at IS NOT NULL AS verified FROM users WHERE username = ?",
562 &a.username.to_lowercase(),
563 )
564 .await?;
565 let Some(user) = user.filter(|user| user.verified) else {
566 return Ok(false);
567 };
568 let readable: Vec<g1t_contracts::repos::Repo> = g1t_kit::call(
569 &self.env.service("REPOS")?,
570 "readable",
571 &g1t_contracts::repos::ReadableArgs {
572 ids: vec![a.repo_id.clone()],
573 viewer: Some(user.clone()),
574 },
575 )
576 .await?;
577 if readable.is_empty() {
578 return Ok(false);
579 }
580 let address = self
581 .db
582 .prepare("SELECT email FROM users WHERE id = ?")
583 .bind(&[user.id.as_str().into()])?
584 .first::<Address>(None)
585 .await?
586 .and_then(|row| row.email)
587 .filter(|email| !email.trim().is_empty());
588 let Some(address) = address else {
589 return Ok(false);
590 };
591 email::send_notification(&self.env, &address, &a).await?;
592 Ok(true)
593 }
594
What happened across an outcome, as a feed beside its graph595 async fn usernames(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, String>> {
596 #[derive(serde::Deserialize)]
597 struct Named {
598 id: String,
599 name: String,
600 }
601 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
602 let mut names = std::collections::HashMap::new();
603 if ids.is_empty() {
604 return Ok(names);
605 }
606 let marks = vec!["?"; ids.len()].join(", ");
607 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
608 for sql in [
609 format!("SELECT id, username AS name FROM users WHERE id IN ({marks})"),
610 format!("SELECT id, slug AS name FROM workspaces WHERE id IN ({marks})"),
611 ] {
612 for row in self.db.prepare(sql).bind(&bind)?.all().await?.results::<Named>()? {
613 names.insert(row.id, row.name);
614 }
615 }
616 Ok(names)
617 }
618
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97619 /// `accounts`: the accounts behind these ids (at most 200), each with
620 /// its username and avatar, for lists that keep ids, such as who
621 /// starred a repository. Ids of no account are left out.
622 async fn accounts(&self, a: UsernamesArgs) -> Result<std::collections::HashMap<String, g1t_contracts::accounts::EmailOwner>> {
623 #[derive(serde::Deserialize)]
624 struct Row {
625 id: String,
626 username: String,
627 avatar: Option<String>,
628 }
629 let ids: Vec<String> = a.ids.into_iter().take(200).collect();
630 let mut found = std::collections::HashMap::new();
631 if ids.is_empty() {
632 return Ok(found);
633 }
634 let marks = vec!["?"; ids.len()].join(", ");
635 let bind: Vec<worker::wasm_bindgen::JsValue> = ids.iter().map(|id| id.as_str().into()).collect();
636 let rows = self
637 .db
638 .prepare(format!("SELECT id, username, avatar FROM users WHERE id IN ({marks})"))
639 .bind(&bind)?
640 .all()
641 .await?
642 .results::<Row>()?;
643 for row in rows {
644 found.insert(row.id.clone(), g1t_contracts::accounts::EmailOwner { id: row.id, username: row.username, avatar: row.avatar });
645 }
646 Ok(found)
647 }
648
API and MCP server, Rust identity service, registration, site redesign649 async fn list_ssh_keys(&self, a: UserArgs) -> Result<Vec<SshKey>> {
650 let rows = self
651 .db
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca652 .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 redesign653 .bind(&[a.user.id.into()])?
654 .all()
655 .await?
656 .results::<KeyRow>()?;
657 Ok(rows.into_iter().map(SshKey::from).collect())
658 }
659
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge660 /// The account (user id) that registered each key, by fingerprint
661 /// (`SHA256:…`). At most 100; unknown keys are left out.
662 async fn ssh_key_owners(&self, a: SshKeyOwnersArgs) -> Result<std::collections::HashMap<String, String>> {
663 #[derive(serde::Deserialize)]
664 struct Row {
665 fingerprint: String,
666 user_id: String,
667 }
668 let fingerprints: Vec<&String> = a.fingerprints.iter().take(100).collect();
669 if fingerprints.is_empty() {
670 return Ok(std::collections::HashMap::new());
671 }
672 let marks = vec!["?"; fingerprints.len()].join(", ");
673 let binds: Vec<JsValue> = fingerprints.iter().map(|fingerprint| fingerprint.as_str().into()).collect();
674 Ok(self
675 .db
676 .prepare(format!("SELECT fingerprint, user_id FROM ssh_keys WHERE fingerprint IN ({marks})"))
677 .bind(&binds)?
678 .all()
679 .await?
680 .results::<Row>()?
681 .into_iter()
682 .map(|row| (row.fingerprint, row.user_id))
683 .collect())
684 }
685
API and MCP server, Rust identity service, registration, site redesign686 async fn add_ssh_key(&self, a: AddSshKeyArgs) -> Result<Outcome<SshKey>> {
687 let Some(key) = crypto::parse_ssh_key(&a.public_key) else {
688 return Ok(Outcome::fail(
689 FailureCode::Invalid,
690 "That is not a valid OpenSSH public key.",
691 ));
692 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca693 // Someone's SSH key, or a repository's deploy key (deploy_keys.rs).
694 if self.key_in_use(&key.fingerprint).await? {
695 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
API and MCP server, Rust identity service, registration, site redesign696 }
697 let now = now_ms();
698 let title = [a.title.trim(), key.comment.as_str(), "SSH key"]
699 .into_iter()
700 .find(|candidate| !candidate.is_empty())
701 .unwrap_or_default()
702 .to_owned();
703 let row = KeyRow {
704 id: new_id("key", now),
705 title,
706 fingerprint: key.fingerprint,
RFC 3339 timestamps in identity and repos707 created_at: rfc3339(now),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca708 last_used_at: None,
API and MCP server, Rust identity service, registration, site redesign709 };
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca710 let inserted = self.db
API and MCP server, Rust identity service, registration, site redesign711 .prepare(
712 "INSERT INTO ssh_keys (id, user_id, title, public_key, fingerprint, created_at)
713 VALUES (?, ?, ?, ?, ?, ?)",
714 )
715 .bind(&[
716 row.id.as_str().into(),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca717 a.user.id.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign718 row.title.as_str().into(),
719 key.public_key.into(),
720 row.fingerprint.as_str().into(),
RFC 3339 timestamps in identity and repos721 row.created_at.as_str().into(),
API and MCP server, Rust identity service, registration, site redesign722 ])?
723 .run()
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca724 .await;
725 // Added at the same moment elsewhere: the trigger or the unique
726 // index refused it.
727 if let Err(error) = inserted {
728 if self.key_in_use(&row.fingerprint).await? {
729 return Ok(Outcome::fail(FailureCode::Conflict, g1t_contracts::deploy_keys::KEY_IN_USE));
730 }
731 return Err(error);
732 }
733 let added = format!("{} ({})", row.title, row.fingerprint);
734 self.log_security(&a.user.id, "ssh_key_added", Some(&added), None).await;
API and MCP server, Rust identity service, registration, site redesign735 Ok(Outcome::Ok(row.into()))
736 }
737
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca738 /// Deletes one of the person's SSH keys, and says so in their security log.
739 async fn remove_ssh_key(&self, a: RemoveArgs) -> Result<()> {
740 let key = self
741 .db
742 .prepare("SELECT id, title, fingerprint, created_at FROM ssh_keys WHERE id = ? AND user_id = ?")
743 .bind(&[a.id.as_str().into(), a.user.id.as_str().into()])?
744 .first::<KeyRow>(None)
745 .await?;
746 let Some(key) = key else {
747 return Ok(());
748 };
749 let user_id = a.user.id.clone();
750 self.remove("ssh_keys", a).await?;
751 let removed = format!("{} ({})", key.title, key.fingerprint);
752 self.log_security(&user_id, "ssh_key_removed", Some(&removed), None).await;
753 Ok(())
754 }
755
API and MCP server, Rust identity service, registration, site redesign756 /// Deletes a row the user owns from `table`.
757 async fn remove(&self, table: &str, a: RemoveArgs) -> Result<()> {
758 self.db
759 .prepare(format!("DELETE FROM {table} WHERE id = ? AND user_id = ?"))
760 .bind(&[a.id.into(), a.user.id.into()])?
761 .run()
762 .await?;
763 Ok(())
764 }
765}
766
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas767/// 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 member768/// the last summary's window was still open, so none waits on a later one;
769/// and deleted workspaces past their restore window are purged
770/// (deletion.rs).
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas771#[event(scheduled)]
772async fn scheduled(_event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
773 let Ok(db) = env.d1("DB") else { return };
774 let identity = Identity { db, env };
775 if let Err(error) = identity.notify_staff_of_requests().await {
776 worker::console_error!("waitlist summary: {error}");
777 }
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member778 if let Err(error) = identity.purge_due_workspaces().await {
779 worker::console_error!("workspace purge: {error}");
780 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas781}
782
API and MCP server, Rust identity service, registration, site redesign783#[event(fetch)]
784async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
785 let Some(method) = rpc_method(&request) else {
786 return Response::error("Not found", 404);
787 };
Fast pages, required checks on the branch, self-hosted runners, honest incidents788 // A replica near the caller when it asks for one (crates/kit/src/d1.rs).
789 let (db, served) = g1t_kit::d1::open(&env, "DB", &request)?;
API and MCP server, Rust identity service, registration, site redesign790 let body: serde_json::Value = request.json().await?;
Fast pages, required checks on the branch, self-hosted runners, honest incidents791 let identity = Identity { db, env };
API and MCP server, Rust identity service, registration, site redesign792
Fast pages, required checks on the branch, self-hosted runners, honest incidents793 let answered = match method.as_str() {
Search across all of g1t, Explore, and a command palette794 "register" => {
795 let outcome = identity.register(args(body)?).await?;
796 if let Outcome::Ok(signed_in) = &outcome {
797 identity.announce_user(&signed_in.user.username, Some(&signed_in.user.id)).await;
798 }
799 reply(&outcome)
800 }
API and MCP server, Rust identity service, registration, site redesign801 "sign_in" => reply(&identity.sign_in(args(body)?).await?),
Search across all of g1t, Explore, and a command palette802 "create_workspace" => {
803 let outcome = identity.create_workspace(args(body)?).await?;
804 if let Outcome::Ok(workspace) = &outcome {
805 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
806 }
807 reply(&outcome)
808 }
Workspaces own repositories809 "get_workspace" => reply(&identity.get_workspace(args(body)?).await?),
810 "list_members" => reply(&identity.list_members(args(body)?).await?),
811 "add_member" => reply(&identity.add_member(args(body)?).await?),
812 "remove_member" => reply(&identity.remove_member(args(body)?).await?),
Search across all of g1t, Explore, and a command palette813 "update_workspace" => {
814 let outcome = identity.update_workspace(args(body)?).await?;
815 if let Outcome::Ok(workspace) = &outcome {
816 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
817 }
818 reply(&outcome)
819 }
Agents and memory, checks and conflicts, profiles, slug renames, custom domains820 "rename_workspace" => reply(&identity.rename_workspace(args(body)?).await?),
821 "check_workspace_rename" => reply(&identity.check_workspace_rename(args(body)?).await?),
822 "resolve_slug" => reply(&identity.resolve_slug(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'823 "resolve_alias" => reply(&identity.resolve_alias(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look824 "check_workspace_deletion" => reply(&identity.check_workspace_deletion(args(body)?).await?),
825 "delete_workspace" => reply(&identity.delete_workspace(args(body)?).await?),
826 "transfer_repo_scopes" => reply(&identity.transfer_repo_scopes(args(body)?).await?),
Search across all of g1t, Explore, and a command palette827 "set_workspace_avatar" => {
828 let outcome = identity.set_workspace_avatar(args(body)?).await?;
829 if let Outcome::Ok(workspace) = &outcome {
830 identity.announce_workspace(&workspace.id, &workspace.slug, None).await;
831 }
832 reply(&outcome)
833 }
834 "set_user_avatar" => {
835 let a: SetUserAvatarArgs = args(body)?;
836 let (username, id) = (a.user.username.clone(), a.user.id.clone());
837 let outcome = identity.set_user_avatar(a).await?;
838 if matches!(outcome, Outcome::Ok(_)) {
839 identity.announce_user(&username, Some(&id)).await;
840 }
841 reply(&outcome)
842 }
Agents as a team: lifecycle, merge queue, billing and a new shell843 "list_workspace_tokens" => reply(&identity.list_workspace_tokens(args(body)?).await?),
844 "create_workspace_token" => reply(&identity.create_workspace_token(args(body)?).await?),
845 "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 look846 // Signing in with GitHub; see github.rs.
847 "github_enabled" => reply(&identity.github_enabled()),
848 "github_start" => reply(&identity.github_start(args(body)?).await?),
849 "github_finish" => reply(&identity.github_finish(args(body)?).await?),
850 "github_pending" => reply(&identity.github_pending(args(body)?).await?),
851 "github_sign_up" => reply(&identity.github_sign_up(args(body)?).await?),
852 "github_claim" => reply(&identity.github_claim(args(body)?).await?),
853 "github_account" => reply(&identity.github_account(args(body)?).await?),
854 "github_unlink" => reply(&identity.github_unlink(args(body)?).await?),
855 "github_user_token" => reply(&identity.github_user_token(args(body)?).await?),
856 "github_revoked" => reply(&identity.github_revoked(args(body)?).await?),
857 "github_usernames" => reply(&identity.github_usernames(args(body)?).await?),
OAuth 2.1 sign-in for MCP clients and other applications858 "oauth_authorize" => reply(&identity.oauth_authorize(args(body)?).await?),
859 "oauth_exchange" => reply(&identity.oauth_exchange(args(body)?).await?),
860 "oauth_refresh" => reply(&identity.oauth_refresh(args(body)?).await?),
861 "list_oauth_grants" => reply(&identity.list_oauth_grants(args(body)?).await?),
862 "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 step863 "update_oauth_grant" => reply(&identity.update_oauth_grant(args(body)?).await?),
Device sign-in replaces registering and minting tokens over the API864 "device_start" => reply(&identity.device_start(args(body)?).await?),
865 "device_lookup" => reply(&identity.device_lookup(args(body)?).await?),
866 "device_resolve" => reply(&identity.device_resolve(args(body)?).await?),
867 "device_claim" => reply(&identity.device_claim(args(body)?).await?),
Email verification, password reset, and Git for AI scale positioning868 "resend_verification" => reply(&identity.resend_verification(args(body)?).await?),
869 "verify_email" => reply(&identity.verify_email(args(body)?).await?),
870 "request_password_reset" => reply(&identity.request_password_reset(args(body)?).await?),
871 "reset_password" => reply(&identity.reset_password(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look872 // A person's email addresses; see emails.rs and security.rs.
873 "list_emails" => reply(&identity.list_emails(args(body)?).await?),
874 "add_email" => reply(&identity.add_email(args(body)?).await?),
875 "remove_email" => reply(&identity.remove_email(args(body)?).await?),
876 "resend_email_verification" => reply(&identity.resend_email_verification(args(body)?).await?),
877 "update_email_settings" => reply(&identity.update_email_settings(args(body)?).await?),
878 "reauthenticate" => reply(&identity.reauthenticate(args(body)?).await?),
879 "security_log" => reply(&identity.security_log(args(body)?).await?),
880 "email_owners" => reply(&identity.email_owners(args(body)?).await?),
881 "commit_identity" => reply(&identity.commit_identity(args(body)?).await?),
882 "push_email_guard" => reply(&identity.push_email_guard(args(body)?).await?),
883 "admin_user" => reply(&identity.admin_user(args(body)?).await?),
884 "admin_remove_email" => reply(&identity.admin_remove_email(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign885 "sign_out" => reply(&identity.sign_out(args(body)?).await?),
886 "user_for_session" => reply(&identity.user_for_session(args(body)?).await?),
887 "user_for_git_credentials" => reply(&identity.user_for_git_credentials(args(body)?).await?),
888 "user_for_access_token" => {
889 let a: TokenArgs = args(body)?;
890 reply(&identity.user_for_access_token(&a.token).await?)
891 }
892 "user_for_ssh_key" => reply(&identity.user_for_ssh_key(args(body)?).await?),
893 "user_by_username" => reply(&identity.user_by_username(args(body)?).await?),
What happened across an outcome, as a feed beside its graph894 "usernames" => reply(&identity.usernames(args(body)?).await?),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97895 "accounts" => reply(&identity.accounts(args(body)?).await?),
Inbox: threads, reasons, subscriptions and watching896 "notify_by_email" => reply(&identity.notify_by_email(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains897 "profile" => reply(&identity.profile(args(body)?).await?),
Search across all of g1t, Explore, and a command palette898 "update_profile" => {
899 let outcome = identity.update_profile(args(body)?).await?;
900 if let Outcome::Ok(profile) = &outcome {
901 identity.announce_user(&profile.username, None).await;
902 }
903 reply(&outcome)
904 }
905 "directory" => reply(&identity.directory(args(body)?).await?),
Agents and memory, checks and conflicts, profiles, slug renames, custom domains906 "profile_workspaces" => reply(&identity.profile_workspaces(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign907 "list_ssh_keys" => reply(&identity.list_ssh_keys(args(body)?).await?),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge908 // Services only: who registered each key, for verifying commit
909 // signatures (repos' signatures.rs).
910 "ssh_key_owners" => reply(&identity.ssh_key_owners(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign911 "add_ssh_key" => reply(&identity.add_ssh_key(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca912 "remove_ssh_key" => reply(&identity.remove_ssh_key(args(body)?).await?),
913 // A repository's deploy keys, and who an SSH key signs in as; see
914 // deploy_keys.rs.
915 "list_deploy_keys" => reply(&identity.list_deploy_keys(args(body)?).await?),
916 "get_deploy_key" => reply(&identity.get_deploy_key(args(body)?).await?),
917 "add_deploy_key" => reply(&identity.add_deploy_key(args(body)?).await?),
918 "remove_deploy_key" => reply(&identity.remove_deploy_key(args(body)?).await?),
919 "principal_for_ssh_key" => reply(&identity.principal_for_ssh_key(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign920 "list_access_tokens" => reply(&identity.list_access_tokens(args(body)?).await?),
921 "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 step922 "update_access_token" => reply(&identity.update_access_token(args(body)?).await?),
Fine-grained personal tokens, workspace token rules and approvals in identity923 // Fine-grained tokens and workspaces' rules for tokens; see token_reach.rs.
924 "create_fine_grained_token" => reply(&identity.create_fine_grained_token(args(body)?).await?),
925 "update_fine_grained_token" => reply(&identity.update_fine_grained_token(args(body)?).await?),
926 "get_token_policy" => reply(&identity.get_token_policy(args(body)?).await?),
927 "set_token_policy" => reply(&identity.set_token_policy(args(body)?).await?),
928 "list_member_tokens" => reply(&identity.list_member_tokens(args(body)?).await?),
929 "review_token_request" => reply(&identity.review_token_request(args(body)?).await?),
930 "revoke_member_token" => reply(&identity.revoke_member_token(args(body)?).await?),
Agents as a team: lifecycle, merge queue, billing and a new shell931 "create_agent_token" => reply(&identity.create_agent_token(args(body)?).await?),
932 "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 API933 "create_run_credential" => reply(&identity.create_run_credential(args(body)?).await?),
934 "bind_run_credentials" => reply(&identity.bind_run_credentials(args(body)?).await?),
935 "revoke_run_credentials" => reply(&identity.revoke_run_credentials(args(body)?).await?),
Merge branch 'worktree-agent-a3abfcce648e87dca'936 "create_job_token" => reply(&identity.create_job_token(args(body)?).await?),
937 "revoke_job_tokens" => reply(&identity.revoke_job_tokens(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign938 "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 look939 // Invites and the waitlist; see invites.rs.
940 "registration" => reply(&identity.registration_mode()),
941 "list_invites" => reply(&identity.list_invites(args(body)?).await?),
942 "create_invite" => reply(&identity.create_invite(args(body)?).await?),
943 "revoke_invite" => reply(&identity.revoke_invite(args(body)?).await?),
944 "check_invite" => reply(&identity.check_invite(args(body)?).await?),
945 "accept_invite" => reply(&identity.accept_invite(args(body)?).await?),
946 "invite_member" => reply(&identity.invite_member(args(body)?).await?),
947 "workspace_invites" => reply(&identity.workspace_invites(args(body)?).await?),
948 "revoke_workspace_invite" => reply(&identity.revoke_workspace_invite(args(body)?).await?),
949 "request_access" => reply(&identity.request_access(args(body)?).await?),
950 // Who has access to a repository; see access.rs.
951 "repo_access" => reply(&identity.repo_access(args(body)?).await?),
952 "add_collaborator" => reply(&identity.add_collaborator(args(body)?).await?),
953 "set_collaborator_role" => reply(&identity.set_collaborator_role(args(body)?).await?),
954 "remove_collaborator" => reply(&identity.remove_collaborator(args(body)?).await?),
955 "collaborator_permission" => reply(&identity.collaborator_permission(args(body)?).await?),
956 "my_repo_invitations" => reply(&identity.my_repo_invitations(args(body)?).await?),
957 "respond_repo_invitation" => reply(&identity.respond_repo_invitation(args(body)?).await?),
958 "revoke_repo_invitation" => reply(&identity.revoke_repo_invitation(args(body)?).await?),
959 "set_base_permission" => reply(&identity.set_base_permission(args(body)?).await?),
Merge branch 'worktree-agent-a2013627e5ea4ab13'960 // Where a workspace keeps its repositories' git data (EU residency).
961 "workspace_residency" => reply(&identity.workspace_residency(args(body)?).await?),
962 "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 look963 "outside_collaborators" => reply(&identity.outside_collaborators(args(body)?).await?),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca964 "forget_repo_access" => {
965 let a: g1t_contracts::access::ForgetRepoAccessArgs = args(body)?;
966 // A purged repository's deploy keys go with its access.
967 identity.forget_deploy_keys(&a.repo_id).await?;
968 reply(&identity.forget_repo_access(a).await?)
969 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar970 // Teams (teams.rs).
971 "list_teams" => reply(&identity.list_teams(args(body)?).await?),
972 "get_team" => reply(&identity.get_team(args(body)?).await?),
973 "create_team" => reply(&identity.create_team(args(body)?).await?),
Merge branch 'worktree-agent-ad7c6d88d93adc817'974 "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 bar975 "update_team" => reply(&identity.update_team(args(body)?).await?),
976 "delete_team" => reply(&identity.delete_team(args(body)?).await?),
977 "team_members" => reply(&identity.team_members(args(body)?).await?),
978 "set_team_member" => reply(&identity.set_team_member(args(body)?).await?),
979 "remove_team_member" => reply(&identity.remove_team_member(args(body)?).await?),
980 "child_teams" => reply(&identity.child_teams(args(body)?).await?),
981 "team_repos" => reply(&identity.team_repos(args(body)?).await?),
982 "set_team_repo" => reply(&identity.set_team_repo(args(body)?).await?),
983 "remove_team_repo" => reply(&identity.remove_team_repo(args(body)?).await?),
984 "user_teams" => reply(&identity.user_teams(args(body)?).await?),
985 "team_memberships" => reply(&identity.team_memberships(args(body)?).await?),
986 "resolve_teams" => reply(&identity.resolve_teams(args(body)?).await?),
987 "resolve_owners" => reply(&identity.resolve_owners(args(body)?).await?),
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace988 // Staff only: sudo.g1t.sh, over its service binding. See admin.rs.
989 "notify_owners" => reply(&identity.notify_owners(args(body)?).await?),
990 "admin_workspaces" => reply(&identity.admin_workspaces(args(body)?).await?),
991 "admin_workspace" => reply(&identity.admin_workspace(args(body)?).await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look992 "admin_waitlist" => reply(&identity.admin_waitlist(args(body)?).await?),
993 "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 quotas994 "admin_waitlist_pending" => reply(&identity.admin_waitlist_pending().await?),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look995 "admin_invites" => reply(&identity.admin_invites(args(body)?).await?),
996 "admin_revoke_invite" => reply(&identity.admin_revoke_invite(args(body)?).await?),
997 "admin_mint_invite" => reply(&identity.admin_mint_invite(args(body)?).await?),
998 "admin_grant_invites" => reply(&identity.admin_grant_invites(args(body)?).await?),
999 "admin_invite_tree" => reply(&identity.admin_invite_tree(args(body)?).await?),
1000 "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 member1001 // Deleted workspaces, restored or purged by staff; see deletion.rs.
1002 "admin_deleted_workspaces" => reply(&identity.admin_deleted_workspaces().await?),
1003 "admin_restore_workspace" => reply(&identity.admin_restore_workspace(args(body)?).await?),
1004 "admin_purge_workspace" => reply(&identity.admin_purge_workspace(args(body)?).await?),
Merge branch 'worktree-agent-a8385d293d42c913a'1005 // Workspace aliases, set by staff only; see aliases.rs.
1006 "admin_aliases" => reply(&identity.admin_aliases().await?),
1007 "admin_set_alias" => reply(&identity.admin_set_alias(args(body)?).await?),
1008 "admin_remove_alias" => reply(&identity.admin_remove_alias(args(body)?).await?),
API and MCP server, Rust identity service, registration, site redesign1009 _ => Response::error("Unknown method", 404),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1010 };
1011 served.finish(answered)
API and MCP server, Rust identity service, registration, site redesign1012}
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent1013
1014#[cfg(test)]
1015mod register_tests {
1016 use super::*;
1017
1018 #[test]
1019 fn nobody_registers_as_g1t() {
1020 // What register checks the username with, whatever its case.
1021 for username in ["g1t", "G1T", "g1t-agent", "G1t-Agent"] {
1022 assert_eq!(claimable_namespace(username), None, "{username}");
1023 }
1024 assert_eq!(claimable_namespace("ana").as_deref(), Some("ana"));
1025 }
1026}

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