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.
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 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 reaches whatever its owner can reach (a workspace's token, that | |
| 7 | * workspace); what a request may do is the intersection of the owner's | |
| 8 | * role and the token's scopes. | |
| 9 | */ | |
| 10 | ||
| 11 | export type ScopeResource = | |
| 12 | | "repo" | |
| 13 | | "code" | |
| 14 | | "issues" | |
| 15 | | "pull_requests" | |
| 16 | | "agents" | |
| 17 | | "workflows" | |
| 18 | | "memory" | |
| 19 | | "account" | |
| 20 | | "workspace" | |
| 21 | | "access" | |
| 22 | | "webhooks" | |
| 23 | | "secrets"; | |
| 24 | ||
| 25 | export type ScopeLevel = "read" | "write" | "run" | "admin"; | |
| 26 | ||
| 27 | /** Every scope, grouped by resource, least first. */ | |
| 28 | export const SCOPES = [ | |
| 29 | { scope: "repo:read", description: "See repositories, their settings, labels and timelines, and search" }, | |
| 30 | { scope: "repo:write", description: "Create repositories, rename branches and change how pull requests merge" }, | |
| 31 | { scope: "repo:admin", description: "Rename, archive, transfer, delete or change who can see a repository" }, | |
| 32 | { scope: "code:read", description: "Clone and fetch private repositories with git" }, | |
| 33 | { scope: "code:write", description: "Push commits with git" }, | |
| 34 | { scope: "issues:read", description: "Read issues, comments and plans" }, | |
| 35 | { scope: "issues:write", description: "Open, edit, close and comment on issues" }, | |
| 36 | { scope: "pull_requests:read", description: "Read pull requests, their changes, sessions and merge queues" }, | |
| 37 | { scope: "pull_requests:write", description: "Open, review, close and merge pull requests" }, | |
| 38 | { scope: "agents:run", description: "Put g1t agents to work and message them, which uses the workspace's money" }, | |
| 39 | { scope: "workflows:read", description: "Read workflows, runs and logs" }, | |
| 40 | { scope: "workflows:write", description: "Run, cancel, rerun and turn workflows on or off" }, | |
| 41 | { scope: "memory:read", description: "Recall memory and search the workspace's context" }, | |
| 42 | { scope: "memory:write", description: "Save memory for the next agent" }, | |
| 43 | { scope: "account:read", description: "Read your email addresses, invites and invitations" }, | |
| 44 | { scope: "account:write", description: "Change your email addresses, make invites and answer invitations" }, | |
| 45 | { scope: "workspace:read", description: "Read workspace invites, integrations and model routes" }, | |
| 46 | { scope: "workspace:admin", description: "Create and delete workspaces, invite members, connect integrations" }, | |
| 47 | { scope: "access:read", description: "See who has access to repositories" }, | |
| 48 | { scope: "access:admin", description: "Give and take away access to repositories" }, | |
| 49 | { scope: "webhooks:read", description: "See webhooks and their deliveries" }, | |
| 50 | { scope: "webhooks:admin", description: "Create, change and delete webhooks" }, | |
| 51 | { scope: "secrets:read", description: "List secrets (never their values) and read variables" }, | |
| 52 | { scope: "secrets:admin", description: "Set and delete secrets and variables" }, | |
| 53 | ] as const; | |
| 54 | ||
| 55 | export type Scope = (typeof SCOPES)[number]["scope"]; | |
| 56 | ||
| 57 | /** Resources in the order settings show them, with their names for people. */ | |
| 58 | export const SCOPE_RESOURCES: { resource: ScopeResource; label: string }[] = [ | |
| 59 | { resource: "repo", label: "Repositories" }, | |
| 60 | { resource: "code", label: "Code" }, | |
| 61 | { resource: "issues", label: "Issues" }, | |
| 62 | { resource: "pull_requests", label: "Pull requests" }, | |
| 63 | { resource: "agents", label: "g1t agents" }, | |
| 64 | { resource: "workflows", label: "Workflows" }, | |
| 65 | { resource: "memory", label: "Memory and context" }, | |
| 66 | { resource: "account", label: "Your account" }, | |
| 67 | { resource: "workspace", label: "Workspaces" }, | |
| 68 | { resource: "access", label: "Who has access" }, | |
| 69 | { resource: "webhooks", label: "Webhooks" }, | |
| 70 | { resource: "secrets", label: "Secrets and variables" }, | |
| 71 | ]; | |
| 72 | ||
| 73 | const LEVEL_ORDER: Record<ScopeLevel, number> = { read: 0, write: 1, run: 2, admin: 3 }; | |
| 74 | ||
| 75 | export function scopeResource(scope: Scope): ScopeResource { | |
| 76 | return scope.split(":")[0] as ScopeResource; | |
| 77 | } | |
| 78 | ||
| 79 | export function scopeLevel(scope: Scope): ScopeLevel { | |
| 80 | return scope.split(":")[1] as ScopeLevel; | |
| 81 | } | |
| 82 | ||
| 83 | export function isScope(text: string): text is Scope { | |
| 84 | return SCOPES.some((row) => row.scope === text); | |
| 85 | } | |
| 86 | ||
| 87 | /** Changes that are hard to undo, or decide who can reach what. */ | |
| 88 | export function isDangerous(scope: Scope): boolean { | |
| 89 | return scopeLevel(scope) === "admin"; | |
| 90 | } | |
| 91 | ||
| 92 | export function describeScope(scope: Scope): string { | |
| 93 | return SCOPES.find((row) => row.scope === scope)?.description ?? scope; | |
| 94 | } | |
| 95 | ||
| 96 | /** Whether holding `held` gives `needed`: the same resource, at its level or lower. */ | |
| 97 | export function scopeIncludes(held: Scope, needed: Scope): boolean { | |
| 98 | return ( | |
| 99 | scopeResource(held) === scopeResource(needed) && | |
| 100 | LEVEL_ORDER[scopeLevel(held)] >= LEVEL_ORDER[scopeLevel(needed)] | |
| 101 | ); | |
| 102 | } | |
| 103 | ||
| 104 | /** The levels a resource has, least first. */ | |
| 105 | export function levelsOf(resource: ScopeResource): ScopeLevel[] { | |
| 106 | return SCOPES.filter((row) => scopeResource(row.scope) === resource).map((row) => scopeLevel(row.scope)); | |
| 107 | } | |
| 108 | ||
| 109 | /** Scopes from text separated by spaces or commas, in table order; unknown ones are left out. */ | |
| 110 | export function parseScopes(text: string): Scope[] { | |
| 111 | const given = new Set(text.split(/[\s,]+/).map((part) => part.trim().toLowerCase())); | |
| 112 | return SCOPES.map((row) => row.scope).filter((scope) => given.has(scope)); | |
| 113 | } | |
| 114 | ||
| 115 | /** What a token stores for full access. */ | |
| 116 | export const FULL_ACCESS = "*"; | |
| 117 | ||
| 118 | export type PresetId = "read_only" | "agent" | "ci" | "full"; | |
| 119 | ||
| 120 | /** Starting points for choosing scopes. `*` is full access. */ | |
| 121 | export const PRESET_SCOPES = { | |
| 122 | read_only: [ | |
| 123 | "repo:read", "code:read", "issues:read", "pull_requests:read", "workflows:read", "memory:read", "account:read", "workspace:read", "access:read", "webhooks:read", "secrets:read", | |
| 124 | ] as const, | |
| 125 | agent: [ | |
| 126 | "repo:read", "code:read", "code:write", "issues:read", "issues:write", "pull_requests:read", "pull_requests:write", "agents:run", "workflows:read", "memory:read", "memory:write", "account:read", "workspace:read", "access:read", "webhooks:read", "secrets:read", | |
| 127 | ] as const, | |
| 128 | ci: [ | |
| 129 | "repo:read", "code:read", "code:write", "workflows:read", "workflows:write", | |
| 130 | ] as const, | |
| 131 | full: [ | |
| 132 | "*", | |
| 133 | ] as const, | |
| 134 | }; | |
| 135 | ||
| 136 | export const PRESETS: { id: PresetId; label: string; description: string }[] = [ | |
| 137 | { id: "read_only", label: "Read only", description: "Read everything you can read; change nothing." }, | |
| 138 | { id: "agent", label: "Agent", description: "Read everything, work on issues and pull requests, push code and run g1t agents." }, | |
| 139 | { id: "ci", label: "CI", description: "Clone and push code, and run workflows." }, | |
| 140 | { id: "full", label: "Full access", description: "Everything you can do, including deleting repositories and changing who has access." }, | |
| 141 | ]; | |
| 142 | ||
| 143 | /** The scopes of a preset, or null for full access. */ | |
| 144 | export function presetScopes(id: PresetId): Scope[] | null { | |
| 145 | if (id === "full") return null; | |
| 146 | return [...PRESET_SCOPES[id]] as Scope[]; | |
| 147 | } | |
| 148 | ||
| 149 | /** What an OAuth client gets when it asks for nothing in particular. */ | |
| 150 | export const OAUTH_DEFAULT_SCOPES: Scope[] = [...PRESET_SCOPES.agent]; | |
| 151 | ||
| 152 | /** The operation each scope gates, by the API's operation names. */ | |
| 153 | export const OPERATION_SCOPES = [ | |
| 154 | ["list_emails", "account:read"], | |
| 155 | ["add_email", "account:write"], | |
| 156 | ["remove_email", "account:write"], | |
| 157 | ["update_email_settings", "account:write"], | |
| 158 | ["list_invites", "account:read"], | |
| 159 | ["create_invite", "account:write"], | |
| 160 | ["revoke_invite", "account:write"], | |
| 161 | ["list_my_repo_invitations", "account:read"], | |
| 162 | ["accept_repo_invitation", "account:write"], | |
| 163 | ["decline_repo_invitation", "account:write"], | |
| 164 | ["create_workspace", "workspace:admin"], | |
| 165 | ["delete_workspace", "workspace:admin"], | |
| 166 | ["list_workspace_invites", "workspace:read"], | |
| 167 | ["invite_member", "workspace:admin"], | |
| 168 | ["revoke_workspace_invite", "workspace:admin"], | |
| 169 | ["list_integrations", "workspace:read"], | |
| 170 | ["connect_integration", "workspace:admin"], | |
| 171 | ["disconnect_integration", "workspace:admin"], | |
| 172 | ["test_integration", "workspace:admin"], | |
| 173 | ["get_model_routes", "workspace:read"], | |
| 174 | ["set_model_routes", "workspace:admin"], | |
| 175 | ["list_repos", "repo:read"], | |
| 176 | ["get_repo", "repo:read"], | |
| 177 | ["search", "repo:read"], | |
| 178 | ["list_events", "repo:read"], | |
| 179 | ["list_labels", "repo:read"], | |
| 180 | ["get_repo_settings", "repo:read"], | |
| 181 | ["list_deleted_repos", "repo:read"], | |
| 182 | ["create_repo", "repo:write"], | |
| 183 | ["update_repo", "repo:write"], | |
| 184 | ["update_repo_settings", "repo:write"], | |
| 185 | ["rename_branch", "repo:write"], | |
| 186 | ["rename_repo", "repo:admin"], | |
| 187 | ["transfer_repo", "repo:admin"], | |
| 188 | ["archive_repo", "repo:admin"], | |
| 189 | ["unarchive_repo", "repo:admin"], | |
| 190 | ["set_repo_visibility", "repo:admin"], | |
| 191 | ["delete_repo", "repo:admin"], | |
| 192 | ["restore_repo", "repo:admin"], | |
| 193 | ["purge_repo", "repo:admin"], | |
| 194 | ["list_issues", "issues:read"], | |
| 195 | ["get_issue", "issues:read"], | |
| 196 | ["get_plan", "issues:read"], | |
| 197 | ["create_issue", "issues:write"], | |
| 198 | ["update_issue", "issues:write"], | |
| 199 | ["close_issue", "issues:write"], | |
| 200 | ["reopen_issue", "issues:write"], | |
| 201 | ["add_comment", "issues:write"], | |
| 202 | ["import_issue", "issues:write"], | |
| 203 | ["apply_plan", "issues:write"], | |
| 204 | ["list_pull_requests", "pull_requests:read"], | |
| 205 | ["get_pull_request", "pull_requests:read"], | |
| 206 | ["get_pull_request_changes", "pull_requests:read"], | |
| 207 | ["read_session", "pull_requests:read"], | |
| 208 | ["get_merge_queue", "pull_requests:read"], | |
| 209 | ["create_pull_request", "pull_requests:write"], | |
| 210 | ["record_session", "pull_requests:write"], | |
| 211 | ["mark_pull_request_ready", "pull_requests:write"], | |
| 212 | ["close_pull_request", "pull_requests:write"], | |
| 213 | ["review_pull_request", "pull_requests:write"], | |
| 214 | ["merge_pull_request", "pull_requests:write"], | |
| 215 | ["assign_issue", "agents:run"], | |
| 216 | ["delegate", "agents:run"], | |
| 217 | ["plan_work", "agents:run"], | |
| 218 | ["message_agent", "agents:run"], | |
| 219 | ["answer_message", "agents:run"], | |
| 220 | ["take_messages", "agents:run"], | |
| 221 | ["list_workflows", "workflows:read"], | |
| 222 | ["list_workflow_runs", "workflows:read"], | |
| 223 | ["get_workflow_run", "workflows:read"], | |
| 224 | ["get_job_logs", "workflows:read"], | |
| 225 | ["dispatch_workflow", "workflows:write"], | |
| 226 | ["cancel_workflow_run", "workflows:write"], | |
| 227 | ["rerun_workflow_run", "workflows:write"], | |
| 228 | ["update_workflow", "workflows:write"], | |
| 229 | ["recall", "memory:read"], | |
| 230 | ["search_context", "memory:read"], | |
| 231 | ["get_entity", "memory:read"], | |
| 232 | ["get_context", "memory:read"], | |
| 233 | ["remember", "memory:write"], | |
| 234 | ["list_collaborators", "access:read"], | |
| 235 | ["get_collaborator_permission", "access:read"], | |
| 236 | ["list_repo_invitations", "access:read"], | |
| 237 | ["list_outside_collaborators", "access:read"], | |
| 238 | ["add_collaborator", "access:admin"], | |
| 239 | ["update_collaborator", "access:admin"], | |
| 240 | ["remove_collaborator", "access:admin"], | |
| 241 | ["revoke_repo_invitation", "access:admin"], | |
| 242 | ["set_base_permission", "access:admin"], | |
| 243 | ["list_webhooks", "webhooks:read"], | |
| 244 | ["list_webhook_deliveries", "webhooks:read"], | |
| 245 | ["create_webhook", "webhooks:admin"], | |
| 246 | ["update_webhook", "webhooks:admin"], | |
| 247 | ["delete_webhook", "webhooks:admin"], | |
| 248 | ["ping_webhook", "webhooks:admin"], | |
| 249 | ["redeliver_webhook", "webhooks:admin"], | |
| 250 | ["list_actions_secrets", "secrets:read"], | |
| 251 | ["list_actions_variables", "secrets:read"], | |
| 252 | ["set_actions_secret", "secrets:admin"], | |
| 253 | ["delete_actions_secret", "secrets:admin"], | |
| 254 | ["set_actions_variable", "secrets:admin"], | |
| 255 | ["delete_actions_variable", "secrets:admin"], | |
| 256 | ] as const; | |
| 257 | ||
| 258 | /** | |
| 259 | * The scopes a token or grant holds, as stored: null for full access, or | |
| 260 | * the list. `legacy` marks a token made before scopes, which has full | |
| 261 | * access until someone narrows it. | |
| 262 | */ | |
| 263 | export type TokenScopes = { | |
| 264 | scopes: Scope[] | null; | |
| 265 | legacy: boolean; | |
| 266 | }; | |
| 267 | ||
| 268 | /** | |
| 269 | * How settings group scopes into a checklist: each group's scopes, least | |
| 270 | * first. Admin scopes are not here; they are under "Dangerous" on their | |
| 271 | * own (see `DANGEROUS_SCOPES`). Every other scope is in exactly one group. | |
| 272 | */ | |
| 273 | export const SCOPE_GROUPS: { id: string; label: string; scopes: Scope[] }[] = [ | |
| 274 | { id: "code", label: "Repositories & code", scopes: ["repo:read", "repo:write", "code:read", "code:write"] }, | |
| 275 | { id: "work", label: "Issues & pull requests", scopes: ["issues:read", "issues:write", "pull_requests:read", "pull_requests:write"] }, | |
| 276 | { id: "agents", label: "Agents", scopes: ["agents:run"] }, | |
| 277 | { id: "workflows", label: "Workflows", scopes: ["workflows:read", "workflows:write"] }, | |
| 278 | { id: "memory", label: "Memory & search", scopes: ["memory:read", "memory:write"] }, | |
| 279 | { id: "account", label: "Account", scopes: ["account:read", "account:write"] }, | |
| 280 | { id: "workspace", label: "Workspace", scopes: ["workspace:read", "access:read", "webhooks:read", "secrets:read"] }, | |
| 281 | ]; | |
| 282 | ||
| 283 | /** The admin scopes, shown under "Dangerous" behind a warning. */ | |
| 284 | export const DANGEROUS_SCOPES: Scope[] = SCOPES.map((row) => row.scope).filter(isDangerous); |