Skip to content
1,149 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.

Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1//! Scopes: what an access token may do on its owner's behalf.
2//!
3//! A personal access token, a workspace's token and an application signed
4//! in with OAuth each carry a set of scopes. A token reaches whatever the
5//! one it acts as can reach: a person's token, that person's workspaces and
6//! repositories; a workspace's token, that workspace. What a request may do
7//! is the intersection of two things: the role of whoever the token acts as
8//! (see [`crate::access`]) and the token's scopes.
9//!
10//! Each scope is a resource and a level, written `resource:level`, such as
11//! `issues:write`. A higher level of a resource includes the lower ones:
12//! `repo:admin` includes `repo:write`, which includes `repo:read`.
13//!
14//! This module is the one source of truth: the API (REST and MCP) and git
15//! enforce it, and identity stores it. `packages/contracts/src/scopes.ts`
16//! mirrors the table for the site; a test keeps the two the same.
17
18use serde::{Deserialize, Serialize};
19
20use crate::credentials::Decision;
21
22/// Something a token can be given access to.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24pub enum Resource {
25 Account,
API: notifications over REST and MCP, with notifications scopes26 Notifications,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step27 Workspace,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit28 Billing,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step29 Repo,
30 Code,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar31 Security,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member32 Packages,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step33 Issues,
34 PullRequests,
35 Agents,
36 Workflows,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9737 Deployments,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step38 Memory,
39 Access,
40 Webhooks,
41 Secrets,
Fast pages, required checks on the branch, self-hosted runners, honest incidents42 Runners,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens43 Models,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step44}
45
46impl Resource {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9747 pub const ALL: [Resource; 19] = [
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step48 Resource::Repo,
49 Resource::Code,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar50 Resource::Security,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member51 Resource::Packages,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step52 Resource::Issues,
53 Resource::PullRequests,
54 Resource::Agents,
55 Resource::Workflows,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9756 Resource::Deployments,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step57 Resource::Memory,
58 Resource::Account,
API: notifications over REST and MCP, with notifications scopes59 Resource::Notifications,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step60 Resource::Workspace,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit61 Resource::Billing,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step62 Resource::Access,
63 Resource::Webhooks,
64 Resource::Secrets,
Fast pages, required checks on the branch, self-hosted runners, honest incidents65 Resource::Runners,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens66 Resource::Models,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step67 ];
68
69 pub fn as_str(self) -> &'static str {
70 match self {
71 Resource::Account => "account",
API: notifications over REST and MCP, with notifications scopes72 Resource::Notifications => "notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step73 Resource::Workspace => "workspace",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit74 Resource::Billing => "billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step75 Resource::Repo => "repo",
76 Resource::Code => "code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar77 Resource::Security => "security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member78 Resource::Packages => "packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step79 Resource::Issues => "issues",
80 Resource::PullRequests => "pull_requests",
81 Resource::Agents => "agents",
82 Resource::Workflows => "workflows",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9783 Resource::Deployments => "deployments",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step84 Resource::Memory => "memory",
85 Resource::Access => "access",
86 Resource::Webhooks => "webhooks",
87 Resource::Secrets => "secrets",
Fast pages, required checks on the branch, self-hosted runners, honest incidents88 Resource::Runners => "runners",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens89 Resource::Models => "models",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step90 }
91 }
92
93 /// Its name, for people.
94 pub fn label(self) -> &'static str {
95 match self {
96 Resource::Account => "Your account",
API: notifications over REST and MCP, with notifications scopes97 Resource::Notifications => "Notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step98 Resource::Workspace => "Workspaces",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit99 Resource::Billing => "Billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step100 Resource::Repo => "Repositories",
101 Resource::Code => "Code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar102 Resource::Security => "Security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member103 Resource::Packages => "Packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step104 Resource::Issues => "Issues",
105 Resource::PullRequests => "Pull requests",
106 Resource::Agents => "g1t agents",
107 Resource::Workflows => "Workflows",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97108 Resource::Deployments => "Deployments",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step109 Resource::Memory => "Memory and context",
110 Resource::Access => "Who has access",
111 Resource::Webhooks => "Webhooks",
112 Resource::Secrets => "Secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents113 Resource::Runners => "Self-hosted runners",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens114 Resource::Models => "AI Gateway",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step115 }
116 }
117}
118
119/// How much of a resource.
120#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
121pub enum Level {
122 Read,
123 Write,
124 /// Starting g1t's agents, which spends the workspace's money.
125 Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member126 /// Deleting what cannot be brought back, such as a package's versions.
127 Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step128 Admin,
129}
130
131impl Level {
132 pub fn as_str(self) -> &'static str {
133 match self {
134 Level::Read => "read",
135 Level::Write => "write",
136 Level::Run => "run",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member137 Level::Delete => "delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step138 Level::Admin => "admin",
139 }
140 }
141}
142
143/// One scope. Its text form, `resource:level`, is what tokens store, OAuth
144/// clients ask for, and errors name.
145#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
146pub enum Scope {
147 RepoRead,
148 RepoWrite,
149 RepoAdmin,
150 CodeRead,
151 CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar152 SecurityRead,
153 SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member154 PackagesRead,
155 PackagesWrite,
156 PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step157 IssuesRead,
158 IssuesWrite,
159 PullRequestsRead,
160 PullRequestsWrite,
161 AgentsRun,
162 WorkflowsRead,
163 WorkflowsWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97164 DeploymentsRead,
165 DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step166 MemoryRead,
167 MemoryWrite,
168 AccountRead,
169 AccountWrite,
API: notifications over REST and MCP, with notifications scopes170 NotificationsRead,
171 NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step172 WorkspaceRead,
173 WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit174 BillingRead,
175 BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step176 AccessRead,
177 AccessAdmin,
178 WebhooksRead,
179 WebhooksAdmin,
180 SecretsRead,
181 SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents182 RunnersRead,
183 RunnersAdmin,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens184 ModelsRead,
185 ModelsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step186}
187
188impl Scope {
189 /// Every scope, grouped by resource, least first.
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97190 pub const ALL: [Scope; 39] = [
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step191 Scope::RepoRead,
192 Scope::RepoWrite,
193 Scope::RepoAdmin,
194 Scope::CodeRead,
195 Scope::CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar196 Scope::SecurityRead,
197 Scope::SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member198 Scope::PackagesRead,
199 Scope::PackagesWrite,
200 Scope::PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step201 Scope::IssuesRead,
202 Scope::IssuesWrite,
203 Scope::PullRequestsRead,
204 Scope::PullRequestsWrite,
205 Scope::AgentsRun,
206 Scope::WorkflowsRead,
207 Scope::WorkflowsWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97208 Scope::DeploymentsRead,
209 Scope::DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step210 Scope::MemoryRead,
211 Scope::MemoryWrite,
212 Scope::AccountRead,
213 Scope::AccountWrite,
API: notifications over REST and MCP, with notifications scopes214 Scope::NotificationsRead,
215 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step216 Scope::WorkspaceRead,
217 Scope::WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit218 Scope::BillingRead,
219 Scope::BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step220 Scope::AccessRead,
221 Scope::AccessAdmin,
222 Scope::WebhooksRead,
223 Scope::WebhooksAdmin,
224 Scope::SecretsRead,
225 Scope::SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents226 Scope::RunnersRead,
227 Scope::RunnersAdmin,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens228 Scope::ModelsRead,
229 Scope::ModelsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step230 ];
231
232 pub fn as_str(self) -> &'static str {
233 match self {
234 Scope::RepoRead => "repo:read",
235 Scope::RepoWrite => "repo:write",
236 Scope::RepoAdmin => "repo:admin",
237 Scope::CodeRead => "code:read",
238 Scope::CodeWrite => "code:write",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar239 Scope::SecurityRead => "security:read",
240 Scope::SecurityWrite => "security:write",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member241 Scope::PackagesRead => "packages:read",
242 Scope::PackagesWrite => "packages:write",
243 Scope::PackagesDelete => "packages:delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step244 Scope::IssuesRead => "issues:read",
245 Scope::IssuesWrite => "issues:write",
246 Scope::PullRequestsRead => "pull_requests:read",
247 Scope::PullRequestsWrite => "pull_requests:write",
248 Scope::AgentsRun => "agents:run",
249 Scope::WorkflowsRead => "workflows:read",
250 Scope::WorkflowsWrite => "workflows:write",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97251 Scope::DeploymentsRead => "deployments:read",
252 Scope::DeploymentsWrite => "deployments:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step253 Scope::MemoryRead => "memory:read",
254 Scope::MemoryWrite => "memory:write",
255 Scope::AccountRead => "account:read",
256 Scope::AccountWrite => "account:write",
API: notifications over REST and MCP, with notifications scopes257 Scope::NotificationsRead => "notifications:read",
258 Scope::NotificationsWrite => "notifications:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step259 Scope::WorkspaceRead => "workspace:read",
260 Scope::WorkspaceAdmin => "workspace:admin",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit261 Scope::BillingRead => "billing:read",
262 Scope::BillingWrite => "billing:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step263 Scope::AccessRead => "access:read",
264 Scope::AccessAdmin => "access:admin",
265 Scope::WebhooksRead => "webhooks:read",
266 Scope::WebhooksAdmin => "webhooks:admin",
267 Scope::SecretsRead => "secrets:read",
268 Scope::SecretsAdmin => "secrets:admin",
Fast pages, required checks on the branch, self-hosted runners, honest incidents269 Scope::RunnersRead => "runners:read",
270 Scope::RunnersAdmin => "runners:admin",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens271 Scope::ModelsRead => "models:read",
272 Scope::ModelsWrite => "models:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step273 }
274 }
275
276 pub fn parse(text: &str) -> Option<Scope> {
277 let text = text.trim().to_ascii_lowercase();
278 Scope::ALL.into_iter().find(|scope| scope.as_str() == text)
279 }
280
281 pub fn resource(self) -> Resource {
282 let name = self.as_str().split_once(':').map_or("", |(resource, _)| resource);
283 Resource::ALL
284 .into_iter()
285 .find(|resource| resource.as_str() == name)
286 .unwrap_or(Resource::Account)
287 }
288
289 pub fn level(self) -> Level {
290 match self.as_str().rsplit_once(':').map_or("", |(_, level)| level) {
291 "write" => Level::Write,
292 "run" => Level::Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member293 "delete" => Level::Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step294 "admin" => Level::Admin,
295 _ => Level::Read,
296 }
297 }
298
299 /// Whether holding `self` gives `other`: the same resource, at the same
300 /// level or a lower one.
301 pub fn includes(self, other: Scope) -> bool {
302 self.resource() == other.resource() && self.level() >= other.level()
303 }
304
305 /// Changes that are hard or impossible to undo, or that decide who can
306 /// reach what. Shown behind a warning wherever scopes are chosen.
307 pub fn dangerous(self) -> bool {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member308 matches!(self.level(), Level::Admin | Level::Delete)
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step309 }
310
311 /// What it lets a token do, in plain words.
312 pub fn describe(self) -> &'static str {
313 match self {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97314 Scope::RepoRead => "See repositories, their settings, labels, timelines, releases, languages, contributors and security alerts, and search",
315 Scope::RepoWrite => "Create repositories, rename branches, change how pull requests merge and publish releases",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge316 Scope::RepoAdmin => "Rename, archive, transfer, delete or change who can see a repository, change its rulesets, and dismiss security alerts",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step317 Scope::CodeRead => "Clone and fetch private repositories with git",
318 Scope::CodeWrite => "Push commits with git",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar319 Scope::SecurityRead => "See secret scanning, code scanning and vulnerability alerts, custom patterns, the dependency graph and SBOM, and security settings",
320 Scope::SecurityWrite => "Dismiss and reopen alerts, bypass push protection, review bypass requests, manage custom patterns, upload SARIF and change security settings",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member321 Scope::PackagesRead => "Pull container images and install private packages",
322 Scope::PackagesWrite => "Push container images and publish packages",
323 Scope::PackagesDelete => "Delete packages and their versions",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step324 Scope::IssuesRead => "Read issues, comments and plans",
325 Scope::IssuesWrite => "Open, edit, close and comment on issues",
326 Scope::PullRequestsRead => "Read pull requests, their changes, sessions and merge queues",
327 Scope::PullRequestsWrite => "Open, review, close and merge pull requests",
328 Scope::AgentsRun => "Put g1t agents to work and message them, which uses the workspace's money",
329 Scope::WorkflowsRead => "Read workflows, runs and logs",
330 Scope::WorkflowsWrite => "Run, cancel, rerun and turn workflows on or off",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97331 Scope::DeploymentsRead => "See deployments, their statuses and environments",
332 Scope::DeploymentsWrite => "Report deployments and their statuses, from any CI",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step333 Scope::MemoryRead => "Recall memory and search the workspace's context",
334 Scope::MemoryWrite => "Save memory for the next agent",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97335 Scope::AccountRead => "Read your email addresses, invites, invitations, pinned projects and stars",
336 Scope::AccountWrite => "Change your email addresses, make invites, answer invitations, pin projects and star repositories",
API: notifications over REST and MCP, with notifications scopes337 Scope::NotificationsRead => "See your inbox, its threads, and what you subscribe to and watch",
338 Scope::NotificationsWrite => "Mark notifications read, done, saved or snoozed, subscribe to threads and watch repositories",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge339 Scope::WorkspaceRead => "Read workspace settings, invites, integrations, model routes, teams and rulesets",
340 Scope::WorkspaceAdmin => "Create and delete workspaces, invite members, connect integrations, create, change and delete teams, and change the workspace's rulesets",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit341 Scope::BillingRead => "See a workspace's usage, budget, AI credit and invoices",
342 Scope::BillingWrite => "Change a workspace's budget and buy AI credit",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step343 Scope::AccessRead => "See who has access to repositories",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar344 Scope::AccessAdmin => "Give and take away access to repositories, a team's included",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step345 Scope::WebhooksRead => "See webhooks and their deliveries",
346 Scope::WebhooksAdmin => "Create, change and delete webhooks",
347 Scope::SecretsRead => "List secrets (never their values) and read variables",
348 Scope::SecretsAdmin => "Set and delete secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents349 Scope::RunnersRead => "See self-hosted runners, their groups and where agents run",
350 Scope::RunnersAdmin => "Register and remove self-hosted runners, change their groups and settings",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens351 Scope::ModelsRead => "See the workspace's AI Gateway requests: their models, tokens, cost and status",
352 Scope::ModelsWrite => "Send model requests through the AI Gateway, which uses the workspace's AI credit",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step353 }
354 }
355}
356
357impl Serialize for Scope {
358 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
359 serializer.serialize_str(self.as_str())
360 }
361}
362
363impl<'de> Deserialize<'de> for Scope {
364 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
365 let text = String::deserialize(deserializer)?;
366 Scope::parse(&text).ok_or_else(|| serde::de::Error::custom(format!("unknown scope {text}")))
367 }
368}
369
370/// Scopes as written in a token's row or an OAuth request: separated by
371/// spaces or commas. Unknown names are left out, so a client asking for a
372/// scope from a newer version gets the rest.
373pub fn parse_scopes(text: &str) -> Vec<Scope> {
374 let mut scopes: Vec<Scope> = text
375 .split(|c: char| c.is_whitespace() || c == ',')
376 .filter_map(Scope::parse)
377 .collect();
378 normalize(&mut scopes);
379 scopes
380}
381
382/// In table order, without repeats.
383pub fn normalize(scopes: &mut Vec<Scope>) {
384 let given = std::mem::take(scopes);
385 scopes.extend(Scope::ALL.into_iter().filter(|scope| given.contains(scope)));
386}
387
388/// Space-separated, as stored and as OAuth writes them.
389pub fn scopes_text(scopes: &[Scope]) -> String {
390 scopes.iter().map(|scope| scope.as_str()).collect::<Vec<_>>().join(" ")
391}
392
393/// What a token stores for full access, which is not a scope a client can
394/// ask for by name.
395pub const FULL_ACCESS: &str = "*";
396
397/// Starting points for choosing scopes.
398#[derive(Clone, Copy, Debug, PartialEq, Eq)]
399pub enum Preset {
400 ReadOnly,
401 Agent,
402 Ci,
403 Full,
404}
405
406impl Preset {
407 pub const ALL: [Preset; 4] = [Preset::ReadOnly, Preset::Agent, Preset::Ci, Preset::Full];
408
409 pub fn as_str(self) -> &'static str {
410 match self {
411 Preset::ReadOnly => "read_only",
412 Preset::Agent => "agent",
413 Preset::Ci => "ci",
414 Preset::Full => "full",
415 }
416 }
417
418 pub fn label(self) -> &'static str {
419 match self {
420 Preset::ReadOnly => "Read only",
421 Preset::Agent => "Agent",
422 Preset::Ci => "CI",
423 Preset::Full => "Full access",
424 }
425 }
426
427 /// Its scopes; `None` for full access.
428 pub fn scopes(self) -> Option<Vec<Scope>> {
429 let reads = || Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read);
430 match self {
431 Preset::ReadOnly => Some(reads().collect()),
432 Preset::Agent => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents433 // Not the machines work runs on: an agent has no business
434 // knowing a workspace's own runners.
435 let mut scopes: Vec<Scope> = reads().filter(|scope| scope.resource() != Resource::Runners).collect();
API: notifications over REST and MCP, with notifications scopes436 // And answering what needs the person it works for: marking
437 // it done, subscribing, watching.
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step438 scopes.extend([
439 Scope::CodeWrite,
440 Scope::IssuesWrite,
441 Scope::PullRequestsWrite,
442 Scope::AgentsRun,
443 Scope::MemoryWrite,
API: notifications over REST and MCP, with notifications scopes444 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step445 ]);
446 normalize(&mut scopes);
447 Some(scopes)
448 }
449 Preset::Ci => Some(vec![
450 Scope::RepoRead,
451 Scope::CodeRead,
452 Scope::CodeWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member453 Scope::PackagesRead,
454 Scope::PackagesWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step455 Scope::WorkflowsRead,
456 Scope::WorkflowsWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97457 Scope::DeploymentsRead,
458 Scope::DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step459 ]),
460 Preset::Full => None,
461 }
462 }
463}
464
465/// What an OAuth client gets when it asks for nothing in particular: the
466/// agent preset. Never an admin scope.
467pub fn oauth_default() -> Vec<Scope> {
468 Preset::Agent.scopes().unwrap_or_default()
469}
470
471/// Set on a [`crate::User`] resolved from an access token: what the token
472/// may do. Absent on a signed-in session, which may do whatever its person
473/// can.
474#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
475pub struct TokenAccess {
476 /// The token's id, as audit entries and errors name it.
477 #[serde(default)]
478 pub token_id: String,
479 /// Its scopes, as `resource:level`. Absent: full access, everything the
480 /// person (or workspace) can do.
481 #[serde(default, skip_serializing_if = "Option::is_none")]
482 pub scopes: Option<Vec<String>>,
483 /// Made before tokens had scopes: full access until someone narrows it.
484 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
485 pub legacy: bool,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens486 /// The token's name, as its owner gave it, so a log can say which
487 /// token made a request. Absent where whoever resolved it did not say.
488 #[serde(default, skip_serializing_if = "Option::is_none")]
489 pub name: Option<String>,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step490}
491
492impl TokenAccess {
493 /// Full access to everything: the access tokens made before scopes had.
494 pub fn full() -> Self {
495 TokenAccess::default()
496 }
497
498 pub fn is_full(&self) -> bool {
499 self.scopes.is_none()
500 }
501
502 /// The scopes it holds, or `None` for full access.
503 pub fn granted(&self) -> Option<Vec<Scope>> {
504 self.scopes
505 .as_ref()
506 .map(|scopes| scopes.iter().filter_map(|scope| Scope::parse(scope)).collect())
507 }
508
509 pub fn allows(&self, needed: Scope) -> bool {
510 match self.granted() {
511 None => true,
512 Some(granted) => granted.iter().any(|held| held.includes(needed)),
513 }
514 }
515}
516
517/// Every operation of the API and MCP server, with the scope it needs. An
518/// operation in [`NO_SCOPE`] needs none. The API checks that every one of
519/// its operations is in exactly one of the two.
520pub const OPERATIONS: &[(&str, Scope)] = &[
521 // Your account.
522 ("list_emails", Scope::AccountRead),
523 ("add_email", Scope::AccountWrite),
524 ("remove_email", Scope::AccountWrite),
525 ("update_email_settings", Scope::AccountWrite),
526 ("list_invites", Scope::AccountRead),
527 ("create_invite", Scope::AccountWrite),
528 ("revoke_invite", Scope::AccountWrite),
529 ("list_my_repo_invitations", Scope::AccountRead),
530 ("accept_repo_invitation", Scope::AccountWrite),
531 ("decline_repo_invitation", Scope::AccountWrite),
API: pinned projects over REST and MCP532 // Your pinned projects: a preference of your account.
533 ("list_pinned_projects", Scope::AccountRead),
534 ("pin_project", Scope::AccountWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97535 // Your stars: a preference of your account.
536 ("list_starred", Scope::AccountRead),
537 ("check_starred", Scope::AccountRead),
538 ("star_repo", Scope::AccountWrite),
539 ("unstar_repo", Scope::AccountWrite),
API: pinned projects over REST and MCP540 ("unpin_project", Scope::AccountWrite),
541 ("reorder_pinned_projects", Scope::AccountWrite),
API: notifications over REST and MCP, with notifications scopes542 // Your inbox: notifications, subscriptions and watching.
543 ("list_notifications", Scope::NotificationsRead),
544 ("get_notification_thread", Scope::NotificationsRead),
545 ("get_thread_subscription", Scope::NotificationsRead),
546 ("get_repo_subscription", Scope::NotificationsRead),
547 ("list_watched_repos", Scope::NotificationsRead),
548 ("mark_notifications_read", Scope::NotificationsWrite),
549 ("mark_thread_read", Scope::NotificationsWrite),
550 ("mark_thread_done", Scope::NotificationsWrite),
551 ("save_thread", Scope::NotificationsWrite),
552 ("snooze_thread", Scope::NotificationsWrite),
553 ("set_thread_subscription", Scope::NotificationsWrite),
554 ("delete_thread_subscription", Scope::NotificationsWrite),
555 ("set_repo_subscription", Scope::NotificationsWrite),
556 ("delete_repo_subscription", Scope::NotificationsWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step557 // Workspaces, their invites and integrations.
558 ("create_workspace", Scope::WorkspaceAdmin),
559 ("delete_workspace", Scope::WorkspaceAdmin),
Merge branch 'worktree-agent-ad7c6d88d93adc817'560 ("get_workspace", Scope::WorkspaceRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily561 ("update_workspace", Scope::WorkspaceAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step562 ("list_workspace_invites", Scope::WorkspaceRead),
563 ("invite_member", Scope::WorkspaceAdmin),
564 ("revoke_workspace_invite", Scope::WorkspaceAdmin),
565 ("list_integrations", Scope::WorkspaceRead),
566 ("connect_integration", Scope::WorkspaceAdmin),
AI Gateway: OpenAI's format, open models, and your own providers567 ("update_integration", Scope::WorkspaceAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step568 ("disconnect_integration", Scope::WorkspaceAdmin),
569 ("test_integration", Scope::WorkspaceAdmin),
570 ("get_model_routes", Scope::WorkspaceRead),
571 ("set_model_routes", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar572 // Teams: reading them, and managing them. A team's role on a
573 // repository is who has access.
574 ("list_teams", Scope::WorkspaceRead),
575 ("get_team", Scope::WorkspaceRead),
576 ("list_team_members", Scope::WorkspaceRead),
577 ("list_child_teams", Scope::WorkspaceRead),
578 ("list_team_repos", Scope::WorkspaceRead),
579 ("list_user_teams", Scope::WorkspaceRead),
580 ("create_team", Scope::WorkspaceAdmin),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge581 ("list_workspace_rulesets", Scope::WorkspaceRead),
582 ("get_workspace_ruleset", Scope::WorkspaceRead),
583 ("list_workspace_rule_evaluations", Scope::WorkspaceRead),
584 ("create_workspace_ruleset", Scope::WorkspaceAdmin),
585 ("update_workspace_ruleset", Scope::WorkspaceAdmin),
586 ("delete_workspace_ruleset", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar587 ("update_team", Scope::WorkspaceAdmin),
588 ("delete_team", Scope::WorkspaceAdmin),
589 ("set_team_member", Scope::WorkspaceAdmin),
590 ("remove_team_member", Scope::WorkspaceAdmin),
591 ("set_team_review_assignment", Scope::WorkspaceAdmin),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit592 // A workspace's billing: usage, budget, AI credit and invoices.
593 ("get_usage", Scope::BillingRead),
594 ("get_budget", Scope::BillingRead),
595 ("get_ai_credit", Scope::BillingRead),
596 ("list_invoices", Scope::BillingRead),
597 ("get_billing_details", Scope::BillingRead),
598 ("set_budget", Scope::BillingWrite),
599 ("buy_ai_credit", Scope::BillingWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step600 // Repositories.
601 ("list_repos", Scope::RepoRead),
602 ("get_repo", Scope::RepoRead),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97603 // Projects follow their repositories.
604 ("list_projects", Scope::RepoRead),
605 ("get_project", Scope::RepoRead),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step606 ("search", Scope::RepoRead),
607 ("list_events", Scope::RepoRead),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97608 // What the default branch says about a repository, who starred it, and
609 // its releases.
610 ("get_languages", Scope::RepoRead),
611 ("list_contributors", Scope::RepoRead),
612 ("get_license", Scope::RepoRead),
613 ("list_stargazers", Scope::RepoRead),
614 ("list_releases", Scope::RepoRead),
615 ("get_latest_release", Scope::RepoRead),
616 ("get_release_by_tag", Scope::RepoRead),
617 ("get_release", Scope::RepoRead),
618 ("create_release", Scope::RepoWrite),
619 ("update_release", Scope::RepoWrite),
620 ("delete_release", Scope::RepoWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step621 ("list_labels", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar622 ("list_milestones", Scope::RepoRead),
623 ("get_milestone", Scope::RepoRead),
624 ("create_label", Scope::IssuesWrite),
625 ("update_label", Scope::IssuesWrite),
626 ("delete_label", Scope::IssuesWrite),
627 ("add_default_labels", Scope::IssuesWrite),
628 ("create_milestone", Scope::IssuesWrite),
629 ("update_milestone", Scope::IssuesWrite),
630 ("delete_milestone", Scope::IssuesWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step631 ("get_repo_settings", Scope::RepoRead),
Fast pages, required checks on the branch, self-hosted runners, honest incidents632 ("list_check_names", Scope::RepoRead),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step633 ("list_deleted_repos", Scope::RepoRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily634 ("list_security_alerts", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar635 ("get_codeowners_errors", Scope::RepoRead),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step636 ("create_repo", Scope::RepoWrite),
637 ("update_repo", Scope::RepoWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97638 ("update_project", Scope::RepoWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step639 ("update_repo_settings", Scope::RepoWrite),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge640 // Rulesets: reading them is reading the repository; changing them
641 // changes what everyone, agents included, may do, so it is admin.
642 ("list_repo_rulesets", Scope::RepoRead),
643 ("get_repo_ruleset", Scope::RepoRead),
644 ("get_branch_rules", Scope::RepoRead),
645 ("list_rule_evaluations", Scope::RepoRead),
646 ("create_repo_ruleset", Scope::RepoAdmin),
647 ("update_repo_ruleset", Scope::RepoAdmin),
648 ("delete_repo_ruleset", Scope::RepoAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step649 ("rename_branch", Scope::RepoWrite),
650 ("rename_repo", Scope::RepoAdmin),
651 ("transfer_repo", Scope::RepoAdmin),
652 ("archive_repo", Scope::RepoAdmin),
653 ("unarchive_repo", Scope::RepoAdmin),
654 ("set_repo_visibility", Scope::RepoAdmin),
655 ("delete_repo", Scope::RepoAdmin),
656 ("restore_repo", Scope::RepoAdmin),
657 ("purge_repo", Scope::RepoAdmin),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily658 // A dismissed secret is let through push protection.
659 ("dismiss_security_alert", Scope::RepoAdmin),
660 ("reopen_security_alert", Scope::RepoAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar661 // The security suite: alerts, push protection, patterns, code
662 // scanning, the supply chain and settings.
663 ("list_secret_scanning_alerts", Scope::SecurityRead),
664 ("get_secret_scanning_alert", Scope::SecurityRead),
665 ("list_secret_scanning_locations", Scope::SecurityRead),
666 ("list_bypass_requests", Scope::SecurityRead),
667 ("list_custom_patterns", Scope::SecurityRead),
668 ("list_code_scanning_alerts", Scope::SecurityRead),
669 ("get_code_scanning_alert", Scope::SecurityRead),
670 ("list_code_scanning_analyses", Scope::SecurityRead),
671 ("get_sarif_upload", Scope::SecurityRead),
672 ("list_vulnerability_alerts", Scope::SecurityRead),
673 ("get_vulnerability_alert", Scope::SecurityRead),
674 ("get_dependency_graph", Scope::SecurityRead),
675 ("get_sbom", Scope::SecurityRead),
676 ("compare_dependencies", Scope::SecurityRead),
677 ("get_security_settings", Scope::SecurityRead),
678 ("get_workspace_security_settings", Scope::SecurityRead),
679 ("get_security_overview", Scope::SecurityRead),
680 ("update_secret_scanning_alert", Scope::SecurityWrite),
681 ("bypass_push_protection", Scope::SecurityWrite),
682 ("check_secret_validity", Scope::SecurityWrite),
683 ("review_bypass_request", Scope::SecurityWrite),
684 ("create_custom_pattern", Scope::SecurityWrite),
685 ("update_custom_pattern", Scope::SecurityWrite),
686 ("delete_custom_pattern", Scope::SecurityWrite),
687 ("dry_run_custom_pattern", Scope::SecurityWrite),
688 ("update_code_scanning_alert", Scope::SecurityWrite),
689 ("upload_sarif", Scope::SecurityWrite),
690 ("update_vulnerability_alert", Scope::SecurityWrite),
691 ("fix_security_alert", Scope::SecurityWrite),
692 ("update_security_settings", Scope::SecurityWrite),
693 ("update_workspace_security_settings", Scope::SecurityWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step694 // Issues and plans.
695 ("list_issues", Scope::IssuesRead),
696 ("get_issue", Scope::IssuesRead),
697 ("get_plan", Scope::IssuesRead),
698 ("create_issue", Scope::IssuesWrite),
699 ("update_issue", Scope::IssuesWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar700 ("list_issue_labels", Scope::IssuesRead),
701 ("add_issue_labels", Scope::IssuesWrite),
702 ("set_issue_labels", Scope::IssuesWrite),
703 ("remove_issue_labels", Scope::IssuesWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step704 ("close_issue", Scope::IssuesWrite),
705 ("reopen_issue", Scope::IssuesWrite),
706 ("add_comment", Scope::IssuesWrite),
707 ("import_issue", Scope::IssuesWrite),
708 ("apply_plan", Scope::IssuesWrite),
709 // Pull requests.
710 ("list_pull_requests", Scope::PullRequestsRead),
711 ("get_pull_request", Scope::PullRequestsRead),
712 ("get_pull_request_changes", Scope::PullRequestsRead),
713 ("read_session", Scope::PullRequestsRead),
714 ("get_merge_queue", Scope::PullRequestsRead),
715 ("create_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar716 ("update_pull_request", Scope::PullRequestsWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step717 ("record_session", Scope::PullRequestsWrite),
718 ("mark_pull_request_ready", Scope::PullRequestsWrite),
719 ("close_pull_request", Scope::PullRequestsWrite),
720 ("review_pull_request", Scope::PullRequestsWrite),
721 ("merge_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar722 ("request_reviewers", Scope::PullRequestsWrite),
723 ("remove_requested_reviewers", Scope::PullRequestsWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step724 // g1t's agents.
725 ("assign_issue", Scope::AgentsRun),
726 ("delegate", Scope::AgentsRun),
727 ("plan_work", Scope::AgentsRun),
728 ("message_agent", Scope::AgentsRun),
729 ("answer_message", Scope::AgentsRun),
730 ("take_messages", Scope::AgentsRun),
731 // Workflows.
732 ("list_workflows", Scope::WorkflowsRead),
733 ("list_workflow_runs", Scope::WorkflowsRead),
734 ("get_workflow_run", Scope::WorkflowsRead),
735 ("get_job_logs", Scope::WorkflowsRead),
736 ("dispatch_workflow", Scope::WorkflowsWrite),
737 ("cancel_workflow_run", Scope::WorkflowsWrite),
738 ("rerun_workflow_run", Scope::WorkflowsWrite),
739 ("update_workflow", Scope::WorkflowsWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97740 // Deployments, wherever they run: reading them, and reporting them.
741 ("list_deployments", Scope::DeploymentsRead),
742 ("get_deployment", Scope::DeploymentsRead),
743 ("list_deployment_statuses", Scope::DeploymentsRead),
744 ("list_environments", Scope::DeploymentsRead),
745 ("get_environment", Scope::DeploymentsRead),
746 ("create_deployment", Scope::DeploymentsWrite),
747 ("create_deployment_status", Scope::DeploymentsWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step748 // Memory and the context hub.
749 ("recall", Scope::MemoryRead),
750 ("search_context", Scope::MemoryRead),
751 ("get_entity", Scope::MemoryRead),
752 ("get_context", Scope::MemoryRead),
753 ("remember", Scope::MemoryWrite),
754 // Who has access.
755 ("list_collaborators", Scope::AccessRead),
756 ("get_collaborator_permission", Scope::AccessRead),
757 ("list_repo_invitations", Scope::AccessRead),
758 ("list_outside_collaborators", Scope::AccessRead),
759 ("add_collaborator", Scope::AccessAdmin),
760 ("update_collaborator", Scope::AccessAdmin),
761 ("remove_collaborator", Scope::AccessAdmin),
762 ("revoke_repo_invitation", Scope::AccessAdmin),
763 ("set_base_permission", Scope::AccessAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar764 ("set_team_repo", Scope::AccessAdmin),
765 ("remove_team_repo", Scope::AccessAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step766 // Webhooks.
767 ("list_webhooks", Scope::WebhooksRead),
768 ("list_webhook_deliveries", Scope::WebhooksRead),
769 ("create_webhook", Scope::WebhooksAdmin),
770 ("update_webhook", Scope::WebhooksAdmin),
771 ("delete_webhook", Scope::WebhooksAdmin),
772 ("ping_webhook", Scope::WebhooksAdmin),
773 ("redeliver_webhook", Scope::WebhooksAdmin),
774 // Secrets and variables.
775 ("list_actions_secrets", Scope::SecretsRead),
776 ("list_actions_variables", Scope::SecretsRead),
777 ("set_actions_secret", Scope::SecretsAdmin),
778 ("delete_actions_secret", Scope::SecretsAdmin),
779 ("set_actions_variable", Scope::SecretsAdmin),
780 ("delete_actions_variable", Scope::SecretsAdmin),
Fast pages, required checks on the branch, self-hosted runners, honest incidents781 // Self-hosted runners.
782 ("list_runners", Scope::RunnersRead),
783 ("list_runner_groups", Scope::RunnersRead),
784 ("get_runner_settings", Scope::RunnersRead),
785 ("create_runner_registration_token", Scope::RunnersAdmin),
786 ("remove_runner", Scope::RunnersAdmin),
787 ("create_runner_group", Scope::RunnersAdmin),
788 ("update_runner_group", Scope::RunnersAdmin),
789 ("delete_runner_group", Scope::RunnersAdmin),
790 ("update_runner_settings", Scope::RunnersAdmin),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens791 // The AI Gateway. Sending a request to a model needs `models:write`,
792 // checked by the model proxy at models.g1t.sh, not here.
793 ("list_gateway_requests", Scope::ModelsRead),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step794];
795
796/// Operations any token may use: saying who it is.
797pub const NO_SCOPE: &[&str] = &["whoami"];
798
799/// The scope `operation` needs. `None` for one in [`NO_SCOPE`]; an
800/// operation in neither list needs full access.
801pub fn scope_for(operation: &str) -> Option<Scope> {
802 OPERATIONS
803 .iter()
804 .find(|(name, _)| *name == operation)
805 .map(|(_, scope)| *scope)
806}
807
808/// What a token needs for `operation` with this input beyond its own
809/// scope: starting agents from an operation that can, and making a
810/// repository public or private.
811pub fn extra_scopes(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
812 let mut extra = Vec::new();
813 let assigns = input["assign"].as_bool() == Some(true)
814 || input["agent"].as_bool() == Some(true)
815 || input["assign_agent"].as_bool() == Some(true);
816 if assigns && matches!(operation, "apply_plan" | "import_issue" | "create_issue") {
817 extra.push(Scope::AgentsRun);
818 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar819 // Fixing an alert opens an issue and puts g1t on it.
820 if operation == "fix_security_alert" {
821 extra.extend([Scope::IssuesWrite, Scope::AgentsRun]);
822 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step823 // Opening the issue an agent is put on.
824 if operation == "delegate" {
825 extra.push(Scope::IssuesWrite);
826 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily827 // A workspace's base permission is who has access.
828 if operation == "update_workspace" && input.get("base_permission").is_some_and(|v| !v.is_null()) {
829 extra.push(Scope::AccessAdmin);
830 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step831 if operation == "update_repo" && (input.get("private").is_some_and(|v| !v.is_null()) || input.get("default_branch").is_some_and(|v| !v.is_null())) {
832 extra.push(Scope::RepoAdmin);
833 }
834 extra
835}
836
837/// The scopes a call needs, its own first.
838pub fn needed(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
839 scope_for(operation)
840 .into_iter()
841 .chain(extra_scopes(operation, input))
842 .collect()
843}
844
845/// Whether `access` may use `operation` with `input`. The person's (or
846/// workspace's) role is checked after this, by the service that owns what
847/// was asked about.
848pub fn decide(access: &TokenAccess, operation: &str, input: &serde_json::Value) -> Decision {
849 let rule = if access.legacy { "token:legacy" } else { "token:scope" };
850 if access.scopes.is_some() {
851 let known = NO_SCOPE.contains(&operation) || scope_for(operation).is_some();
852 if !known {
853 return Decision::deny("token:scope", format!("This access token cannot use {operation}: it needs full access."));
854 }
855 if let Some(missing) = needed(operation, input).into_iter().find(|scope| !access.allows(*scope)) {
856 return Decision::deny(
857 "token:scope",
858 format!("This access token needs the {} scope to use {operation}.", missing.as_str()),
859 );
860 }
861 }
862 Decision::allow(rule)
863}
864
865/// Whether a token may clone or fetch (`write` false), or push to (`write`
866/// true), a repository with git. `public` is whether anyone may read it,
867/// which needs no scope.
868pub fn decide_git(access: &TokenAccess, write: bool, public: bool) -> Decision {
869 let needed = if write { Scope::CodeWrite } else { Scope::CodeRead };
870 if !access.allows(needed) && (write || !public) {
871 return Decision::deny(
872 "token:scope",
873 format!("This access token needs the {} scope to {} with git.", needed.as_str(), if write { "push" } else { "clone or fetch a private repository" }),
874 );
875 }
876 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
877}
878
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member879/// Whether a token may pull (`Level::Read`), push or publish
880/// (`Level::Write`), or delete (`Level::Delete`) packages. `public` is
881/// whether anyone may pull the package, which needs no scope.
882pub fn decide_packages(access: &TokenAccess, level: Level, public: bool) -> Decision {
883 let (needed, doing) = match level {
884 Level::Read => (Scope::PackagesRead, "pull a private package"),
885 Level::Delete | Level::Admin => (Scope::PackagesDelete, "delete packages"),
886 Level::Write | Level::Run => (Scope::PackagesWrite, "push or publish packages"),
887 };
888 if !access.allows(needed) && !(level == Level::Read && public) {
889 return Decision::deny(
890 "token:scope",
891 format!("This access token needs the {} scope to {doing}.", needed.as_str()),
892 );
893 }
894 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
895}
896
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step897#[cfg(test)]
898mod tests {
899 use super::*;
900 use serde_json::json;
901
902 fn token(scopes: &[Scope]) -> TokenAccess {
903 TokenAccess {
904 token_id: "tok_1".to_owned(),
905 scopes: Some(scopes.iter().map(|scope| scope.as_str().to_owned()).collect()),
906 legacy: false,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens907 name: None,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step908 }
909 }
910
911 #[test]
912 fn every_scope_reads_back_and_belongs_to_a_resource() {
913 for scope in Scope::ALL {
914 assert_eq!(Scope::parse(scope.as_str()), Some(scope));
915 assert!(scope.as_str().starts_with(scope.resource().as_str()));
916 assert!(scope.includes(scope));
917 }
918 assert_eq!(Scope::parse(" Issues:Write "), Some(Scope::IssuesWrite));
919 assert_eq!(Scope::parse("issues"), None);
920 }
921
922 #[test]
923 fn a_higher_level_includes_the_lower_ones_of_its_resource_only() {
924 assert!(Scope::RepoAdmin.includes(Scope::RepoRead));
925 assert!(Scope::RepoAdmin.includes(Scope::RepoWrite));
926 assert!(Scope::IssuesWrite.includes(Scope::IssuesRead));
927 assert!(!Scope::IssuesRead.includes(Scope::IssuesWrite));
928 assert!(!Scope::RepoAdmin.includes(Scope::CodeWrite));
929 assert!(!Scope::PullRequestsWrite.includes(Scope::IssuesWrite));
930 }
931
932 #[test]
933 fn operations_are_listed_once_and_never_also_free() {
934 let mut seen = std::collections::HashSet::new();
935 for (name, _) in OPERATIONS {
936 assert!(seen.insert(*name), "{name} twice");
937 assert!(!NO_SCOPE.contains(name), "{name}");
938 }
939 }
940
941 #[test]
942 fn scopes_are_parsed_from_oauth_text_leaving_out_unknown_ones() {
943 assert_eq!(
944 parse_scopes("issues:write repo:read,bogus:thing issues:write"),
945 vec![Scope::RepoRead, Scope::IssuesWrite]
946 );
947 assert_eq!(scopes_text(&[Scope::RepoRead, Scope::IssuesWrite]), "repo:read issues:write");
948 }
949
950 #[test]
951 fn the_oauth_default_is_the_agent_preset_and_never_admin() {
952 let scopes = oauth_default();
953 assert!(scopes.contains(&Scope::IssuesWrite));
954 assert!(scopes.contains(&Scope::PullRequestsWrite));
955 assert!(scopes.contains(&Scope::AgentsRun));
956 assert!(scopes.iter().all(|scope| !scope.dangerous()), "{scopes:?}");
957 for read in Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents958 // Every read but the machines work runs on.
959 assert_eq!(scopes.contains(&read), read != Scope::RunnersRead, "{read:?}");
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step960 }
961 assert!(Preset::ReadOnly.scopes().unwrap().iter().all(|scope| scope.level() == Level::Read));
962 assert_eq!(Preset::Full.scopes(), None);
963 }
964
965 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit966 fn billing_is_read_by_presets_and_changed_by_none_but_full_access() {
967 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::BillingRead));
968 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
969 assert!(!preset.scopes().unwrap().contains(&Scope::BillingWrite), "{}", preset.as_str());
970 }
971 assert_eq!(scope_for("set_budget"), Some(Scope::BillingWrite));
972 assert_eq!(scope_for("buy_ai_credit"), Some(Scope::BillingWrite));
973 assert_eq!(scope_for("get_usage"), Some(Scope::BillingRead));
974 let reader = token(&[Scope::BillingRead]);
975 assert!(decide(&reader, "list_invoices", &json!({})).allowed);
976 assert!(decide(&reader, "set_budget", &json!({})).reason.unwrap().contains("billing:write"));
977 }
978
979 #[test]
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens980 fn the_ai_gateway_spends_only_with_models_write_which_no_preset_gives() {
981 // Reading the log is a read like any other.
982 assert_eq!(scope_for("list_gateway_requests"), Some(Scope::ModelsRead));
983 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::ModelsRead));
984 // Sending requests spends the workspace's AI credit: chosen on purpose.
985 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
986 assert!(!preset.scopes().unwrap().contains(&Scope::ModelsWrite), "{}", preset.as_str());
987 }
988 assert!(Scope::ModelsWrite.includes(Scope::ModelsRead));
989 assert!(!Scope::ModelsWrite.dangerous());
990 assert!(token(&[Scope::ModelsWrite]).allows(Scope::ModelsWrite));
991 assert!(!token(&[Scope::BillingWrite]).allows(Scope::ModelsWrite));
992 assert!(TokenAccess::full().allows(Scope::ModelsWrite));
993 }
994
995 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step996 fn a_legacy_token_can_do_everything() {
997 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
998 for (operation, _) in OPERATIONS {
999 assert!(decide(&legacy, operation, &json!({})).allowed, "{operation}");
1000 }
1001 assert_eq!(decide(&legacy, "delete_repo", &json!({})).rule, "token:legacy");
1002 }
1003
1004 #[test]
1005 fn a_missing_scope_is_named() {
1006 let read = token(&[Scope::IssuesRead]);
1007 assert!(decide(&read, "get_issue", &json!({})).allowed);
1008 assert!(decide(&read, "whoami", &json!({})).allowed);
1009 let refused = decide(&read, "create_issue", &json!({}));
1010 assert!(!refused.allowed);
1011 assert_eq!(refused.reason.as_deref(), Some("This access token needs the issues:write scope to use create_issue."));
1012 // An operation the table does not know needs full access.
1013 assert!(!decide(&read, "something_new", &json!({})).allowed);
1014 }
1015
1016 #[test]
1017 fn starting_agents_from_another_operation_needs_agents_run() {
1018 let writer = token(&[Scope::IssuesWrite]);
1019 assert!(decide(&writer, "apply_plan", &json!({})).allowed);
1020 let refused = decide(&writer, "apply_plan", &json!({ "assign": true }));
1021 assert!(refused.reason.unwrap().contains("agents:run"));
1022 let maintainer = token(&[Scope::RepoWrite]);
1023 assert!(decide(&maintainer, "update_repo", &json!({ "description": "x" })).allowed);
1024 assert!(!decide(&maintainer, "update_repo", &json!({ "private": true })).allowed);
1025 }
1026
1027 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1028 fn a_workspaces_base_permission_needs_access_admin_too() {
1029 let admin = token(&[Scope::WorkspaceAdmin]);
1030 assert!(decide(&admin, "update_workspace", &json!({ "name": "Acme" })).allowed);
1031 let refused = decide(&admin, "update_workspace", &json!({ "name": "Acme", "base_permission": "read" }));
1032 assert!(refused.reason.unwrap().contains("access:admin"));
1033 let both = token(&[Scope::WorkspaceAdmin, Scope::AccessAdmin]);
1034 assert!(decide(&both, "update_workspace", &json!({ "base_permission": "read" })).allowed);
1035 assert!(!decide(&token(&[Scope::WorkspaceRead]), "update_workspace", &json!({ "name": "Acme" })).allowed);
1036 }
1037
1038 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1039 fn delegating_needs_both_agents_and_issues() {
1040 let agents = token(&[Scope::AgentsRun]);
1041 assert!(decide(&agents, "delegate", &json!({})).reason.unwrap().contains("issues:write"));
1042 let both = token(&[Scope::AgentsRun, Scope::IssuesWrite]);
1043 assert!(decide(&both, "delegate", &json!({})).allowed);
1044 }
1045
1046 #[test]
1047 fn git_push_needs_code_write_and_private_reads_need_code_read() {
1048 let reader = token(&[Scope::CodeRead]);
1049 assert!(decide_git(&reader, false, false).allowed);
1050 let refused = decide_git(&reader, true, false);
1051 assert!(!refused.allowed);
1052 assert!(refused.reason.unwrap().contains("code:write"));
1053 let issues = token(&[Scope::IssuesWrite]);
1054 assert!(!decide_git(&issues, false, false).allowed);
1055 assert!(decide_git(&issues, false, true).allowed, "public code needs no scope");
1056 assert!(!decide_git(&issues, true, true).allowed, "pushing to public code still needs code:write");
1057 let writer = token(&[Scope::CodeWrite]);
1058 assert!(decide_git(&writer, true, false).allowed);
1059 assert!(decide_git(&writer, false, false).allowed, "code:write includes code:read");
1060 assert!(decide_git(&TokenAccess::full(), true, false).allowed);
1061 }
1062
1063 #[test]
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1064 fn packages_need_their_own_scopes_and_public_pulls_none() {
1065 let reader = token(&[Scope::PackagesRead]);
1066 assert!(decide_packages(&reader, Level::Read, false).allowed);
1067 assert!(!decide_packages(&reader, Level::Write, false).allowed);
1068 let code = token(&[Scope::CodeWrite]);
1069 assert!(!decide_packages(&code, Level::Read, false).allowed, "code scopes are not package scopes");
1070 assert!(decide_packages(&code, Level::Read, true).allowed, "public packages pull with any token");
1071 let writer = token(&[Scope::PackagesWrite]);
1072 assert!(decide_packages(&writer, Level::Write, false).allowed);
1073 assert!(decide_packages(&writer, Level::Read, false).allowed, "packages:write includes packages:read");
1074 let refused = decide_packages(&writer, Level::Delete, false);
1075 assert!(refused.reason.unwrap().contains("packages:delete"));
1076 assert!(decide_packages(&token(&[Scope::PackagesDelete]), Level::Write, false).allowed);
1077 assert!(Scope::PackagesDelete.dangerous());
1078 // Tokens made before these scopes, and full-access ones, keep working.
1079 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
1080 assert!(decide_packages(&legacy, Level::Delete, false).allowed);
1081 assert!(decide_packages(&TokenAccess::full(), Level::Write, false).allowed);
1082 }
1083
1084 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1085 fn token_access_travels_as_json() {
1086 let access = token(&[Scope::IssuesRead]);
1087 let wire = serde_json::to_value(&access).unwrap();
1088 assert_eq!(wire["scopes"], json!(["issues:read"]));
1089 assert!(wire.get("resources").is_none());
1090 let back: TokenAccess = serde_json::from_value(wire).unwrap();
1091 assert_eq!(back, access);
1092 let full: TokenAccess = serde_json::from_value(json!({})).unwrap();
1093 assert!(full.is_full());
1094 // A reach written by an older version is ignored: a token reaches
1095 // whatever its owner can.
1096 let older: TokenAccess = serde_json::from_value(json!({
1097 "token_id": "tok_1",
1098 "scopes": ["issues:read"],
1099 "resources": { "kind": "repositories", "repositories": ["acme/rocket"] },
1100 }))
1101 .unwrap();
1102 assert_eq!(older, access);
1103 }
1104
1105 /// The site's copy of the table, `packages/contracts/src/scopes.ts`,
1106 /// lists the same scopes in the same order, the same operations with
1107 /// the same scopes, and the same presets.
1108 #[test]
1109 fn the_typescript_mirror_has_the_same_table() {
1110 let ts = include_str!("../../../packages/contracts/src/scopes.ts");
1111 let section = |start: &str| {
1112 ts.split_once(start)
1113 .and_then(|(_, rest)| rest.split_once("] as const"))
1114 .map(|(table, _)| table)
1115 .unwrap_or_else(|| panic!("{start} in scopes.ts"))
1116 };
1117 let scopes: Vec<&str> = section("export const SCOPES = [")
1118 .lines()
1119 .filter_map(|line| line.split_once("scope: \"").and_then(|(_, rest)| rest.split_once('"')).map(|(scope, _)| scope))
1120 .collect();
1121 let expected: Vec<&str> = Scope::ALL.iter().map(|scope| scope.as_str()).collect();
1122 assert_eq!(scopes, expected);
1123 let operations: Vec<(String, String)> = section("export const OPERATION_SCOPES = [")
1124 .lines()
1125 .filter_map(|line| {
1126 let mut quoted = line.split('"').skip(1).step_by(2);
1127 Some((quoted.next()?.to_owned(), quoted.next()?.to_owned()))
1128 })
1129 .collect();
1130 let expected: Vec<(String, String)> = OPERATIONS
1131 .iter()
1132 .map(|(name, scope)| ((*name).to_owned(), scope.as_str().to_owned()))
1133 .collect();
1134 assert_eq!(operations, expected);
1135 for preset in Preset::ALL {
1136 let list = section(&format!("{}: [", preset.as_str()));
1137 let mirrored: Vec<&str> = list
1138 .split(',')
1139 .map(|item| item.trim().trim_matches('"'))
1140 .filter(|item| !item.is_empty())
1141 .collect();
1142 let expected: Vec<&str> = preset
1143 .scopes()
1144 .map(|scopes| scopes.iter().map(|scope| scope.as_str()).collect())
1145 .unwrap_or_else(|| vec!["*"]);
1146 assert_eq!(mirrored, expected, "{}", preset.as_str());
1147 }
1148 }
1149}

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