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.

838 linesCodeBlameRaw
1/**
2 * Scopes: what an access token may do on its owner's behalf. Mirrors
3 * `crates/contracts/src/scopes.rs`, which is the source of truth; a Rust
4 * test keeps the tables here the same.
5 *
6 * A token's permissions are its scopes read per resource: each resource
7 * at none or one level, such as work: write. What a request may do is
8 * the intersection of the owner's role, the token's reach (the workspace
9 * it is made for, and its repositories) and its permissions.
10 */
11
12export type ScopeResource =
13 | "repo"
14 | "code"
15 | "security"
16 | "packages"
17 | "work"
18 | "pull_requests"
19 | "agents"
20 | "workflows"
21 | "workflow_files"
22 | "checks"
23 | "deployments"
24 | "observability"
25 | "memory"
26 | "account"
27 | "notifications"
28 | "chat"
29 | "workspace"
30 | "billing"
31 | "access"
32 | "webhooks"
33 | "secrets"
34 | "runners"
35 | "models"
36 | "artifacts";
37
38export type ScopeLevel = "read" | "create" | "write" | "run" | "delete" | "admin";
39
40/** Every scope, grouped by resource, least first. */
41export const SCOPES = [
42 { scope: "repo:read", description: "See repositories, their settings, timelines, releases, languages, contributors and security alerts, and search" },
43 { scope: "repo:create", description: "Create repositories, and nothing that changes or deletes one" },
44 { scope: "repo:write", description: "Create repositories, create, rename and delete branches, change how pull requests merge and publish releases" },
45 {
46 scope: "repo:admin",
47 description: "Rename, archive, transfer, delete or change who can see a repository, change its rulesets, and dismiss security alerts",
48 },
49 { scope: "code:read", description: "Clone and fetch private repositories with git" },
50 { scope: "code:write", description: "Push commits with git" },
51 {
52 scope: "security:read",
53 description: "See secret scanning, code scanning and vulnerability alerts, custom patterns, the dependency graph and SBOM, and security settings",
54 },
55 {
56 scope: "security:write",
57 description: "Dismiss and reopen alerts, bypass push protection, review bypass requests, manage custom patterns, upload SARIF and change security settings",
58 },
59 { scope: "packages:read", description: "Pull container images and install private packages" },
60 { scope: "packages:write", description: "Push container images and publish packages" },
61 { scope: "packages:delete", description: "Delete and restore packages and their versions" },
62 { scope: "work:read", description: "Read work items, their comments, labels, milestones and claims" },
63 { scope: "work:write", description: "Open, edit, link, close and comment on work items, claim them, and manage labels and milestones" },
64 { scope: "pull_requests:read", description: "Read pull requests, their changes, sessions and merge queues" },
65 { scope: "pull_requests:write", description: "Open, review, comment on, close and merge pull requests" },
66 { scope: "workflows:read", description: "Read workflows, runs and logs" },
67 { scope: "workflows:write", description: "Run, cancel, rerun and turn workflows on or off" },
68 { scope: "workflow_files:write", description: "Add, change and delete workflow files under .g1t/workflows and .github/workflows, with git or the API" },
69 { scope: "checks:read", description: "Read commits' statuses, check runs, check suites and annotations" },
70 { scope: "checks:write", description: "Report statuses and check runs on commits, and ask for checks to run again" },
71 { scope: "deployments:read", description: "See deployments, their statuses and environments" },
72 { scope: "deployments:write", description: "Report deployments and their statuses, from any CI" },
73 {
74 scope: "observability:read",
75 description: "See projects' requests, logs, traces, errors and metrics, and their observability settings, ingest tokens and exports",
76 },
77 { scope: "observability:write", description: "Resolve and ignore errors, and change projects' observability settings, ingest tokens and exports" },
78 { scope: "account:read", description: "Read your email addresses, invites, invitations, pinned projects and stars" },
79 { scope: "account:write", description: "Change your email addresses, make invites, answer invitations, pin projects and star repositories" },
80 { scope: "notifications:read", description: "See your inbox, its threads, and what you subscribe to and watch" },
81 { scope: "notifications:write", description: "Mark notifications read, done, saved or snoozed, subscribe to threads and watch repositories" },
82 { scope: "chat:read", description: "Read the conversations you are in: channels, direct messages, threads and reactions, live" },
83 { scope: "chat:write", description: "Send, edit and delete your messages, react, mark conversations read, and answer cards in chat" },
84 { scope: "workspace:read", description: "Read workspace settings, invites, integrations, AI providers, teams and rulesets" },
85 {
86 scope: "workspace:admin",
87 description:
88 "Create and delete workspaces, invite members, connect integrations, add AI providers and choose what Ask answers with, create, change and delete teams, and change the workspace's rulesets",
89 },
90 { scope: "billing:read", description: "See a workspace's usage, budget and invoices" },
91 { scope: "billing:write", description: "Change a workspace's budget" },
92 { scope: "access:read", description: "See who has access to repositories" },
93 { scope: "access:admin", description: "Give and take away access to repositories, a team's included" },
94 { scope: "webhooks:read", description: "See webhooks and their deliveries" },
95 { scope: "webhooks:admin", description: "Create, change and delete webhooks" },
96 { scope: "secrets:read", description: "List secrets (never their values) and read variables" },
97 { scope: "secrets:admin", description: "Set and delete secrets and variables" },
98 { scope: "runners:read", description: "See self-hosted runners and their groups" },
99 { scope: "runners:admin", description: "Register and remove self-hosted runners, change their groups and settings" },
100 { scope: "artifacts:read", description: "List, read and search artifacts you can see, their versions, and the numbers their dashboards show" },
101 { scope: "artifacts:write", description: "Create, rename, move, edit, trash and restore artifacts, and propose changes to them" },
102 { scope: "artifacts:admin", description: "Share artifacts, change who can open them, and delete them for good" },
103] as const;
104
105/**
106 * Scopes of resources being built that tokens are not offered yet (Rust:
107 * `Resource::offered`). They parse in Rust and are typed here, but no
108 * preset, full access, OAuth request or token form hands them out, and no
109 * operation needs them. When one ships, its rows move to the end of
110 * `SCOPES` and `SCOPE_RESOURCES`. Empty now: Artifacts shipped last.
111 */
112export const UPCOMING_SCOPES: readonly { scope: Scope; description: string }[] = [] as const;
113
114/**
115 * Scopes of what g1t no longer has (Rust: `Resource::retired`): `agents`
116 * (g1t's own agents), `memory` (project and workspace memory) and `models`
117 * (its model proxy and AI Gateway). Tokens
118 * and OAuth grants that stored them still read, but they are never offered
119 * or granted and no operation needs them: they give nothing. `isScope` and
120 * `parseScopes` leave them out.
121 */
122export const RETIRED_SCOPES = [
123 { scope: "agents:run", description: "Retired: put g1t's own agents to work. Grants nothing now" },
124 { scope: "memory:read", description: "Retired: recall project and workspace memory. Grants nothing now" },
125 { scope: "memory:write", description: "Retired: save project and workspace memory. Grants nothing now" },
126 { scope: "models:read", description: "Retired: see the AI Gateway's requests. Grants nothing now" },
127 { scope: "models:write", description: "Retired: send requests through the AI Gateway. Grants nothing now" },
128] as const;
129
130export type Scope = (typeof SCOPES)[number]["scope"];
131
132/** Where a resource sits on the token form; only a person's token may hold account ones. */
133export type ResourceGroup = "repository" | "workspace" | "account";
134
135/** Resources in the order settings show them, with their names for people and their group. */
136export const SCOPE_RESOURCES: { resource: ScopeResource; label: string; group: ResourceGroup }[] = [
137 { resource: "repo", label: "Repositories", group: "repository" },
138 { resource: "code", label: "Code", group: "repository" },
139 { resource: "security", label: "Security", group: "repository" },
140 { resource: "packages", label: "Packages", group: "repository" },
141 { resource: "work", label: "Work", group: "repository" },
142 { resource: "pull_requests", label: "Pull requests", group: "repository" },
143 { resource: "workflows", label: "Workflows", group: "repository" },
144 { resource: "workflow_files", label: "Workflow files", group: "repository" },
145 { resource: "checks", label: "Checks and statuses", group: "repository" },
146 { resource: "deployments", label: "Deployments", group: "repository" },
147 { resource: "observability", label: "Observability", group: "repository" },
148 { resource: "account", label: "Your account", group: "account" },
149 { resource: "notifications", label: "Notifications", group: "account" },
150 { resource: "chat", label: "Chat", group: "account" },
151 { resource: "workspace", label: "Workspaces", group: "workspace" },
152 { resource: "billing", label: "Billing", group: "workspace" },
153 { resource: "access", label: "Who has access", group: "repository" },
154 { resource: "webhooks", label: "Webhooks", group: "repository" },
155 { resource: "secrets", label: "Secrets and variables", group: "repository" },
156 { resource: "runners", label: "Self-hosted runners", group: "workspace" },
157 { resource: "artifacts", label: "Artifacts", group: "workspace" },
158];
159
160/** Resources not offered yet, as `UPCOMING_SCOPES`: settings never show them. */
161export const UPCOMING_RESOURCES: { resource: ScopeResource; label: string; group: ResourceGroup }[] = [];
162
163/** Resources of what g1t no longer has, as `RETIRED_SCOPES`: settings never show them. */
164export const RETIRED_RESOURCES: { resource: ScopeResource; label: string; group: ResourceGroup }[] = [
165 { resource: "agents", label: "g1t agents", group: "repository" },
166 { resource: "memory", label: "Memory", group: "repository" },
167 { resource: "models", label: "AI Gateway", group: "workspace" },
168];
169
170const LEVEL_ORDER: Record<ScopeLevel, number> = { read: 0, create: 1, write: 2, run: 3, delete: 4, admin: 5 };
171
172export function scopeResource(scope: Scope): ScopeResource {
173 return scope.split(":")[0] as ScopeResource;
174}
175
176export function scopeLevel(scope: Scope): ScopeLevel {
177 return scope.split(":")[1] as ScopeLevel;
178}
179
180export function isScope(text: string): text is Scope {
181 return SCOPES.some((row) => row.scope === text);
182}
183
184/** Changes that are hard to undo, or decide who can reach what. */
185export function isDangerous(scope: Scope): boolean {
186 const level = scopeLevel(scope);
187 return level === "admin" || level === "delete";
188}
189
190export function describeScope(scope: Scope): string {
191 return [...SCOPES, ...UPCOMING_SCOPES].find((row) => row.scope === scope)?.description ?? scope;
192}
193
194/** Whether holding `held` gives `needed`: the same resource, at its level or lower. */
195export function scopeIncludes(held: Scope, needed: Scope): boolean {
196 return scopeResource(held) === scopeResource(needed) && LEVEL_ORDER[scopeLevel(held)] >= LEVEL_ORDER[scopeLevel(needed)];
197}
198
199/**
200 * Whether a token holding `scopes` may do what `needed` gates (Rust:
201 * `TokenAccess::allows`). Null or absent scopes are full access, which
202 * holds every scope; unknown names give nothing.
203 */
204export function tokenAllows(scopes: readonly string[] | null | undefined, needed: Scope): boolean {
205 if (scopes === null || scopes === undefined) return true;
206 return scopes.some((held) => isScope(held) && scopeIncludes(held, needed));
207}
208
209/** The levels a resource has, least first. */
210export function levelsOf(resource: ScopeResource): ScopeLevel[] {
211 return SCOPES.filter((row) => scopeResource(row.scope) === resource).map((row) => scopeLevel(row.scope));
212}
213
214/** The token form's groups, in order. */
215export const RESOURCE_GROUPS: { group: ResourceGroup; label: string; about: string }[] = [
216 { group: "repository", label: "Repository permissions", about: "What it may do in the repositories it reaches." },
217 { group: "workspace", label: "Workspace permissions", about: "What it may do with the workspaces it reaches themselves." },
218 { group: "account", label: "Account permissions", about: "What it may do with your own account. Personal tokens only." },
219];
220
221/** A token's permissions: each resource held, at its highest level; left out is none. */
222export type Permissions = Partial<Record<ScopeResource, ScopeLevel>>;
223
224/** Scopes as permissions. Null scopes (full access) are every resource at its highest. */
225export function permissionsOf(scopes: readonly string[] | null): Permissions {
226 const permissions: Permissions = {};
227 const held = scopes === null ? SCOPES.map((row) => row.scope) : parseScopes(scopes.join(" "));
228 for (const scope of held) {
229 const resource = scopeResource(scope);
230 const now = permissions[resource];
231 if (!now || LEVEL_ORDER[scopeLevel(scope)] > LEVEL_ORDER[now]) permissions[resource] = scopeLevel(scope);
232 }
233 return permissions;
234}
235
236/** Permissions as the scopes a token stores: the highest of each resource, in table order. */
237export function scopesOfPermissions(permissions: Permissions): Scope[] {
238 const wanted = new Set(
239 Object.entries(permissions)
240 .filter(([, level]) => level)
241 .map(([resource, level]) => `${resource}:${level}`),
242 );
243 return SCOPES.map((row) => row.scope).filter((scope) => wanted.has(scope));
244}
245
246/** The longest a token with an expiry may last, in days. */
247export const MAX_TOKEN_LIFETIME_DAYS = 366;
248
249/** The most repositories a token may select. */
250export const MAX_SELECTED_REPOSITORIES = 50;
251
252/** Scopes from text separated by spaces or commas, in table order; unknown ones are left out. */
253export function parseScopes(text: string): Scope[] {
254 const given = new Set(text.split(/[\s,]+/).map((part) => part.trim().toLowerCase()));
255 return SCOPES.map((row) => row.scope).filter((scope) => given.has(scope));
256}
257
258/** What a token stores for full access. */
259export const FULL_ACCESS = "*";
260
261export type PresetId = "read_only" | "agent" | "ci" | "full";
262
263/** Starting points for choosing scopes. `*` is full access. No preset gives chat: an application gets it only by asking for it. */
264export const PRESET_SCOPES = {
265 read_only: [
266 "repo:read",
267 "code:read",
268 "security:read",
269 "packages:read",
270 "work:read",
271 "pull_requests:read",
272 "workflows:read",
273 "checks:read",
274 "deployments:read",
275 "observability:read",
276 "account:read",
277 "notifications:read",
278 "workspace:read",
279 "billing:read",
280 "access:read",
281 "webhooks:read",
282 "secrets:read",
283 "runners:read",
284 "artifacts:read",
285 ] as const,
286 agent: [
287 "repo:read",
288 "repo:create",
289 "code:read",
290 "code:write",
291 "security:read",
292 "packages:read",
293 "work:read",
294 "work:write",
295 "pull_requests:read",
296 "pull_requests:write",
297 "workflows:read",
298 "checks:read",
299 "deployments:read",
300 "observability:read",
301 "account:read",
302 "notifications:read",
303 "notifications:write",
304 "workspace:read",
305 "billing:read",
306 "access:read",
307 "webhooks:read",
308 "secrets:read",
309 "artifacts:read",
310 ] as const,
311 ci: [
312 "repo:read",
313 "code:read",
314 "code:write",
315 "packages:read",
316 "packages:write",
317 "workflows:read",
318 "workflows:write",
319 "checks:read",
320 "checks:write",
321 "deployments:read",
322 "deployments:write",
323 ] as const,
324 full: ["*"] as const,
325};
326
327export const PRESETS: { id: PresetId; label: string; description: string }[] = [
328 { id: "read_only", label: "Read only", description: "Read everything you can read; change nothing." },
329 { id: "agent", label: "Agent", description: "Read everything, work on work items and pull requests, push code, and create repositories." },
330 { id: "ci", label: "CI", description: "Clone and push code, push and pull packages, run workflows, and report checks and deployments." },
331 { id: "full", label: "Full access", description: "Everything you can do, including deleting repositories and changing who has access." },
332];
333
334/** The scopes of a preset, or null for full access. */
335export function presetScopes(id: PresetId): Scope[] | null {
336 if (id === "full") return null;
337 return [...PRESET_SCOPES[id]] as Scope[];
338}
339
340/** What an OAuth client gets when it asks for nothing in particular. */
341export const OAUTH_DEFAULT_SCOPES: Scope[] = [...PRESET_SCOPES.agent];
342
343/** The operation each scope gates, by the API's operation names. */
344export const OPERATION_SCOPES = [
345 ["list_emails", "account:read"],
346 ["add_email", "account:write"],
347 ["confirm_email", "account:write"],
348 ["remove_email", "account:write"],
349 ["update_email_settings", "account:write"],
350 ["list_invites", "account:read"],
351 ["create_invite", "account:write"],
352 ["revoke_invite", "account:write"],
353 ["resend_invite", "account:write"],
354 ["list_invitations", "account:read"],
355 ["accept_invitation", "account:write"],
356 ["decline_invitation", "account:write"],
357 ["list_my_repo_invitations", "account:read"],
358 ["accept_repo_invitation", "account:write"],
359 ["decline_repo_invitation", "account:write"],
360 // Your pinned projects: a preference of your account.
361 ["list_pinned_projects", "account:read"],
362 ["pin_project", "account:write"],
363 // Your stars: a preference of your account.
364 ["list_starred", "account:read"],
365 ["check_starred", "account:read"],
366 ["star_repo", "account:write"],
367 ["unstar_repo", "account:write"],
368 ["unpin_project", "account:write"],
369 ["reorder_pinned_projects", "account:write"],
370 // Your inbox: notifications, subscriptions and watching.
371 ["list_notifications", "notifications:read"],
372 ["get_notification_thread", "notifications:read"],
373 ["get_thread_subscription", "notifications:read"],
374 ["get_repo_subscription", "notifications:read"],
375 ["list_watched_repos", "notifications:read"],
376 ["mark_notifications_read", "notifications:write"],
377 ["mark_thread_read", "notifications:write"],
378 ["mark_thread_done", "notifications:write"],
379 ["save_thread", "notifications:write"],
380 ["snooze_thread", "notifications:write"],
381 ["set_thread_subscription", "notifications:write"],
382 ["delete_thread_subscription", "notifications:write"],
383 ["set_repo_subscription", "notifications:write"],
384 ["delete_repo_subscription", "notifications:write"],
385 ["create_workspace", "workspace:admin"],
386 ["delete_workspace", "workspace:admin"],
387 ["get_workspace", "workspace:read"],
388 ["update_workspace", "workspace:admin"],
389 ["list_members", "workspace:read"],
390 ["update_member", "workspace:admin"],
391 ["remove_member", "workspace:admin"],
392 ["transfer_ownership", "workspace:admin"],
393 ["leave_workspace", "account:write"],
394 ["list_workspace_invites", "workspace:read"],
395 ["invite_member", "workspace:admin"],
396 ["revoke_workspace_invite", "workspace:admin"],
397 ["list_integrations", "workspace:read"],
398 ["connect_integration", "workspace:admin"],
399 ["update_integration", "workspace:admin"],
400 ["disconnect_integration", "workspace:admin"],
401 ["test_integration", "workspace:admin"],
402 ["list_ai_providers", "workspace:read"],
403 ["add_ai_provider", "workspace:admin"],
404 ["update_ai_provider", "workspace:admin"],
405 ["remove_ai_provider", "workspace:admin"],
406 ["test_ai_provider", "workspace:admin"],
407 ["get_ask_settings", "workspace:read"],
408 ["set_ask_settings", "workspace:admin"],
409 ["list_teams", "workspace:read"],
410 ["get_team", "workspace:read"],
411 ["list_team_members", "workspace:read"],
412 ["list_child_teams", "workspace:read"],
413 ["list_team_repos", "workspace:read"],
414 ["list_user_teams", "workspace:read"],
415 ["create_team", "workspace:admin"],
416 ["list_workspace_rulesets", "workspace:read"],
417 ["get_workspace_ruleset", "workspace:read"],
418 ["list_workspace_rule_evaluations", "workspace:read"],
419 ["create_workspace_ruleset", "workspace:admin"],
420 ["update_workspace_ruleset", "workspace:admin"],
421 ["delete_workspace_ruleset", "workspace:admin"],
422 ["update_team", "workspace:admin"],
423 ["delete_team", "workspace:admin"],
424 ["set_team_member", "workspace:admin"],
425 ["remove_team_member", "workspace:admin"],
426 ["set_team_review_assignment", "workspace:admin"],
427 // A workspace's billing: usage, budget and invoices.
428 ["get_usage", "billing:read"],
429 ["get_budget", "billing:read"],
430 ["list_invoices", "billing:read"],
431 ["get_billing_details", "billing:read"],
432 ["set_budget", "billing:write"],
433 ["list_repos", "repo:read"],
434 ["get_repo", "repo:read"],
435 ["list_forks", "repo:read"],
436 ["list_repo_commit_comments", "repo:read"],
437 ["list_commit_comments", "repo:read"],
438 // A file or directory of a repository, as clone and fetch would give it.
439 ["get_contents", "code:read"],
440 ["list_commits", "code:read"],
441 ["download_tarball_archive", "code:read"],
442 ["download_zipball_archive", "code:read"],
443 ["why", "code:read"],
444 // Projects follow their repositories.
445 ["list_projects", "repo:read"],
446 ["get_project", "repo:read"],
447 ["search", "repo:read"],
448 ["list_events", "repo:read"],
449 // A workspace's activity: the events of its repositories the caller can read.
450 ["get_workspace_activity", "repo:read"],
451 // What the default branch says about a repository, who starred it, and
452 // its releases.
453 ["get_languages", "repo:read"],
454 ["list_contributors", "repo:read"],
455 ["get_license", "repo:read"],
456 ["list_stargazers", "repo:read"],
457 ["list_releases", "repo:read"],
458 ["get_latest_release", "repo:read"],
459 ["get_release_by_tag", "repo:read"],
460 ["get_release", "repo:read"],
461 ["create_release", "repo:write"],
462 ["update_release", "repo:write"],
463 ["delete_release", "repo:write"],
464 ["list_labels", "work:read"],
465 ["list_milestones", "work:read"],
466 ["get_milestone", "work:read"],
467 ["create_label", "work:write"],
468 ["update_label", "work:write"],
469 ["delete_label", "work:write"],
470 ["add_default_labels", "work:write"],
471 ["create_milestone", "work:write"],
472 ["update_milestone", "work:write"],
473 ["delete_milestone", "work:write"],
474 ["get_repo_settings", "repo:read"],
475 ["list_check_names", "repo:read"],
476 ["list_deleted_repos", "repo:read"],
477 ["list_security_alerts", "repo:read"],
478 ["get_codeowners_errors", "repo:read"],
479 ["create_repo", "repo:create"],
480 ["update_repo", "repo:write"],
481 ["update_project", "repo:write"],
482 ["update_repo_settings", "repo:write"],
483 ["list_repo_rulesets", "repo:read"],
484 ["get_repo_ruleset", "repo:read"],
485 ["get_branch_rules", "repo:read"],
486 ["list_rule_evaluations", "repo:read"],
487 ["create_repo_ruleset", "repo:admin"],
488 ["update_repo_ruleset", "repo:admin"],
489 ["delete_repo_ruleset", "repo:admin"],
490 ["rename_branch", "repo:write"],
491 ["create_branch", "repo:write"],
492 ["delete_branch", "repo:write"],
493 ["rename_repo", "repo:admin"],
494 ["transfer_repo", "repo:admin"],
495 ["fork_repo", "repo:write"],
496 ["sync_fork", "repo:write"],
497 ["detach_fork", "repo:admin"],
498 ["archive_repo", "repo:admin"],
499 ["unarchive_repo", "repo:admin"],
500 ["set_repo_visibility", "repo:admin"],
501 ["delete_repo", "repo:admin"],
502 ["restore_repo", "repo:admin"],
503 ["purge_repo", "repo:admin"],
504 ["dismiss_security_alert", "repo:admin"],
505 ["reopen_security_alert", "repo:admin"],
506 // The security suite.
507 ["list_secret_scanning_alerts", "security:read"],
508 ["get_secret_scanning_alert", "security:read"],
509 ["list_secret_scanning_locations", "security:read"],
510 ["list_bypass_requests", "security:read"],
511 ["list_custom_patterns", "security:read"],
512 ["list_code_scanning_alerts", "security:read"],
513 ["get_code_scanning_alert", "security:read"],
514 ["list_code_scanning_analyses", "security:read"],
515 ["get_sarif_upload", "security:read"],
516 ["list_vulnerability_alerts", "security:read"],
517 ["get_vulnerability_alert", "security:read"],
518 ["get_dependency_graph", "security:read"],
519 ["get_sbom", "security:read"],
520 ["compare_dependencies", "security:read"],
521 ["rerun_dependency_review", "security:write"],
522 ["get_security_settings", "security:read"],
523 ["get_workspace_security_settings", "security:read"],
524 ["get_security_overview", "security:read"],
525 ["list_security_advisories", "security:read"],
526 ["get_security_advisory", "security:read"],
527 ["list_vulnerability_reports", "security:read"],
528 ["update_secret_scanning_alert", "security:write"],
529 ["bypass_push_protection", "security:write"],
530 ["check_secret_validity", "security:write"],
531 ["review_bypass_request", "security:write"],
532 ["create_custom_pattern", "security:write"],
533 ["update_custom_pattern", "security:write"],
534 ["delete_custom_pattern", "security:write"],
535 ["dry_run_custom_pattern", "security:write"],
536 ["update_code_scanning_alert", "security:write"],
537 ["upload_sarif", "security:write"],
538 ["update_vulnerability_alert", "security:write"],
539 ["update_security_settings", "security:write"],
540 ["update_workspace_security_settings", "security:write"],
541 ["create_security_advisory", "security:write"],
542 ["update_security_advisory", "security:write"],
543 ["set_security_advisory_state", "security:write"],
544 ["report_vulnerability", "security:write"],
545 ["review_vulnerability_report", "security:write"],
546 ["list_work_items", "work:read"],
547 ["list_repo_work_items", "work:read"],
548 ["get_work_item", "work:read"],
549 ["list_work_item_links", "work:read"],
550 ["resolve_ref", "work:read"],
551 ["open_work_item", "work:write"],
552 ["update_work_item", "work:write"],
553 ["close_work_item", "work:write"],
554 ["reopen_work_item", "work:write"],
555 ["link_work_item", "work:write"],
556 ["unlink_work_item", "work:write"],
557 ["list_work_item_labels", "work:read"],
558 ["add_work_item_labels", "work:write"],
559 ["set_work_item_labels", "work:write"],
560 ["remove_work_item_labels", "work:write"],
561 ["list_work_item_decisions", "work:read"],
562 ["add_work_item_comment", "work:write"],
563 ["edit_work_item_comment", "work:write"],
564 ["delete_work_item_comment", "work:write"],
565 ["react_to_work_item", "work:write"],
566 ["upload_attachment", "work:write"],
567 ["upload_work_item_attachment", "work:write"],
568 ["create_commit_comment", "work:write"],
569 ["update_commit_comment", "work:write"],
570 ["delete_commit_comment", "work:write"],
571 ["get_context", "work:read"],
572 ["import_work_items", "work:write"],
573 ["list_claims", "work:read"],
574 ["get_work", "work:read"],
575 ["claim_work_item", "work:write"],
576 ["release_claim", "work:write"],
577 ["hand_off_work_item", "work:write"],
578 ["list_pull_requests", "pull_requests:read"],
579 ["get_pull_request", "pull_requests:read"],
580 ["get_pull_request_changes", "pull_requests:read"],
581 ["read_session", "pull_requests:read"],
582 ["get_merge_queue", "pull_requests:read"],
583 ["get_pull_request_merge_options", "pull_requests:read"],
584 ["list_decisions", "pull_requests:read"],
585 ["create_pull_request", "pull_requests:write"],
586 ["update_pull_request", "pull_requests:write"],
587 ["record_session", "pull_requests:write"],
588 ["record_decision", "pull_requests:write"],
589 ["mark_pull_request_ready", "pull_requests:write"],
590 ["close_pull_request", "pull_requests:write"],
591 ["reopen_pull_request", "pull_requests:write"],
592 ["convert_pull_request_to_draft", "pull_requests:write"],
593 ["review_pull_request", "pull_requests:write"],
594 ["merge_pull_request", "pull_requests:write"],
595 ["update_pull_request_branch", "pull_requests:write"],
596 ["enable_pull_request_auto_merge", "pull_requests:write"],
597 ["disable_pull_request_auto_merge", "pull_requests:write"],
598 ["delete_pull_request_head_branch", "pull_requests:write"],
599 ["restore_pull_request_head_branch", "pull_requests:write"],
600 ["request_reviewers", "pull_requests:write"],
601 ["add_comment", "pull_requests:write"],
602 ["edit_comment", "pull_requests:write"],
603 ["delete_comment", "pull_requests:write"],
604 ["react", "pull_requests:write"],
605 ["remove_requested_reviewers", "pull_requests:write"],
606 ["get_session", "pull_requests:read"],
607 ["list_sessions", "pull_requests:read"],
608 ["list_workspace_sessions", "pull_requests:read"],
609 ["get_workspace_session", "pull_requests:read"],
610 ["start_session", "pull_requests:write"],
611 ["update_session", "pull_requests:write"],
612 ["end_session", "pull_requests:write"],
613 ["record_session_entries", "pull_requests:write"],
614 ["start_agent_session", "pull_requests:write"],
615 ["list_workflows", "workflows:read"],
616 ["get_workflow", "workflows:read"],
617 ["list_workflow_runs", "workflows:read"],
618 ["get_workflow_run", "workflows:read"],
619 ["get_job_logs", "workflows:read"],
620 ["dispatch_workflow", "workflows:write"],
621 ["cancel_workflow_run", "workflows:write"],
622 ["rerun_workflow_run", "workflows:write"],
623 ["update_workflow", "workflows:write"],
624 ["list_artifacts", "workflows:read"],
625 ["list_workflow_run_artifacts", "workflows:read"],
626 ["get_artifact", "workflows:read"],
627 ["download_artifact", "workflows:read"],
628 ["get_artifact_retention", "workflows:read"],
629 ["delete_artifact", "workflows:write"],
630 ["set_artifact_retention", "workflows:write"],
631 ["list_caches", "workflows:read"],
632 ["get_cache_usage", "workflows:read"],
633 ["delete_caches_by_key", "workflows:write"],
634 ["delete_cache", "workflows:write"],
635 ["list_commit_statuses", "checks:read"],
636 ["get_combined_status", "checks:read"],
637 ["list_check_runs_for_ref", "checks:read"],
638 ["get_check_run", "checks:read"],
639 ["list_check_run_annotations", "checks:read"],
640 ["list_check_suites_for_ref", "checks:read"],
641 ["get_check_suite", "checks:read"],
642 ["create_commit_status", "checks:write"],
643 ["create_check_run", "checks:write"],
644 ["update_check_run", "checks:write"],
645 ["rerequest_check_run", "checks:write"],
646 ["rerequest_check_suite", "checks:write"],
647 // Deployments, wherever they run: reading them, and reporting them.
648 ["list_deployments", "deployments:read"],
649 ["get_deployment", "deployments:read"],
650 ["list_deployment_statuses", "deployments:read"],
651 ["list_environments", "deployments:read"],
652 ["get_environment", "deployments:read"],
653 ["create_deployment", "deployments:write"],
654 ["create_deployment_status", "deployments:write"],
655 ["start_deployment_build", "deployments:write"],
656 ["get_deployment_settings", "deployments:read"],
657 ["update_deployment_settings", "repo:admin"],
658 // Observability: what a project's software did, and how it is collected.
659 ["get_observability_overview", "observability:read"],
660 ["list_requests", "observability:read"],
661 ["get_request", "observability:read"],
662 ["list_logs", "observability:read"],
663 ["list_traces", "observability:read"],
664 ["get_trace", "observability:read"],
665 ["list_errors", "observability:read"],
666 ["list_metrics", "observability:read"],
667 ["get_metric", "observability:read"],
668 ["get_observability_settings", "observability:read"],
669 ["list_ingest_tokens", "observability:read"],
670 ["list_telemetry_exports", "observability:read"],
671 ["get_observability_usage", "observability:read"],
672 ["update_error", "observability:write"],
673 ["update_observability_settings", "observability:write"],
674 ["create_ingest_token", "observability:write"],
675 ["delete_ingest_token", "observability:write"],
676 ["create_telemetry_export", "observability:write"],
677 ["update_telemetry_export", "observability:write"],
678 ["delete_telemetry_export", "observability:write"],
679 // What keeps runs safe: the runs environments hold and reviewing them,
680 // approving a pull request's run, and a repository's own rules for its
681 // environments and tokens, which are an admin's.
682 ["get_pending_deployments", "workflows:read"],
683 ["review_pending_deployments", "workflows:write"],
684 ["approve_workflow_run", "workflows:write"],
685 ["get_workflow_permissions", "repo:read"],
686 ["get_fork_pr_approval", "repo:read"],
687 ["get_actions_access", "repo:read"],
688 ["get_actions_permissions", "repo:read"],
689 ["update_environment", "repo:admin"],
690 ["delete_environment", "repo:admin"],
691 ["set_workflow_permissions", "repo:admin"],
692 ["set_fork_pr_approval", "repo:admin"],
693 ["set_actions_access", "repo:admin"],
694 ["set_actions_permissions", "repo:admin"],
695 // Starting workflows from outside, as a push would.
696 ["create_repository_dispatch", "code:write"],
697 // A workspace's policy for its repositories' tokens.
698 ["get_workspace_workflow_permissions", "workspace:read"],
699 ["set_workspace_workflow_permissions", "workspace:admin"],
700 ["list_workspace_managed_workflows", "workspace:read"],
701 ["create_workspace_managed_workflow", "workspace:admin"],
702 ["update_workspace_managed_workflow", "workspace:admin"],
703 ["delete_workspace_managed_workflow", "workspace:admin"],
704 // A person's own Getting started in a workspace.
705 ["get_onboarding", "workspace:read"],
706 ["update_onboarding", "account:write"],
707 // A workspace's rules for personal access tokens, and its members' tokens.
708 ["get_token_policy", "workspace:read"],
709 ["set_token_policy", "workspace:admin"],
710 ["list_member_tokens", "access:read"],
711 ["list_token_requests", "access:read"],
712 ["review_token_request", "access:admin"],
713 ["revoke_member_token", "access:admin"],
714 // A workspace's single sign-on: its settings, and who is linked.
715 ["get_sso", "workspace:read"],
716 ["configure_sso", "workspace:admin"],
717 ["set_sso_required", "workspace:admin"],
718 ["delete_sso", "workspace:admin"],
719 ["list_sso_identities", "access:read"],
720 ["unlink_sso_identity", "access:admin"],
721 ["enable_scim", "workspace:admin"],
722 ["disable_scim", "workspace:admin"],
723 ["list_collaborators", "access:read"],
724 ["get_collaborator_permission", "access:read"],
725 ["list_repo_invitations", "access:read"],
726 ["list_outside_collaborators", "access:read"],
727 ["add_collaborator", "access:admin"],
728 ["update_collaborator", "access:admin"],
729 ["remove_collaborator", "access:admin"],
730 ["revoke_repo_invitation", "access:admin"],
731 ["set_base_permission", "access:admin"],
732 ["set_team_repo", "access:admin"],
733 ["remove_team_repo", "access:admin"],
734 ["list_deploy_keys", "access:read"],
735 ["get_deploy_key", "access:read"],
736 ["create_deploy_key", "access:admin"],
737 ["delete_deploy_key", "access:admin"],
738 ["list_webhooks", "webhooks:read"],
739 ["list_webhook_deliveries", "webhooks:read"],
740 ["create_webhook", "webhooks:admin"],
741 ["update_webhook", "webhooks:admin"],
742 ["delete_webhook", "webhooks:admin"],
743 ["ping_webhook", "webhooks:admin"],
744 ["redeliver_webhook", "webhooks:admin"],
745 ["list_actions_secrets", "secrets:read"],
746 ["list_actions_variables", "secrets:read"],
747 ["set_actions_secret", "secrets:admin"],
748 ["delete_actions_secret", "secrets:admin"],
749 ["set_actions_variable", "secrets:admin"],
750 ["delete_actions_variable", "secrets:admin"],
751 // Self-hosted runners.
752 ["list_runners", "runners:read"],
753 ["list_runner_groups", "runners:read"],
754 ["get_runner_settings", "runners:read"],
755 ["create_runner_registration_token", "runners:admin"],
756 ["remove_runner", "runners:admin"],
757 ["create_runner_group", "runners:admin"],
758 ["update_runner_group", "runners:admin"],
759 ["delete_runner_group", "runners:admin"],
760 ["update_runner_settings", "runners:admin"],
761 // Packages: reading them, their versions and who may use them needs
762 // `packages:read`; changing their settings, access and Manage Actions
763 // access `packages:write` (and the Admin role on the package, which the
764 // packages service checks); deleting and restoring packages and
765 // versions `packages:delete`, as the registries' own deletes do.
766 ["list_packages", "packages:read"],
767 ["get_package", "packages:read"],
768 ["list_package_versions", "packages:read"],
769 ["get_package_version", "packages:read"],
770 ["list_package_access", "packages:read"],
771 ["list_package_actions_access", "packages:read"],
772 ["update_package", "packages:write"],
773 ["link_package", "packages:write"],
774 ["unlink_package", "packages:write"],
775 ["set_package_access", "packages:write"],
776 ["remove_package_access", "packages:write"],
777 ["set_package_actions_access", "packages:write"],
778 ["remove_package_actions_access", "packages:write"],
779 ["delete_package", "packages:delete"],
780 ["restore_package", "packages:delete"],
781 ["delete_package_version", "packages:delete"],
782 ["restore_package_version", "packages:delete"],
783 // Artifacts mode's docs, slides, designs and dashboards (the `artifact`
784 // MCP tool). The artifacts service then checks the person's role on each.
785 ["list_workspace_artifacts", "artifacts:read"],
786 ["search_workspace_artifacts", "artifacts:read"],
787 ["get_workspace_artifact", "artifacts:read"],
788 ["get_workspace_artifact_content", "artifacts:read"],
789 ["list_workspace_artifact_versions", "artifacts:read"],
790 ["get_workspace_artifact_access", "artifacts:read"],
791 ["list_workspace_artifact_templates", "artifacts:read"],
792 ["list_workspace_artifact_spaces", "artifacts:read"],
793 ["query_workspace_dataset", "artifacts:read"],
794 ["create_workspace_artifact", "artifacts:write"],
795 ["update_workspace_artifact", "artifacts:write"],
796 ["edit_workspace_artifact", "artifacts:write"],
797 ["trash_workspace_artifact", "artifacts:write"],
798 ["restore_workspace_artifact", "artifacts:write"],
799 ["restore_workspace_artifact_version", "artifacts:write"],
800 ["set_workspace_artifact_access", "artifacts:admin"],
801 ["purge_workspace_artifact", "artifacts:admin"],
802] as const;
803
804/**
805 * The scopes a token or grant holds, as stored: null for full access, or
806 * the list. `legacy` marks a token made before scopes, which has full
807 * access until someone narrows it.
808 */
809export type TokenScopes = {
810 scopes: Scope[] | null;
811 legacy: boolean;
812};
813
814/**
815 * How settings group scopes into a checklist: each group's scopes, least
816 * first. Admin scopes are not here; they are under "Dangerous" on their
817 * own (see `DANGEROUS_SCOPES`). Every other scope is in exactly one group.
818 */
819export const SCOPE_GROUPS: { id: string; label: string; scopes: Scope[] }[] = [
820 { id: "code", label: "Repositories & code", scopes: ["repo:read", "repo:create", "repo:write", "code:read", "code:write"] },
821 { id: "security", label: "Security", scopes: ["security:read", "security:write"] },
822 { id: "packages", label: "Packages", scopes: ["packages:read", "packages:write"] },
823 { id: "work", label: "Work & pull requests", scopes: ["work:read", "work:write", "pull_requests:read", "pull_requests:write"] },
824 { id: "workflows", label: "Workflows", scopes: ["workflows:read", "workflows:write", "workflow_files:write"] },
825 { id: "checks", label: "Checks", scopes: ["checks:read", "checks:write"] },
826 { id: "deployments", label: "Deployments", scopes: ["deployments:read", "deployments:write"] },
827 { id: "observability", label: "Observability", scopes: ["observability:read", "observability:write"] },
828 { id: "account", label: "Account", scopes: ["account:read", "account:write"] },
829 { id: "notifications", label: "Notifications", scopes: ["notifications:read", "notifications:write"] },
830 { id: "chat", label: "Chat", scopes: ["chat:read", "chat:write"] },
831 { id: "workspace", label: "Workspace", scopes: ["workspace:read", "access:read", "webhooks:read", "secrets:read"] },
832 { id: "billing", label: "Billing", scopes: ["billing:read", "billing:write"] },
833 { id: "runners", label: "Runners", scopes: ["runners:read"] },
834 { id: "artifacts", label: "Artifacts", scopes: ["artifacts:read", "artifacts:write"] },
835];
836
837/** The admin and delete scopes, shown under "Dangerous" behind a warning. */
838export const DANGEROUS_SCOPES: Scope[] = SCOPES.map((row) => row.scope).filter(isDangerous);