Skip to content

g1t/crates/contracts/src/scopes.rs

1,068 lines45,625 bytesCodeBlame

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,
37 Memory,
38 Access,
39 Webhooks,
40 Secrets,
Fast pages, required checks on the branch, self-hosted runners, honest incidents41 Runners,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step42}
43
44impl Resource {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit45 pub const ALL: [Resource; 17] = [
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step46 Resource::Repo,
47 Resource::Code,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar48 Resource::Security,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member49 Resource::Packages,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step50 Resource::Issues,
51 Resource::PullRequests,
52 Resource::Agents,
53 Resource::Workflows,
54 Resource::Memory,
55 Resource::Account,
API: notifications over REST and MCP, with notifications scopes56 Resource::Notifications,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step57 Resource::Workspace,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit58 Resource::Billing,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step59 Resource::Access,
60 Resource::Webhooks,
61 Resource::Secrets,
Fast pages, required checks on the branch, self-hosted runners, honest incidents62 Resource::Runners,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step63 ];
64
65 pub fn as_str(self) -> &'static str {
66 match self {
67 Resource::Account => "account",
API: notifications over REST and MCP, with notifications scopes68 Resource::Notifications => "notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step69 Resource::Workspace => "workspace",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit70 Resource::Billing => "billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step71 Resource::Repo => "repo",
72 Resource::Code => "code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar73 Resource::Security => "security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member74 Resource::Packages => "packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step75 Resource::Issues => "issues",
76 Resource::PullRequests => "pull_requests",
77 Resource::Agents => "agents",
78 Resource::Workflows => "workflows",
79 Resource::Memory => "memory",
80 Resource::Access => "access",
81 Resource::Webhooks => "webhooks",
82 Resource::Secrets => "secrets",
Fast pages, required checks on the branch, self-hosted runners, honest incidents83 Resource::Runners => "runners",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step84 }
85 }
86
87 /// Its name, for people.
88 pub fn label(self) -> &'static str {
89 match self {
90 Resource::Account => "Your account",
API: notifications over REST and MCP, with notifications scopes91 Resource::Notifications => "Notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step92 Resource::Workspace => "Workspaces",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit93 Resource::Billing => "Billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step94 Resource::Repo => "Repositories",
95 Resource::Code => "Code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar96 Resource::Security => "Security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member97 Resource::Packages => "Packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step98 Resource::Issues => "Issues",
99 Resource::PullRequests => "Pull requests",
100 Resource::Agents => "g1t agents",
101 Resource::Workflows => "Workflows",
102 Resource::Memory => "Memory and context",
103 Resource::Access => "Who has access",
104 Resource::Webhooks => "Webhooks",
105 Resource::Secrets => "Secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents106 Resource::Runners => "Self-hosted runners",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step107 }
108 }
109}
110
111/// How much of a resource.
112#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
113pub enum Level {
114 Read,
115 Write,
116 /// Starting g1t's agents, which spends the workspace's money.
117 Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member118 /// Deleting what cannot be brought back, such as a package's versions.
119 Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step120 Admin,
121}
122
123impl Level {
124 pub fn as_str(self) -> &'static str {
125 match self {
126 Level::Read => "read",
127 Level::Write => "write",
128 Level::Run => "run",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member129 Level::Delete => "delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step130 Level::Admin => "admin",
131 }
132 }
133}
134
135/// One scope. Its text form, `resource:level`, is what tokens store, OAuth
136/// clients ask for, and errors name.
137#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
138pub enum Scope {
139 RepoRead,
140 RepoWrite,
141 RepoAdmin,
142 CodeRead,
143 CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar144 SecurityRead,
145 SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member146 PackagesRead,
147 PackagesWrite,
148 PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step149 IssuesRead,
150 IssuesWrite,
151 PullRequestsRead,
152 PullRequestsWrite,
153 AgentsRun,
154 WorkflowsRead,
155 WorkflowsWrite,
156 MemoryRead,
157 MemoryWrite,
158 AccountRead,
159 AccountWrite,
API: notifications over REST and MCP, with notifications scopes160 NotificationsRead,
161 NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step162 WorkspaceRead,
163 WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit164 BillingRead,
165 BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step166 AccessRead,
167 AccessAdmin,
168 WebhooksRead,
169 WebhooksAdmin,
170 SecretsRead,
171 SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents172 RunnersRead,
173 RunnersAdmin,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step174}
175
176impl Scope {
177 /// Every scope, grouped by resource, least first.
Usage, Billing settings and prepaid AI credit; fixes from the UX audit178 pub const ALL: [Scope; 35] = [
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step179 Scope::RepoRead,
180 Scope::RepoWrite,
181 Scope::RepoAdmin,
182 Scope::CodeRead,
183 Scope::CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar184 Scope::SecurityRead,
185 Scope::SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member186 Scope::PackagesRead,
187 Scope::PackagesWrite,
188 Scope::PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step189 Scope::IssuesRead,
190 Scope::IssuesWrite,
191 Scope::PullRequestsRead,
192 Scope::PullRequestsWrite,
193 Scope::AgentsRun,
194 Scope::WorkflowsRead,
195 Scope::WorkflowsWrite,
196 Scope::MemoryRead,
197 Scope::MemoryWrite,
198 Scope::AccountRead,
199 Scope::AccountWrite,
API: notifications over REST and MCP, with notifications scopes200 Scope::NotificationsRead,
201 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step202 Scope::WorkspaceRead,
203 Scope::WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit204 Scope::BillingRead,
205 Scope::BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step206 Scope::AccessRead,
207 Scope::AccessAdmin,
208 Scope::WebhooksRead,
209 Scope::WebhooksAdmin,
210 Scope::SecretsRead,
211 Scope::SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents212 Scope::RunnersRead,
213 Scope::RunnersAdmin,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step214 ];
215
216 pub fn as_str(self) -> &'static str {
217 match self {
218 Scope::RepoRead => "repo:read",
219 Scope::RepoWrite => "repo:write",
220 Scope::RepoAdmin => "repo:admin",
221 Scope::CodeRead => "code:read",
222 Scope::CodeWrite => "code:write",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar223 Scope::SecurityRead => "security:read",
224 Scope::SecurityWrite => "security:write",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member225 Scope::PackagesRead => "packages:read",
226 Scope::PackagesWrite => "packages:write",
227 Scope::PackagesDelete => "packages:delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step228 Scope::IssuesRead => "issues:read",
229 Scope::IssuesWrite => "issues:write",
230 Scope::PullRequestsRead => "pull_requests:read",
231 Scope::PullRequestsWrite => "pull_requests:write",
232 Scope::AgentsRun => "agents:run",
233 Scope::WorkflowsRead => "workflows:read",
234 Scope::WorkflowsWrite => "workflows:write",
235 Scope::MemoryRead => "memory:read",
236 Scope::MemoryWrite => "memory:write",
237 Scope::AccountRead => "account:read",
238 Scope::AccountWrite => "account:write",
API: notifications over REST and MCP, with notifications scopes239 Scope::NotificationsRead => "notifications:read",
240 Scope::NotificationsWrite => "notifications:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step241 Scope::WorkspaceRead => "workspace:read",
242 Scope::WorkspaceAdmin => "workspace:admin",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit243 Scope::BillingRead => "billing:read",
244 Scope::BillingWrite => "billing:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step245 Scope::AccessRead => "access:read",
246 Scope::AccessAdmin => "access:admin",
247 Scope::WebhooksRead => "webhooks:read",
248 Scope::WebhooksAdmin => "webhooks:admin",
249 Scope::SecretsRead => "secrets:read",
250 Scope::SecretsAdmin => "secrets:admin",
Fast pages, required checks on the branch, self-hosted runners, honest incidents251 Scope::RunnersRead => "runners:read",
252 Scope::RunnersAdmin => "runners:admin",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step253 }
254 }
255
256 pub fn parse(text: &str) -> Option<Scope> {
257 let text = text.trim().to_ascii_lowercase();
258 Scope::ALL.into_iter().find(|scope| scope.as_str() == text)
259 }
260
261 pub fn resource(self) -> Resource {
262 let name = self.as_str().split_once(':').map_or("", |(resource, _)| resource);
263 Resource::ALL
264 .into_iter()
265 .find(|resource| resource.as_str() == name)
266 .unwrap_or(Resource::Account)
267 }
268
269 pub fn level(self) -> Level {
270 match self.as_str().rsplit_once(':').map_or("", |(_, level)| level) {
271 "write" => Level::Write,
272 "run" => Level::Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member273 "delete" => Level::Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step274 "admin" => Level::Admin,
275 _ => Level::Read,
276 }
277 }
278
279 /// Whether holding `self` gives `other`: the same resource, at the same
280 /// level or a lower one.
281 pub fn includes(self, other: Scope) -> bool {
282 self.resource() == other.resource() && self.level() >= other.level()
283 }
284
285 /// Changes that are hard or impossible to undo, or that decide who can
286 /// reach what. Shown behind a warning wherever scopes are chosen.
287 pub fn dangerous(self) -> bool {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member288 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 step289 }
290
291 /// What it lets a token do, in plain words.
292 pub fn describe(self) -> &'static str {
293 match self {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily294 Scope::RepoRead => "See repositories, their settings, labels, timelines and security alerts, and search",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step295 Scope::RepoWrite => "Create repositories, rename branches and change how pull requests merge",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge296 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 step297 Scope::CodeRead => "Clone and fetch private repositories with git",
298 Scope::CodeWrite => "Push commits with git",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar299 Scope::SecurityRead => "See secret scanning, code scanning and vulnerability alerts, custom patterns, the dependency graph and SBOM, and security settings",
300 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 member301 Scope::PackagesRead => "Pull container images and install private packages",
302 Scope::PackagesWrite => "Push container images and publish packages",
303 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 step304 Scope::IssuesRead => "Read issues, comments and plans",
305 Scope::IssuesWrite => "Open, edit, close and comment on issues",
306 Scope::PullRequestsRead => "Read pull requests, their changes, sessions and merge queues",
307 Scope::PullRequestsWrite => "Open, review, close and merge pull requests",
308 Scope::AgentsRun => "Put g1t agents to work and message them, which uses the workspace's money",
309 Scope::WorkflowsRead => "Read workflows, runs and logs",
310 Scope::WorkflowsWrite => "Run, cancel, rerun and turn workflows on or off",
311 Scope::MemoryRead => "Recall memory and search the workspace's context",
312 Scope::MemoryWrite => "Save memory for the next agent",
API: pinned projects over REST and MCP313 Scope::AccountRead => "Read your email addresses, invites, invitations and pinned projects",
314 Scope::AccountWrite => "Change your email addresses, make invites, answer invitations and pin projects",
API: notifications over REST and MCP, with notifications scopes315 Scope::NotificationsRead => "See your inbox, its threads, and what you subscribe to and watch",
316 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 merge317 Scope::WorkspaceRead => "Read workspace settings, invites, integrations, model routes, teams and rulesets",
318 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 audit319 Scope::BillingRead => "See a workspace's usage, budget, AI credit and invoices",
320 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 step321 Scope::AccessRead => "See who has access to repositories",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar322 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 step323 Scope::WebhooksRead => "See webhooks and their deliveries",
324 Scope::WebhooksAdmin => "Create, change and delete webhooks",
325 Scope::SecretsRead => "List secrets (never their values) and read variables",
326 Scope::SecretsAdmin => "Set and delete secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents327 Scope::RunnersRead => "See self-hosted runners, their groups and where agents run",
328 Scope::RunnersAdmin => "Register and remove self-hosted runners, change their groups and settings",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step329 }
330 }
331}
332
333impl Serialize for Scope {
334 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
335 serializer.serialize_str(self.as_str())
336 }
337}
338
339impl<'de> Deserialize<'de> for Scope {
340 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
341 let text = String::deserialize(deserializer)?;
342 Scope::parse(&text).ok_or_else(|| serde::de::Error::custom(format!("unknown scope {text}")))
343 }
344}
345
346/// Scopes as written in a token's row or an OAuth request: separated by
347/// spaces or commas. Unknown names are left out, so a client asking for a
348/// scope from a newer version gets the rest.
349pub fn parse_scopes(text: &str) -> Vec<Scope> {
350 let mut scopes: Vec<Scope> = text
351 .split(|c: char| c.is_whitespace() || c == ',')
352 .filter_map(Scope::parse)
353 .collect();
354 normalize(&mut scopes);
355 scopes
356}
357
358/// In table order, without repeats.
359pub fn normalize(scopes: &mut Vec<Scope>) {
360 let given = std::mem::take(scopes);
361 scopes.extend(Scope::ALL.into_iter().filter(|scope| given.contains(scope)));
362}
363
364/// Space-separated, as stored and as OAuth writes them.
365pub fn scopes_text(scopes: &[Scope]) -> String {
366 scopes.iter().map(|scope| scope.as_str()).collect::<Vec<_>>().join(" ")
367}
368
369/// What a token stores for full access, which is not a scope a client can
370/// ask for by name.
371pub const FULL_ACCESS: &str = "*";
372
373/// Starting points for choosing scopes.
374#[derive(Clone, Copy, Debug, PartialEq, Eq)]
375pub enum Preset {
376 ReadOnly,
377 Agent,
378 Ci,
379 Full,
380}
381
382impl Preset {
383 pub const ALL: [Preset; 4] = [Preset::ReadOnly, Preset::Agent, Preset::Ci, Preset::Full];
384
385 pub fn as_str(self) -> &'static str {
386 match self {
387 Preset::ReadOnly => "read_only",
388 Preset::Agent => "agent",
389 Preset::Ci => "ci",
390 Preset::Full => "full",
391 }
392 }
393
394 pub fn label(self) -> &'static str {
395 match self {
396 Preset::ReadOnly => "Read only",
397 Preset::Agent => "Agent",
398 Preset::Ci => "CI",
399 Preset::Full => "Full access",
400 }
401 }
402
403 /// Its scopes; `None` for full access.
404 pub fn scopes(self) -> Option<Vec<Scope>> {
405 let reads = || Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read);
406 match self {
407 Preset::ReadOnly => Some(reads().collect()),
408 Preset::Agent => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents409 // Not the machines work runs on: an agent has no business
410 // knowing a workspace's own runners.
411 let mut scopes: Vec<Scope> = reads().filter(|scope| scope.resource() != Resource::Runners).collect();
API: notifications over REST and MCP, with notifications scopes412 // And answering what needs the person it works for: marking
413 // it done, subscribing, watching.
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step414 scopes.extend([
415 Scope::CodeWrite,
416 Scope::IssuesWrite,
417 Scope::PullRequestsWrite,
418 Scope::AgentsRun,
419 Scope::MemoryWrite,
API: notifications over REST and MCP, with notifications scopes420 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step421 ]);
422 normalize(&mut scopes);
423 Some(scopes)
424 }
425 Preset::Ci => Some(vec![
426 Scope::RepoRead,
427 Scope::CodeRead,
428 Scope::CodeWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member429 Scope::PackagesRead,
430 Scope::PackagesWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step431 Scope::WorkflowsRead,
432 Scope::WorkflowsWrite,
433 ]),
434 Preset::Full => None,
435 }
436 }
437}
438
439/// What an OAuth client gets when it asks for nothing in particular: the
440/// agent preset. Never an admin scope.
441pub fn oauth_default() -> Vec<Scope> {
442 Preset::Agent.scopes().unwrap_or_default()
443}
444
445/// Set on a [`crate::User`] resolved from an access token: what the token
446/// may do. Absent on a signed-in session, which may do whatever its person
447/// can.
448#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
449pub struct TokenAccess {
450 /// The token's id, as audit entries and errors name it.
451 #[serde(default)]
452 pub token_id: String,
453 /// Its scopes, as `resource:level`. Absent: full access, everything the
454 /// person (or workspace) can do.
455 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub scopes: Option<Vec<String>>,
457 /// Made before tokens had scopes: full access until someone narrows it.
458 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
459 pub legacy: bool,
460}
461
462impl TokenAccess {
463 /// Full access to everything: the access tokens made before scopes had.
464 pub fn full() -> Self {
465 TokenAccess::default()
466 }
467
468 pub fn is_full(&self) -> bool {
469 self.scopes.is_none()
470 }
471
472 /// The scopes it holds, or `None` for full access.
473 pub fn granted(&self) -> Option<Vec<Scope>> {
474 self.scopes
475 .as_ref()
476 .map(|scopes| scopes.iter().filter_map(|scope| Scope::parse(scope)).collect())
477 }
478
479 pub fn allows(&self, needed: Scope) -> bool {
480 match self.granted() {
481 None => true,
482 Some(granted) => granted.iter().any(|held| held.includes(needed)),
483 }
484 }
485}
486
487/// Every operation of the API and MCP server, with the scope it needs. An
488/// operation in [`NO_SCOPE`] needs none. The API checks that every one of
489/// its operations is in exactly one of the two.
490pub const OPERATIONS: &[(&str, Scope)] = &[
491 // Your account.
492 ("list_emails", Scope::AccountRead),
493 ("add_email", Scope::AccountWrite),
494 ("remove_email", Scope::AccountWrite),
495 ("update_email_settings", Scope::AccountWrite),
496 ("list_invites", Scope::AccountRead),
497 ("create_invite", Scope::AccountWrite),
498 ("revoke_invite", Scope::AccountWrite),
499 ("list_my_repo_invitations", Scope::AccountRead),
500 ("accept_repo_invitation", Scope::AccountWrite),
501 ("decline_repo_invitation", Scope::AccountWrite),
API: pinned projects over REST and MCP502 // Your pinned projects: a preference of your account.
503 ("list_pinned_projects", Scope::AccountRead),
504 ("pin_project", Scope::AccountWrite),
505 ("unpin_project", Scope::AccountWrite),
506 ("reorder_pinned_projects", Scope::AccountWrite),
API: notifications over REST and MCP, with notifications scopes507 // Your inbox: notifications, subscriptions and watching.
508 ("list_notifications", Scope::NotificationsRead),
509 ("get_notification_thread", Scope::NotificationsRead),
510 ("get_thread_subscription", Scope::NotificationsRead),
511 ("get_repo_subscription", Scope::NotificationsRead),
512 ("list_watched_repos", Scope::NotificationsRead),
513 ("mark_notifications_read", Scope::NotificationsWrite),
514 ("mark_thread_read", Scope::NotificationsWrite),
515 ("mark_thread_done", Scope::NotificationsWrite),
516 ("save_thread", Scope::NotificationsWrite),
517 ("snooze_thread", Scope::NotificationsWrite),
518 ("set_thread_subscription", Scope::NotificationsWrite),
519 ("delete_thread_subscription", Scope::NotificationsWrite),
520 ("set_repo_subscription", Scope::NotificationsWrite),
521 ("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 step522 // Workspaces, their invites and integrations.
523 ("create_workspace", Scope::WorkspaceAdmin),
524 ("delete_workspace", Scope::WorkspaceAdmin),
Merge branch 'worktree-agent-ad7c6d88d93adc817'525 ("get_workspace", Scope::WorkspaceRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily526 ("update_workspace", Scope::WorkspaceAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step527 ("list_workspace_invites", Scope::WorkspaceRead),
528 ("invite_member", Scope::WorkspaceAdmin),
529 ("revoke_workspace_invite", Scope::WorkspaceAdmin),
530 ("list_integrations", Scope::WorkspaceRead),
531 ("connect_integration", Scope::WorkspaceAdmin),
532 ("disconnect_integration", Scope::WorkspaceAdmin),
533 ("test_integration", Scope::WorkspaceAdmin),
534 ("get_model_routes", Scope::WorkspaceRead),
535 ("set_model_routes", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar536 // Teams: reading them, and managing them. A team's role on a
537 // repository is who has access.
538 ("list_teams", Scope::WorkspaceRead),
539 ("get_team", Scope::WorkspaceRead),
540 ("list_team_members", Scope::WorkspaceRead),
541 ("list_child_teams", Scope::WorkspaceRead),
542 ("list_team_repos", Scope::WorkspaceRead),
543 ("list_user_teams", Scope::WorkspaceRead),
544 ("create_team", Scope::WorkspaceAdmin),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge545 ("list_workspace_rulesets", Scope::WorkspaceRead),
546 ("get_workspace_ruleset", Scope::WorkspaceRead),
547 ("list_workspace_rule_evaluations", Scope::WorkspaceRead),
548 ("create_workspace_ruleset", Scope::WorkspaceAdmin),
549 ("update_workspace_ruleset", Scope::WorkspaceAdmin),
550 ("delete_workspace_ruleset", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar551 ("update_team", Scope::WorkspaceAdmin),
552 ("delete_team", Scope::WorkspaceAdmin),
553 ("set_team_member", Scope::WorkspaceAdmin),
554 ("remove_team_member", Scope::WorkspaceAdmin),
555 ("set_team_review_assignment", Scope::WorkspaceAdmin),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit556 // A workspace's billing: usage, budget, AI credit and invoices.
557 ("get_usage", Scope::BillingRead),
558 ("get_budget", Scope::BillingRead),
559 ("get_ai_credit", Scope::BillingRead),
560 ("list_invoices", Scope::BillingRead),
561 ("get_billing_details", Scope::BillingRead),
562 ("set_budget", Scope::BillingWrite),
563 ("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 step564 // Repositories.
565 ("list_repos", Scope::RepoRead),
566 ("get_repo", Scope::RepoRead),
567 ("search", Scope::RepoRead),
568 ("list_events", Scope::RepoRead),
569 ("list_labels", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar570 ("list_milestones", Scope::RepoRead),
571 ("get_milestone", Scope::RepoRead),
572 ("create_label", Scope::IssuesWrite),
573 ("update_label", Scope::IssuesWrite),
574 ("delete_label", Scope::IssuesWrite),
575 ("add_default_labels", Scope::IssuesWrite),
576 ("create_milestone", Scope::IssuesWrite),
577 ("update_milestone", Scope::IssuesWrite),
578 ("delete_milestone", Scope::IssuesWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step579 ("get_repo_settings", Scope::RepoRead),
Fast pages, required checks on the branch, self-hosted runners, honest incidents580 ("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 step581 ("list_deleted_repos", Scope::RepoRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily582 ("list_security_alerts", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar583 ("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 step584 ("create_repo", Scope::RepoWrite),
585 ("update_repo", Scope::RepoWrite),
586 ("update_repo_settings", Scope::RepoWrite),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge587 // Rulesets: reading them is reading the repository; changing them
588 // changes what everyone, agents included, may do, so it is admin.
589 ("list_repo_rulesets", Scope::RepoRead),
590 ("get_repo_ruleset", Scope::RepoRead),
591 ("get_branch_rules", Scope::RepoRead),
592 ("list_rule_evaluations", Scope::RepoRead),
593 ("create_repo_ruleset", Scope::RepoAdmin),
594 ("update_repo_ruleset", Scope::RepoAdmin),
595 ("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 step596 ("rename_branch", Scope::RepoWrite),
597 ("rename_repo", Scope::RepoAdmin),
598 ("transfer_repo", Scope::RepoAdmin),
599 ("archive_repo", Scope::RepoAdmin),
600 ("unarchive_repo", Scope::RepoAdmin),
601 ("set_repo_visibility", Scope::RepoAdmin),
602 ("delete_repo", Scope::RepoAdmin),
603 ("restore_repo", Scope::RepoAdmin),
604 ("purge_repo", Scope::RepoAdmin),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily605 // A dismissed secret is let through push protection.
606 ("dismiss_security_alert", Scope::RepoAdmin),
607 ("reopen_security_alert", Scope::RepoAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar608 // The security suite: alerts, push protection, patterns, code
609 // scanning, the supply chain and settings.
610 ("list_secret_scanning_alerts", Scope::SecurityRead),
611 ("get_secret_scanning_alert", Scope::SecurityRead),
612 ("list_secret_scanning_locations", Scope::SecurityRead),
613 ("list_bypass_requests", Scope::SecurityRead),
614 ("list_custom_patterns", Scope::SecurityRead),
615 ("list_code_scanning_alerts", Scope::SecurityRead),
616 ("get_code_scanning_alert", Scope::SecurityRead),
617 ("list_code_scanning_analyses", Scope::SecurityRead),
618 ("get_sarif_upload", Scope::SecurityRead),
619 ("list_vulnerability_alerts", Scope::SecurityRead),
620 ("get_vulnerability_alert", Scope::SecurityRead),
621 ("get_dependency_graph", Scope::SecurityRead),
622 ("get_sbom", Scope::SecurityRead),
623 ("compare_dependencies", Scope::SecurityRead),
624 ("get_security_settings", Scope::SecurityRead),
625 ("get_workspace_security_settings", Scope::SecurityRead),
626 ("get_security_overview", Scope::SecurityRead),
627 ("update_secret_scanning_alert", Scope::SecurityWrite),
628 ("bypass_push_protection", Scope::SecurityWrite),
629 ("check_secret_validity", Scope::SecurityWrite),
630 ("review_bypass_request", Scope::SecurityWrite),
631 ("create_custom_pattern", Scope::SecurityWrite),
632 ("update_custom_pattern", Scope::SecurityWrite),
633 ("delete_custom_pattern", Scope::SecurityWrite),
634 ("dry_run_custom_pattern", Scope::SecurityWrite),
635 ("update_code_scanning_alert", Scope::SecurityWrite),
636 ("upload_sarif", Scope::SecurityWrite),
637 ("update_vulnerability_alert", Scope::SecurityWrite),
638 ("fix_security_alert", Scope::SecurityWrite),
639 ("update_security_settings", Scope::SecurityWrite),
640 ("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 step641 // Issues and plans.
642 ("list_issues", Scope::IssuesRead),
643 ("get_issue", Scope::IssuesRead),
644 ("get_plan", Scope::IssuesRead),
645 ("create_issue", Scope::IssuesWrite),
646 ("update_issue", Scope::IssuesWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar647 ("list_issue_labels", Scope::IssuesRead),
648 ("add_issue_labels", Scope::IssuesWrite),
649 ("set_issue_labels", Scope::IssuesWrite),
650 ("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 step651 ("close_issue", Scope::IssuesWrite),
652 ("reopen_issue", Scope::IssuesWrite),
653 ("add_comment", Scope::IssuesWrite),
654 ("import_issue", Scope::IssuesWrite),
655 ("apply_plan", Scope::IssuesWrite),
656 // Pull requests.
657 ("list_pull_requests", Scope::PullRequestsRead),
658 ("get_pull_request", Scope::PullRequestsRead),
659 ("get_pull_request_changes", Scope::PullRequestsRead),
660 ("read_session", Scope::PullRequestsRead),
661 ("get_merge_queue", Scope::PullRequestsRead),
662 ("create_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar663 ("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 step664 ("record_session", Scope::PullRequestsWrite),
665 ("mark_pull_request_ready", Scope::PullRequestsWrite),
666 ("close_pull_request", Scope::PullRequestsWrite),
667 ("review_pull_request", Scope::PullRequestsWrite),
668 ("merge_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar669 ("request_reviewers", Scope::PullRequestsWrite),
670 ("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 step671 // g1t's agents.
672 ("assign_issue", Scope::AgentsRun),
673 ("delegate", Scope::AgentsRun),
674 ("plan_work", Scope::AgentsRun),
675 ("message_agent", Scope::AgentsRun),
676 ("answer_message", Scope::AgentsRun),
677 ("take_messages", Scope::AgentsRun),
678 // Workflows.
679 ("list_workflows", Scope::WorkflowsRead),
680 ("list_workflow_runs", Scope::WorkflowsRead),
681 ("get_workflow_run", Scope::WorkflowsRead),
682 ("get_job_logs", Scope::WorkflowsRead),
683 ("dispatch_workflow", Scope::WorkflowsWrite),
684 ("cancel_workflow_run", Scope::WorkflowsWrite),
685 ("rerun_workflow_run", Scope::WorkflowsWrite),
686 ("update_workflow", Scope::WorkflowsWrite),
687 // Memory and the context hub.
688 ("recall", Scope::MemoryRead),
689 ("search_context", Scope::MemoryRead),
690 ("get_entity", Scope::MemoryRead),
691 ("get_context", Scope::MemoryRead),
692 ("remember", Scope::MemoryWrite),
693 // Who has access.
694 ("list_collaborators", Scope::AccessRead),
695 ("get_collaborator_permission", Scope::AccessRead),
696 ("list_repo_invitations", Scope::AccessRead),
697 ("list_outside_collaborators", Scope::AccessRead),
698 ("add_collaborator", Scope::AccessAdmin),
699 ("update_collaborator", Scope::AccessAdmin),
700 ("remove_collaborator", Scope::AccessAdmin),
701 ("revoke_repo_invitation", Scope::AccessAdmin),
702 ("set_base_permission", Scope::AccessAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar703 ("set_team_repo", Scope::AccessAdmin),
704 ("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 step705 // Webhooks.
706 ("list_webhooks", Scope::WebhooksRead),
707 ("list_webhook_deliveries", Scope::WebhooksRead),
708 ("create_webhook", Scope::WebhooksAdmin),
709 ("update_webhook", Scope::WebhooksAdmin),
710 ("delete_webhook", Scope::WebhooksAdmin),
711 ("ping_webhook", Scope::WebhooksAdmin),
712 ("redeliver_webhook", Scope::WebhooksAdmin),
713 // Secrets and variables.
714 ("list_actions_secrets", Scope::SecretsRead),
715 ("list_actions_variables", Scope::SecretsRead),
716 ("set_actions_secret", Scope::SecretsAdmin),
717 ("delete_actions_secret", Scope::SecretsAdmin),
718 ("set_actions_variable", Scope::SecretsAdmin),
719 ("delete_actions_variable", Scope::SecretsAdmin),
Fast pages, required checks on the branch, self-hosted runners, honest incidents720 // Self-hosted runners.
721 ("list_runners", Scope::RunnersRead),
722 ("list_runner_groups", Scope::RunnersRead),
723 ("get_runner_settings", Scope::RunnersRead),
724 ("create_runner_registration_token", Scope::RunnersAdmin),
725 ("remove_runner", Scope::RunnersAdmin),
726 ("create_runner_group", Scope::RunnersAdmin),
727 ("update_runner_group", Scope::RunnersAdmin),
728 ("delete_runner_group", Scope::RunnersAdmin),
729 ("update_runner_settings", Scope::RunnersAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step730];
731
732/// Operations any token may use: saying who it is.
733pub const NO_SCOPE: &[&str] = &["whoami"];
734
735/// The scope `operation` needs. `None` for one in [`NO_SCOPE`]; an
736/// operation in neither list needs full access.
737pub fn scope_for(operation: &str) -> Option<Scope> {
738 OPERATIONS
739 .iter()
740 .find(|(name, _)| *name == operation)
741 .map(|(_, scope)| *scope)
742}
743
744/// What a token needs for `operation` with this input beyond its own
745/// scope: starting agents from an operation that can, and making a
746/// repository public or private.
747pub fn extra_scopes(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
748 let mut extra = Vec::new();
749 let assigns = input["assign"].as_bool() == Some(true)
750 || input["agent"].as_bool() == Some(true)
751 || input["assign_agent"].as_bool() == Some(true);
752 if assigns && matches!(operation, "apply_plan" | "import_issue" | "create_issue") {
753 extra.push(Scope::AgentsRun);
754 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar755 // Fixing an alert opens an issue and puts g1t on it.
756 if operation == "fix_security_alert" {
757 extra.extend([Scope::IssuesWrite, Scope::AgentsRun]);
758 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step759 // Opening the issue an agent is put on.
760 if operation == "delegate" {
761 extra.push(Scope::IssuesWrite);
762 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily763 // A workspace's base permission is who has access.
764 if operation == "update_workspace" && input.get("base_permission").is_some_and(|v| !v.is_null()) {
765 extra.push(Scope::AccessAdmin);
766 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step767 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())) {
768 extra.push(Scope::RepoAdmin);
769 }
770 extra
771}
772
773/// The scopes a call needs, its own first.
774pub fn needed(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
775 scope_for(operation)
776 .into_iter()
777 .chain(extra_scopes(operation, input))
778 .collect()
779}
780
781/// Whether `access` may use `operation` with `input`. The person's (or
782/// workspace's) role is checked after this, by the service that owns what
783/// was asked about.
784pub fn decide(access: &TokenAccess, operation: &str, input: &serde_json::Value) -> Decision {
785 let rule = if access.legacy { "token:legacy" } else { "token:scope" };
786 if access.scopes.is_some() {
787 let known = NO_SCOPE.contains(&operation) || scope_for(operation).is_some();
788 if !known {
789 return Decision::deny("token:scope", format!("This access token cannot use {operation}: it needs full access."));
790 }
791 if let Some(missing) = needed(operation, input).into_iter().find(|scope| !access.allows(*scope)) {
792 return Decision::deny(
793 "token:scope",
794 format!("This access token needs the {} scope to use {operation}.", missing.as_str()),
795 );
796 }
797 }
798 Decision::allow(rule)
799}
800
801/// Whether a token may clone or fetch (`write` false), or push to (`write`
802/// true), a repository with git. `public` is whether anyone may read it,
803/// which needs no scope.
804pub fn decide_git(access: &TokenAccess, write: bool, public: bool) -> Decision {
805 let needed = if write { Scope::CodeWrite } else { Scope::CodeRead };
806 if !access.allows(needed) && (write || !public) {
807 return Decision::deny(
808 "token:scope",
809 format!("This access token needs the {} scope to {} with git.", needed.as_str(), if write { "push" } else { "clone or fetch a private repository" }),
810 );
811 }
812 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
813}
814
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member815/// Whether a token may pull (`Level::Read`), push or publish
816/// (`Level::Write`), or delete (`Level::Delete`) packages. `public` is
817/// whether anyone may pull the package, which needs no scope.
818pub fn decide_packages(access: &TokenAccess, level: Level, public: bool) -> Decision {
819 let (needed, doing) = match level {
820 Level::Read => (Scope::PackagesRead, "pull a private package"),
821 Level::Delete | Level::Admin => (Scope::PackagesDelete, "delete packages"),
822 Level::Write | Level::Run => (Scope::PackagesWrite, "push or publish packages"),
823 };
824 if !access.allows(needed) && !(level == Level::Read && public) {
825 return Decision::deny(
826 "token:scope",
827 format!("This access token needs the {} scope to {doing}.", needed.as_str()),
828 );
829 }
830 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
831}
832
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step833#[cfg(test)]
834mod tests {
835 use super::*;
836 use serde_json::json;
837
838 fn token(scopes: &[Scope]) -> TokenAccess {
839 TokenAccess {
840 token_id: "tok_1".to_owned(),
841 scopes: Some(scopes.iter().map(|scope| scope.as_str().to_owned()).collect()),
842 legacy: false,
843 }
844 }
845
846 #[test]
847 fn every_scope_reads_back_and_belongs_to_a_resource() {
848 for scope in Scope::ALL {
849 assert_eq!(Scope::parse(scope.as_str()), Some(scope));
850 assert!(scope.as_str().starts_with(scope.resource().as_str()));
851 assert!(scope.includes(scope));
852 }
853 assert_eq!(Scope::parse(" Issues:Write "), Some(Scope::IssuesWrite));
854 assert_eq!(Scope::parse("issues"), None);
855 }
856
857 #[test]
858 fn a_higher_level_includes_the_lower_ones_of_its_resource_only() {
859 assert!(Scope::RepoAdmin.includes(Scope::RepoRead));
860 assert!(Scope::RepoAdmin.includes(Scope::RepoWrite));
861 assert!(Scope::IssuesWrite.includes(Scope::IssuesRead));
862 assert!(!Scope::IssuesRead.includes(Scope::IssuesWrite));
863 assert!(!Scope::RepoAdmin.includes(Scope::CodeWrite));
864 assert!(!Scope::PullRequestsWrite.includes(Scope::IssuesWrite));
865 }
866
867 #[test]
868 fn operations_are_listed_once_and_never_also_free() {
869 let mut seen = std::collections::HashSet::new();
870 for (name, _) in OPERATIONS {
871 assert!(seen.insert(*name), "{name} twice");
872 assert!(!NO_SCOPE.contains(name), "{name}");
873 }
874 }
875
876 #[test]
877 fn scopes_are_parsed_from_oauth_text_leaving_out_unknown_ones() {
878 assert_eq!(
879 parse_scopes("issues:write repo:read,bogus:thing issues:write"),
880 vec![Scope::RepoRead, Scope::IssuesWrite]
881 );
882 assert_eq!(scopes_text(&[Scope::RepoRead, Scope::IssuesWrite]), "repo:read issues:write");
883 }
884
885 #[test]
886 fn the_oauth_default_is_the_agent_preset_and_never_admin() {
887 let scopes = oauth_default();
888 assert!(scopes.contains(&Scope::IssuesWrite));
889 assert!(scopes.contains(&Scope::PullRequestsWrite));
890 assert!(scopes.contains(&Scope::AgentsRun));
891 assert!(scopes.iter().all(|scope| !scope.dangerous()), "{scopes:?}");
892 for read in Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents893 // Every read but the machines work runs on.
894 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 step895 }
896 assert!(Preset::ReadOnly.scopes().unwrap().iter().all(|scope| scope.level() == Level::Read));
897 assert_eq!(Preset::Full.scopes(), None);
898 }
899
900 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit901 fn billing_is_read_by_presets_and_changed_by_none_but_full_access() {
902 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::BillingRead));
903 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
904 assert!(!preset.scopes().unwrap().contains(&Scope::BillingWrite), "{}", preset.as_str());
905 }
906 assert_eq!(scope_for("set_budget"), Some(Scope::BillingWrite));
907 assert_eq!(scope_for("buy_ai_credit"), Some(Scope::BillingWrite));
908 assert_eq!(scope_for("get_usage"), Some(Scope::BillingRead));
909 let reader = token(&[Scope::BillingRead]);
910 assert!(decide(&reader, "list_invoices", &json!({})).allowed);
911 assert!(decide(&reader, "set_budget", &json!({})).reason.unwrap().contains("billing:write"));
912 }
913
914 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step915 fn a_legacy_token_can_do_everything() {
916 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
917 for (operation, _) in OPERATIONS {
918 assert!(decide(&legacy, operation, &json!({})).allowed, "{operation}");
919 }
920 assert_eq!(decide(&legacy, "delete_repo", &json!({})).rule, "token:legacy");
921 }
922
923 #[test]
924 fn a_missing_scope_is_named() {
925 let read = token(&[Scope::IssuesRead]);
926 assert!(decide(&read, "get_issue", &json!({})).allowed);
927 assert!(decide(&read, "whoami", &json!({})).allowed);
928 let refused = decide(&read, "create_issue", &json!({}));
929 assert!(!refused.allowed);
930 assert_eq!(refused.reason.as_deref(), Some("This access token needs the issues:write scope to use create_issue."));
931 // An operation the table does not know needs full access.
932 assert!(!decide(&read, "something_new", &json!({})).allowed);
933 }
934
935 #[test]
936 fn starting_agents_from_another_operation_needs_agents_run() {
937 let writer = token(&[Scope::IssuesWrite]);
938 assert!(decide(&writer, "apply_plan", &json!({})).allowed);
939 let refused = decide(&writer, "apply_plan", &json!({ "assign": true }));
940 assert!(refused.reason.unwrap().contains("agents:run"));
941 let maintainer = token(&[Scope::RepoWrite]);
942 assert!(decide(&maintainer, "update_repo", &json!({ "description": "x" })).allowed);
943 assert!(!decide(&maintainer, "update_repo", &json!({ "private": true })).allowed);
944 }
945
946 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily947 fn a_workspaces_base_permission_needs_access_admin_too() {
948 let admin = token(&[Scope::WorkspaceAdmin]);
949 assert!(decide(&admin, "update_workspace", &json!({ "name": "Acme" })).allowed);
950 let refused = decide(&admin, "update_workspace", &json!({ "name": "Acme", "base_permission": "read" }));
951 assert!(refused.reason.unwrap().contains("access:admin"));
952 let both = token(&[Scope::WorkspaceAdmin, Scope::AccessAdmin]);
953 assert!(decide(&both, "update_workspace", &json!({ "base_permission": "read" })).allowed);
954 assert!(!decide(&token(&[Scope::WorkspaceRead]), "update_workspace", &json!({ "name": "Acme" })).allowed);
955 }
956
957 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step958 fn delegating_needs_both_agents_and_issues() {
959 let agents = token(&[Scope::AgentsRun]);
960 assert!(decide(&agents, "delegate", &json!({})).reason.unwrap().contains("issues:write"));
961 let both = token(&[Scope::AgentsRun, Scope::IssuesWrite]);
962 assert!(decide(&both, "delegate", &json!({})).allowed);
963 }
964
965 #[test]
966 fn git_push_needs_code_write_and_private_reads_need_code_read() {
967 let reader = token(&[Scope::CodeRead]);
968 assert!(decide_git(&reader, false, false).allowed);
969 let refused = decide_git(&reader, true, false);
970 assert!(!refused.allowed);
971 assert!(refused.reason.unwrap().contains("code:write"));
972 let issues = token(&[Scope::IssuesWrite]);
973 assert!(!decide_git(&issues, false, false).allowed);
974 assert!(decide_git(&issues, false, true).allowed, "public code needs no scope");
975 assert!(!decide_git(&issues, true, true).allowed, "pushing to public code still needs code:write");
976 let writer = token(&[Scope::CodeWrite]);
977 assert!(decide_git(&writer, true, false).allowed);
978 assert!(decide_git(&writer, false, false).allowed, "code:write includes code:read");
979 assert!(decide_git(&TokenAccess::full(), true, false).allowed);
980 }
981
982 #[test]
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member983 fn packages_need_their_own_scopes_and_public_pulls_none() {
984 let reader = token(&[Scope::PackagesRead]);
985 assert!(decide_packages(&reader, Level::Read, false).allowed);
986 assert!(!decide_packages(&reader, Level::Write, false).allowed);
987 let code = token(&[Scope::CodeWrite]);
988 assert!(!decide_packages(&code, Level::Read, false).allowed, "code scopes are not package scopes");
989 assert!(decide_packages(&code, Level::Read, true).allowed, "public packages pull with any token");
990 let writer = token(&[Scope::PackagesWrite]);
991 assert!(decide_packages(&writer, Level::Write, false).allowed);
992 assert!(decide_packages(&writer, Level::Read, false).allowed, "packages:write includes packages:read");
993 let refused = decide_packages(&writer, Level::Delete, false);
994 assert!(refused.reason.unwrap().contains("packages:delete"));
995 assert!(decide_packages(&token(&[Scope::PackagesDelete]), Level::Write, false).allowed);
996 assert!(Scope::PackagesDelete.dangerous());
997 // Tokens made before these scopes, and full-access ones, keep working.
998 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
999 assert!(decide_packages(&legacy, Level::Delete, false).allowed);
1000 assert!(decide_packages(&TokenAccess::full(), Level::Write, false).allowed);
1001 }
1002
1003 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1004 fn token_access_travels_as_json() {
1005 let access = token(&[Scope::IssuesRead]);
1006 let wire = serde_json::to_value(&access).unwrap();
1007 assert_eq!(wire["scopes"], json!(["issues:read"]));
1008 assert!(wire.get("resources").is_none());
1009 let back: TokenAccess = serde_json::from_value(wire).unwrap();
1010 assert_eq!(back, access);
1011 let full: TokenAccess = serde_json::from_value(json!({})).unwrap();
1012 assert!(full.is_full());
1013 // A reach written by an older version is ignored: a token reaches
1014 // whatever its owner can.
1015 let older: TokenAccess = serde_json::from_value(json!({
1016 "token_id": "tok_1",
1017 "scopes": ["issues:read"],
1018 "resources": { "kind": "repositories", "repositories": ["acme/rocket"] },
1019 }))
1020 .unwrap();
1021 assert_eq!(older, access);
1022 }
1023
1024 /// The site's copy of the table, `packages/contracts/src/scopes.ts`,
1025 /// lists the same scopes in the same order, the same operations with
1026 /// the same scopes, and the same presets.
1027 #[test]
1028 fn the_typescript_mirror_has_the_same_table() {
1029 let ts = include_str!("../../../packages/contracts/src/scopes.ts");
1030 let section = |start: &str| {
1031 ts.split_once(start)
1032 .and_then(|(_, rest)| rest.split_once("] as const"))
1033 .map(|(table, _)| table)
1034 .unwrap_or_else(|| panic!("{start} in scopes.ts"))
1035 };
1036 let scopes: Vec<&str> = section("export const SCOPES = [")
1037 .lines()
1038 .filter_map(|line| line.split_once("scope: \"").and_then(|(_, rest)| rest.split_once('"')).map(|(scope, _)| scope))
1039 .collect();
1040 let expected: Vec<&str> = Scope::ALL.iter().map(|scope| scope.as_str()).collect();
1041 assert_eq!(scopes, expected);
1042 let operations: Vec<(String, String)> = section("export const OPERATION_SCOPES = [")
1043 .lines()
1044 .filter_map(|line| {
1045 let mut quoted = line.split('"').skip(1).step_by(2);
1046 Some((quoted.next()?.to_owned(), quoted.next()?.to_owned()))
1047 })
1048 .collect();
1049 let expected: Vec<(String, String)> = OPERATIONS
1050 .iter()
1051 .map(|(name, scope)| ((*name).to_owned(), scope.as_str().to_owned()))
1052 .collect();
1053 assert_eq!(operations, expected);
1054 for preset in Preset::ALL {
1055 let list = section(&format!("{}: [", preset.as_str()));
1056 let mirrored: Vec<&str> = list
1057 .split(',')
1058 .map(|item| item.trim().trim_matches('"'))
1059 .filter(|item| !item.is_empty())
1060 .collect();
1061 let expected: Vec<&str> = preset
1062 .scopes()
1063 .map(|scopes| scopes.iter().map(|scope| scope.as_str()).collect())
1064 .unwrap_or_else(|| vec!["*"]);
1065 assert_eq!(mirrored, expected, "{}", preset.as_str());
1066 }
1067 }
1068}

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