Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
314 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.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1/**
2 * Who may do what in a repository: repository roles, the capabilities each
3 * one carries, and how a person's permission is worked out.
4 *
5 * Mirrors `crates/contracts/src/access.rs`, which holds the one permission
6 * table; a test there reads `CAPABILITIES` and `OWNER_ONLY` below and fails
7 * when the two differ. Keep each row on one line.
8 */
9import type { Membership, Role, User } from "./identity";
Merge main (membership, two-factor, GitHub repo roles) into tokens10import type { MemberPrivileges } from "./members";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas11import type { Result } from "./result";
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar12import type { RepoTeam } from "./teams";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas13
Merge main (membership, two-factor, GitHub repo roles) into tokens14/** What members may do in a workspace that has not chosen otherwise: what they could before the setting existed. */
15export const DEFAULT_MEMBER_PRIVILEGES: MemberPrivileges = {
16 members_can_create_public_repositories: true,
17 members_can_create_private_repositories: true,
18 members_can_change_repo_visibility: true,
19 members_can_delete_repositories: false,
20 members_can_invite_outside_collaborators: true,
21};
22
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas23/** What someone may do in one repository, from least to most. */
24export type RepoRole = "read" | "triage" | "write" | "maintain" | "admin";
25
26export const REPO_ROLES: readonly RepoRole[] = ["read", "triage", "write", "maintain", "admin"];
27
28export const REPO_ROLE_LABELS: Record<RepoRole, string> = {
29 read: "Read",
30 triage: "Triage",
31 write: "Write",
32 maintain: "Maintain",
33 admin: "Admin",
34};
35
36/** One line on what each role is for, as role pickers show it. */
37export const REPO_ROLE_SUMMARIES: Record<RepoRole, string> = {
38 read: "Read and clone; open issues and pull requests, and comment.",
Merge main (membership, two-factor, GitHub repo roles) into tokens39 triage: "Read, and manage issues and pull requests: apply labels, assign, close.",
40 write: "Triage, and push, merge, manage labels, see security alerts, and put agents to work.",
41 maintain: "Write, and manage the repository's settings and topics.",
42 admin: "Everything: branch protection, webhooks, secrets, security, access, name and visibility.",
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas43};
44
45/** What every member of a workspace gets on each of its repositories. */
46export type BasePermission = "none" | "read" | "write" | "admin";
47
48export const BASE_PERMISSIONS: readonly BasePermission[] = ["none", "read", "write", "admin"];
49
Merge main (membership, two-factor, GitHub repo roles) into tokens50/** What a membership that does not say gets: what members could do before roles. */
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas51export const DEFAULT_BASE_PERMISSION: BasePermission = "write";
52
Merge main (membership, two-factor, GitHub repo roles) into tokens53/** What a new workspace's members get, as on GitHub. Workspaces made before 2026-10-08 kept Write. */
54export const NEW_WORKSPACE_BASE_PERMISSION: BasePermission = "read";
55
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas56export const BASE_PERMISSION_LABELS: Record<BasePermission, string> = {
57 none: "No permission",
58 read: "Read",
59 write: "Write",
60 admin: "Admin",
61};
62
63export type Capability =
64 | "read"
65 | "participate"
66 | "triage"
67 | "push"
68 | "merge"
Merge main (membership, two-factor, GitHub repo roles) into tokens69 | "manage_labels"
70 | "security_alerts"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas71 | "run"
72 | "manage_settings"
73 | "manage_protection"
Merge main (membership, two-factor, GitHub repo roles) into tokens74 | "manage_security"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas75 | "manage_integrations"
76 | "manage_access"
77 | "administer"
Merge main (membership, two-factor, GitHub repo roles) into tokens78 | "change_visibility"
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas79 | "delete";
80
81/** The permission table: the least role for each capability. */
82export const CAPABILITIES = [
83 { capability: "read", role: "read", about: "See code, issues and pull requests; clone and fetch" },
84 { capability: "participate", role: "read", about: "Open issues and pull requests, and comment" },
Merge main (membership, two-factor, GitHub repo roles) into tokens85 { capability: "triage", role: "triage", about: "Apply labels and milestones; assign, close and reopen issues and pull requests" },
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas86 { capability: "push", role: "write", about: "Push to branches that are not protected" },
87 { capability: "merge", role: "write", about: "Merge pull requests and use the merge queue" },
Merge main (membership, two-factor, GitHub repo roles) into tokens88 { capability: "manage_labels", role: "write", about: "Create, edit and delete labels and milestones" },
89 { capability: "security_alerts", role: "write", about: "See and dismiss security alerts" },
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas90 { capability: "run", role: "write", about: "Assign agents and start runs, plans and workflows" },
91 { capability: "manage_settings", role: "maintain", about: "Change the description, topics, and pull request and agent settings" },
Merge main (membership, two-factor, GitHub repo roles) into tokens92 { capability: "manage_protection", role: "admin", about: "Change branch protection, rulesets and guardrails" },
93 { capability: "manage_security", role: "admin", about: "Change security settings, custom patterns and bypass reviews" },
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas94 { capability: "manage_integrations", role: "admin", about: "Manage webhooks, secrets, variables, deployments and domains" },
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca95 { capability: "manage_access", role: "admin", about: "Manage who has access, invitations and deploy keys" },
Merge main (membership, two-factor, GitHub repo roles) into tokens96 { capability: "administer", role: "admin", about: "Rename, archive and change the default branch" },
97 { capability: "change_visibility", role: "admin", about: "Change visibility (owners only, unless member privileges allow admins)" },
98 { capability: "delete", role: "admin", about: "Transfer or delete the repository (owners only, unless member privileges allow admins)" },
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas99] as const satisfies readonly { capability: Capability; role: RepoRole; about: string }[];
100
Merge main (membership, two-factor, GitHub repo roles) into tokens101/**
102 * Capabilities that also need an owner of the repository's workspace,
103 * unless its member privileges let members with the Admin role do them.
104 */
105export const OWNER_ONLY = ["change_visibility", "delete"] as const satisfies readonly Capability[];
106
107/** What a security manager may do on every repository of their workspace. */
108export const SECURITY_MANAGER = ["read", "participate", "security_alerts", "manage_security"] as const satisfies readonly Capability[];
109
110/** Whether an owner-only capability is left to owners by these member privileges. */
111export function ownerOnly(capability: Capability, privileges: MemberPrivileges | null | undefined): boolean {
112 const p = { ...DEFAULT_MEMBER_PRIVILEGES, ...(privileges ?? {}) };
113 if (capability === "change_visibility") return !p.members_can_change_repo_visibility;
114 if (capability === "delete") return !p.members_can_delete_repositories;
115 return false;
116}
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas117
118/** A person's role on one repository, given directly. */
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar119export type RepoGrant = {
120 repo_id: string;
121 workspace: string;
122 role: RepoRole;
123 /** The team it comes through, when it is a team's grant. */
124 team?: string;
125};
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas126
127/** What `permission` needs to know about a repository. */
128export type RepoRef = { id: string; namespace: string; isPrivate: boolean };
129
130const rank = (role: RepoRole): number => REPO_ROLES.indexOf(role);
131
132/** The higher of two roles; null is lower than any. */
133export function maxRole(a: RepoRole | null | undefined, b: RepoRole | null | undefined): RepoRole | null {
134 if (!a) return b ?? null;
135 if (!b) return a;
136 return rank(a) >= rank(b) ? a : b;
137}
138
139export function leastRole(capability: Capability): RepoRole {
140 return CAPABILITIES.find((row) => row.capability === capability)?.role ?? "admin";
141}
142
143/** Whether `role` has `capability`, going by the table alone. */
144export function allows(role: RepoRole | null | undefined, capability: Capability): boolean {
145 return role != null && rank(role) >= rank(leastRole(capability));
146}
147
148export function baseRole(base: BasePermission | null | undefined): RepoRole | null {
149 const value = base ?? DEFAULT_BASE_PERMISSION;
150 return value === "none" ? null : value;
151}
152
153function membershipRole(user: User, membership: Membership): RepoRole | null {
TS access mirrors the workspace token cap and fine-grained reach154 // A workspace's own token has Write, or Admin when an owner gave it that;
155 // g1t, and a service acting as the workspace, do what an owner can.
156 if (user.kind === "workspace" && user.token) return user.token.admin ? "admin" : "write";
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily157 if (user.kind === "workspace" || user.kind === "system") return "admin";
Merge main (membership, two-factor, GitHub repo roles) into tokens158 if (membership.role === "owner") return "admin";
159 const base = baseRole(membership.base_permission);
160 // A security manager reads every repository.
161 return membership.org_roles?.includes("security_manager") ? maxRole(base, "read") : base;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas162}
163
164/** The user's role on the repository, not counting that it may be public. */
165export function granted(user: User, repo: RepoRef): RepoRole | null {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers166 // A token outside the workspace it is made for, or its selection: no
167 // role. (The wire key is `fine_grained`, kept from before tokens were
168 // one kind.)
TS access mirrors the workspace token cap and fine-grained reach169 const reach = user.token?.fine_grained;
170 if (reach) {
171 const inside =
172 !!reach.workspace &&
173 reach.workspace.toLowerCase() === repo.namespace.toLowerCase() &&
174 (reach.repositories === "all" || (reach.repositories === "selected" && (reach.repo_ids ?? []).includes(repo.id)));
175 if (!inside) return null;
176 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas177 const namespace = repo.namespace.toLowerCase();
178 const membership = user.workspaces?.find((m) => m.slug.toLowerCase() === namespace);
179 let role = membership ? membershipRole(user, membership) : null;
180 for (const grant of user.grants ?? []) {
181 if (grant.repo_id === repo.id) role = maxRole(role, grant.role);
182 }
183 return role;
184}
185
186/** The viewer's effective role on a repository; null means they may not see it. */
187export function permission(viewer: User | null | undefined, repo: RepoRef): RepoRole | null {
188 const role = viewer ? granted(viewer, repo) : null;
189 return repo.isPrivate ? role : maxRole(role, "read");
190}
191
192/** Whether the viewer may do `capability` in the repository. */
193export function can(viewer: User | null | undefined, repo: RepoRef, capability: Capability): boolean {
Merge main (membership, two-factor, GitHub repo roles) into tokens194 const namespace = repo.namespace.toLowerCase();
195 const membership = viewer?.workspaces?.find((m) => m.slug.toLowerCase() === namespace);
196 if (!allows(permission(viewer, repo), capability)) {
197 return (
198 (SECURITY_MANAGER as readonly Capability[]).includes(capability) &&
199 (viewer?.kind ?? "user") === "user" &&
200 !!membership?.org_roles?.includes("security_manager")
201 );
202 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas203 if ((OWNER_ONLY as readonly Capability[]).includes(capability)) {
Merge main (membership, two-factor, GitHub repo roles) into tokens204 if (!membership) return false;
205 return membership.role === "owner" || !ownerOnly(capability, membership.privileges);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas206 }
207 return true;
208}
209
210/** Every capability, true or false, for one viewer and repository: what pages pass to their components. */
211export type Abilities = Record<Capability, boolean>;
212
213export function abilities(viewer: User | null | undefined, repo: RepoRef): Abilities {
214 return Object.fromEntries(CAPABILITIES.map((row) => [row.capability, can(viewer, repo, row.capability)])) as Abilities;
215}
216
217/** The sentence shown beside something the viewer cannot use. */
218export function needs(capability: Capability): string {
Merge main (membership, two-factor, GitHub repo roles) into tokens219 if ((OWNER_ONLY as readonly Capability[]).includes(capability))
220 return "Only an owner of the workspace can do this, unless its member privileges let repository admins.";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas221 return `Needs the ${REPO_ROLE_LABELS[leastRole(capability)]} role or higher.`;
222}
223
224/** Whether the user belongs to the workspace or has a role on one of its repositories. */
225export function hasAccessIn(user: User | null | undefined, namespace: string): boolean {
226 const slug = namespace.toLowerCase();
227 return !!user && (!!user.workspaces?.some((m) => m.slug.toLowerCase() === slug) || !!user.grants?.some((g) => g.workspace.toLowerCase() === slug));
228}
229
230/** The workspaces where the user has repositories shared with them without being a member. */
231export function sharedWorkspaces(user: User | null | undefined): string[] {
232 if (!user) return [];
233 const member = new Set((user.workspaces ?? []).map((m) => m.slug));
234 return [...new Set((user.grants ?? []).map((g) => g.workspace).filter((slug) => !member.has(slug)))];
235}
236
237// --- Who has access ----------------------------------------------------------
238
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar239export type AccessSource = "owner" | "base" | "direct" | "team";
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas240
241export type Collaborator = {
242 username: string;
243 name: string | null;
244 avatar: string | null;
245 role: RepoRole;
246 source: AccessSource;
247 direct: RepoRole | null;
248 /** Null for an outside collaborator. */
249 workspace_role: Role | null;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar250 /** The highest role a team gives them here, and that team's slug. */
251 team_role?: RepoRole | null;
252 team?: string | null;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas253};
254
255export type RepoInvitationStatus = "pending" | "accepted" | "declined" | "revoked" | "expired";
256
257export type RepoInvitation = {
258 id: string;
259 /** `workspace/name`. */
260 repo: string;
261 repo_id: string;
262 invitee: string | null;
263 email: string | null;
264 role: RepoRole;
265 invited_by: string | null;
266 /** The inviter's avatar hash, served at `/avatars/<avatar>`; null for the generated letter avatar. */
267 inviter_avatar?: string | null;
268 status: RepoInvitationStatus;
269 created_at: string;
270 expires_at: string;
271};
272
273export type RepoAccess = {
274 repo: string;
275 base_permission: BasePermission;
276 people: Collaborator[];
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar277 /** The workspace's teams given a role on it. */
278 teams?: RepoTeam[];
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas279 invitations: RepoInvitation[];
280 viewer_role: RepoRole | null;
281 can_manage: boolean;
282};
283
284export type Added =
285 | { result: "granted"; collaborator: Collaborator }
286 | { result: "invited"; invitation: RepoInvitation };
287
288export type PermissionInfo = {
289 username: string;
290 role: RepoRole | null;
291 source: AccessSource | null;
292 capabilities: Capability[];
293};
294
295export type OutsideCollaborator = {
296 username: string;
297 name: string | null;
298 avatar: string | null;
299 repos: { repo: string; role: RepoRole }[];
300};
301
302/** Identity's access methods, by repository path. */
303export interface AccessClient {
304 repoAccess(owner: string, name: string, viewer: User | null): Promise<Result<RepoAccess>>;
305 addCollaborator(actor: User, owner: string, name: string, invitee: string, role: RepoRole): Promise<Result<Added>>;
306 setCollaboratorRole(actor: User, owner: string, name: string, username: string, role: RepoRole): Promise<Result<Collaborator>>;
307 removeCollaborator(actor: User, owner: string, name: string, username: string): Promise<Result<boolean>>;
308 collaboratorPermission(viewer: User | null, owner: string, name: string, username: string): Promise<Result<PermissionInfo>>;
309 myRepoInvitations(user: User): Promise<RepoInvitation[]>;
310 respondRepoInvitation(user: User, id: string, accept: boolean): Promise<Result<RepoInvitation>>;
311 revokeRepoInvitation(actor: User, owner: string, name: string, id: string): Promise<Result<RepoInvitation>>;
312 setBasePermission(actor: User, slug: string, base: BasePermission): Promise<Result<BasePermission>>;
313 outsideCollaborators(viewer: User | null, slug: string): Promise<Result<OutsideCollaborator[]>>;
314}

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