Skip to content
1,089 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,
37 Memory,
38 Access,
39 Webhooks,
40 Secrets,
Fast pages, required checks on the branch, self-hosted runners, honest incidents41 Runners,
AI Gateway: your own code calls models with a workspace token42 Models,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step43}
44
45impl Resource {
AI Gateway: your own code calls models with a workspace token46 pub const ALL: [Resource; 18] = [
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step47 Resource::Repo,
48 Resource::Code,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar49 Resource::Security,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member50 Resource::Packages,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step51 Resource::Issues,
52 Resource::PullRequests,
53 Resource::Agents,
54 Resource::Workflows,
55 Resource::Memory,
56 Resource::Account,
API: notifications over REST and MCP, with notifications scopes57 Resource::Notifications,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step58 Resource::Workspace,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit59 Resource::Billing,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step60 Resource::Access,
61 Resource::Webhooks,
62 Resource::Secrets,
Fast pages, required checks on the branch, self-hosted runners, honest incidents63 Resource::Runners,
AI Gateway: your own code calls models with a workspace token64 Resource::Models,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step65 ];
66
67 pub fn as_str(self) -> &'static str {
68 match self {
69 Resource::Account => "account",
API: notifications over REST and MCP, with notifications scopes70 Resource::Notifications => "notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step71 Resource::Workspace => "workspace",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit72 Resource::Billing => "billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step73 Resource::Repo => "repo",
74 Resource::Code => "code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar75 Resource::Security => "security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member76 Resource::Packages => "packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step77 Resource::Issues => "issues",
78 Resource::PullRequests => "pull_requests",
79 Resource::Agents => "agents",
80 Resource::Workflows => "workflows",
81 Resource::Memory => "memory",
82 Resource::Access => "access",
83 Resource::Webhooks => "webhooks",
84 Resource::Secrets => "secrets",
Fast pages, required checks on the branch, self-hosted runners, honest incidents85 Resource::Runners => "runners",
AI Gateway: your own code calls models with a workspace token86 Resource::Models => "models",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step87 }
88 }
89
90 /// Its name, for people.
91 pub fn label(self) -> &'static str {
92 match self {
93 Resource::Account => "Your account",
API: notifications over REST and MCP, with notifications scopes94 Resource::Notifications => "Notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step95 Resource::Workspace => "Workspaces",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit96 Resource::Billing => "Billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step97 Resource::Repo => "Repositories",
98 Resource::Code => "Code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar99 Resource::Security => "Security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member100 Resource::Packages => "Packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step101 Resource::Issues => "Issues",
102 Resource::PullRequests => "Pull requests",
103 Resource::Agents => "g1t agents",
104 Resource::Workflows => "Workflows",
105 Resource::Memory => "Memory and context",
106 Resource::Access => "Who has access",
107 Resource::Webhooks => "Webhooks",
108 Resource::Secrets => "Secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents109 Resource::Runners => "Self-hosted runners",
AI Gateway: your own code calls models with a workspace token110 Resource::Models => "AI Gateway",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step111 }
112 }
113}
114
115/// How much of a resource.
116#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
117pub enum Level {
118 Read,
119 Write,
120 /// Starting g1t's agents, which spends the workspace's money.
121 Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member122 /// Deleting what cannot be brought back, such as a package's versions.
123 Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step124 Admin,
125}
126
127impl Level {
128 pub fn as_str(self) -> &'static str {
129 match self {
130 Level::Read => "read",
131 Level::Write => "write",
132 Level::Run => "run",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member133 Level::Delete => "delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step134 Level::Admin => "admin",
135 }
136 }
137}
138
139/// One scope. Its text form, `resource:level`, is what tokens store, OAuth
140/// clients ask for, and errors name.
141#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
142pub enum Scope {
143 RepoRead,
144 RepoWrite,
145 RepoAdmin,
146 CodeRead,
147 CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar148 SecurityRead,
149 SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member150 PackagesRead,
151 PackagesWrite,
152 PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step153 IssuesRead,
154 IssuesWrite,
155 PullRequestsRead,
156 PullRequestsWrite,
157 AgentsRun,
158 WorkflowsRead,
159 WorkflowsWrite,
160 MemoryRead,
161 MemoryWrite,
162 AccountRead,
163 AccountWrite,
API: notifications over REST and MCP, with notifications scopes164 NotificationsRead,
165 NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step166 WorkspaceRead,
167 WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit168 BillingRead,
169 BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step170 AccessRead,
171 AccessAdmin,
172 WebhooksRead,
173 WebhooksAdmin,
174 SecretsRead,
175 SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents176 RunnersRead,
177 RunnersAdmin,
AI Gateway: your own code calls models with a workspace token178 ModelsRead,
179 ModelsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step180}
181
182impl Scope {
183 /// Every scope, grouped by resource, least first.
AI Gateway: your own code calls models with a workspace token184 pub const ALL: [Scope; 37] = [
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step185 Scope::RepoRead,
186 Scope::RepoWrite,
187 Scope::RepoAdmin,
188 Scope::CodeRead,
189 Scope::CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar190 Scope::SecurityRead,
191 Scope::SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member192 Scope::PackagesRead,
193 Scope::PackagesWrite,
194 Scope::PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step195 Scope::IssuesRead,
196 Scope::IssuesWrite,
197 Scope::PullRequestsRead,
198 Scope::PullRequestsWrite,
199 Scope::AgentsRun,
200 Scope::WorkflowsRead,
201 Scope::WorkflowsWrite,
202 Scope::MemoryRead,
203 Scope::MemoryWrite,
204 Scope::AccountRead,
205 Scope::AccountWrite,
API: notifications over REST and MCP, with notifications scopes206 Scope::NotificationsRead,
207 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step208 Scope::WorkspaceRead,
209 Scope::WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit210 Scope::BillingRead,
211 Scope::BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step212 Scope::AccessRead,
213 Scope::AccessAdmin,
214 Scope::WebhooksRead,
215 Scope::WebhooksAdmin,
216 Scope::SecretsRead,
217 Scope::SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents218 Scope::RunnersRead,
219 Scope::RunnersAdmin,
AI Gateway: your own code calls models with a workspace token220 Scope::ModelsRead,
221 Scope::ModelsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step222 ];
223
224 pub fn as_str(self) -> &'static str {
225 match self {
226 Scope::RepoRead => "repo:read",
227 Scope::RepoWrite => "repo:write",
228 Scope::RepoAdmin => "repo:admin",
229 Scope::CodeRead => "code:read",
230 Scope::CodeWrite => "code:write",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar231 Scope::SecurityRead => "security:read",
232 Scope::SecurityWrite => "security:write",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member233 Scope::PackagesRead => "packages:read",
234 Scope::PackagesWrite => "packages:write",
235 Scope::PackagesDelete => "packages:delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step236 Scope::IssuesRead => "issues:read",
237 Scope::IssuesWrite => "issues:write",
238 Scope::PullRequestsRead => "pull_requests:read",
239 Scope::PullRequestsWrite => "pull_requests:write",
240 Scope::AgentsRun => "agents:run",
241 Scope::WorkflowsRead => "workflows:read",
242 Scope::WorkflowsWrite => "workflows:write",
243 Scope::MemoryRead => "memory:read",
244 Scope::MemoryWrite => "memory:write",
245 Scope::AccountRead => "account:read",
246 Scope::AccountWrite => "account:write",
API: notifications over REST and MCP, with notifications scopes247 Scope::NotificationsRead => "notifications:read",
248 Scope::NotificationsWrite => "notifications:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step249 Scope::WorkspaceRead => "workspace:read",
250 Scope::WorkspaceAdmin => "workspace:admin",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit251 Scope::BillingRead => "billing:read",
252 Scope::BillingWrite => "billing:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step253 Scope::AccessRead => "access:read",
254 Scope::AccessAdmin => "access:admin",
255 Scope::WebhooksRead => "webhooks:read",
256 Scope::WebhooksAdmin => "webhooks:admin",
257 Scope::SecretsRead => "secrets:read",
258 Scope::SecretsAdmin => "secrets:admin",
Fast pages, required checks on the branch, self-hosted runners, honest incidents259 Scope::RunnersRead => "runners:read",
260 Scope::RunnersAdmin => "runners:admin",
AI Gateway: your own code calls models with a workspace token261 Scope::ModelsRead => "models:read",
262 Scope::ModelsWrite => "models:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step263 }
264 }
265
266 pub fn parse(text: &str) -> Option<Scope> {
267 let text = text.trim().to_ascii_lowercase();
268 Scope::ALL.into_iter().find(|scope| scope.as_str() == text)
269 }
270
271 pub fn resource(self) -> Resource {
272 let name = self.as_str().split_once(':').map_or("", |(resource, _)| resource);
273 Resource::ALL
274 .into_iter()
275 .find(|resource| resource.as_str() == name)
276 .unwrap_or(Resource::Account)
277 }
278
279 pub fn level(self) -> Level {
280 match self.as_str().rsplit_once(':').map_or("", |(_, level)| level) {
281 "write" => Level::Write,
282 "run" => Level::Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member283 "delete" => Level::Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step284 "admin" => Level::Admin,
285 _ => Level::Read,
286 }
287 }
288
289 /// Whether holding `self` gives `other`: the same resource, at the same
290 /// level or a lower one.
291 pub fn includes(self, other: Scope) -> bool {
292 self.resource() == other.resource() && self.level() >= other.level()
293 }
294
295 /// Changes that are hard or impossible to undo, or that decide who can
296 /// reach what. Shown behind a warning wherever scopes are chosen.
297 pub fn dangerous(self) -> bool {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member298 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 step299 }
300
301 /// What it lets a token do, in plain words.
302 pub fn describe(self) -> &'static str {
303 match self {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily304 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 step305 Scope::RepoWrite => "Create repositories, rename branches and change how pull requests merge",
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily306 Scope::RepoAdmin => "Rename, archive, transfer, delete or change who can see a repository, and dismiss security alerts",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step307 Scope::CodeRead => "Clone and fetch private repositories with git",
308 Scope::CodeWrite => "Push commits with git",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar309 Scope::SecurityRead => "See secret scanning, code scanning and vulnerability alerts, custom patterns, the dependency graph and SBOM, and security settings",
310 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 member311 Scope::PackagesRead => "Pull container images and install private packages",
312 Scope::PackagesWrite => "Push container images and publish packages",
313 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 step314 Scope::IssuesRead => "Read issues, comments and plans",
315 Scope::IssuesWrite => "Open, edit, close and comment on issues",
316 Scope::PullRequestsRead => "Read pull requests, their changes, sessions and merge queues",
317 Scope::PullRequestsWrite => "Open, review, close and merge pull requests",
318 Scope::AgentsRun => "Put g1t agents to work and message them, which uses the workspace's money",
319 Scope::WorkflowsRead => "Read workflows, runs and logs",
320 Scope::WorkflowsWrite => "Run, cancel, rerun and turn workflows on or off",
321 Scope::MemoryRead => "Recall memory and search the workspace's context",
322 Scope::MemoryWrite => "Save memory for the next agent",
API: pinned projects over REST and MCP323 Scope::AccountRead => "Read your email addresses, invites, invitations and pinned projects",
324 Scope::AccountWrite => "Change your email addresses, make invites, answer invitations and pin projects",
API: notifications over REST and MCP, with notifications scopes325 Scope::NotificationsRead => "See your inbox, its threads, and what you subscribe to and watch",
326 Scope::NotificationsWrite => "Mark notifications read, done, saved or snoozed, subscribe to threads and watch repositories",
Merge branch 'worktree-agent-ad7c6d88d93adc817'327 Scope::WorkspaceRead => "Read workspace settings, invites, integrations, model routes and teams",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar328 Scope::WorkspaceAdmin => "Create and delete workspaces, invite members, connect integrations, and create, change and delete teams",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit329 Scope::BillingRead => "See a workspace's usage, budget, AI credit and invoices",
330 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 step331 Scope::AccessRead => "See who has access to repositories",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar332 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 step333 Scope::WebhooksRead => "See webhooks and their deliveries",
334 Scope::WebhooksAdmin => "Create, change and delete webhooks",
335 Scope::SecretsRead => "List secrets (never their values) and read variables",
336 Scope::SecretsAdmin => "Set and delete secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents337 Scope::RunnersRead => "See self-hosted runners, their groups and where agents run",
338 Scope::RunnersAdmin => "Register and remove self-hosted runners, change their groups and settings",
AI Gateway: your own code calls models with a workspace token339 Scope::ModelsRead => "See the workspace's AI Gateway requests: their models, tokens, cost and status",
340 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 step341 }
342 }
343}
344
345impl Serialize for Scope {
346 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
347 serializer.serialize_str(self.as_str())
348 }
349}
350
351impl<'de> Deserialize<'de> for Scope {
352 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
353 let text = String::deserialize(deserializer)?;
354 Scope::parse(&text).ok_or_else(|| serde::de::Error::custom(format!("unknown scope {text}")))
355 }
356}
357
358/// Scopes as written in a token's row or an OAuth request: separated by
359/// spaces or commas. Unknown names are left out, so a client asking for a
360/// scope from a newer version gets the rest.
361pub fn parse_scopes(text: &str) -> Vec<Scope> {
362 let mut scopes: Vec<Scope> = text
363 .split(|c: char| c.is_whitespace() || c == ',')
364 .filter_map(Scope::parse)
365 .collect();
366 normalize(&mut scopes);
367 scopes
368}
369
370/// In table order, without repeats.
371pub fn normalize(scopes: &mut Vec<Scope>) {
372 let given = std::mem::take(scopes);
373 scopes.extend(Scope::ALL.into_iter().filter(|scope| given.contains(scope)));
374}
375
376/// Space-separated, as stored and as OAuth writes them.
377pub fn scopes_text(scopes: &[Scope]) -> String {
378 scopes.iter().map(|scope| scope.as_str()).collect::<Vec<_>>().join(" ")
379}
380
381/// What a token stores for full access, which is not a scope a client can
382/// ask for by name.
383pub const FULL_ACCESS: &str = "*";
384
385/// Starting points for choosing scopes.
386#[derive(Clone, Copy, Debug, PartialEq, Eq)]
387pub enum Preset {
388 ReadOnly,
389 Agent,
390 Ci,
391 Full,
392}
393
394impl Preset {
395 pub const ALL: [Preset; 4] = [Preset::ReadOnly, Preset::Agent, Preset::Ci, Preset::Full];
396
397 pub fn as_str(self) -> &'static str {
398 match self {
399 Preset::ReadOnly => "read_only",
400 Preset::Agent => "agent",
401 Preset::Ci => "ci",
402 Preset::Full => "full",
403 }
404 }
405
406 pub fn label(self) -> &'static str {
407 match self {
408 Preset::ReadOnly => "Read only",
409 Preset::Agent => "Agent",
410 Preset::Ci => "CI",
411 Preset::Full => "Full access",
412 }
413 }
414
415 /// Its scopes; `None` for full access.
416 pub fn scopes(self) -> Option<Vec<Scope>> {
417 let reads = || Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read);
418 match self {
419 Preset::ReadOnly => Some(reads().collect()),
420 Preset::Agent => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents421 // Not the machines work runs on: an agent has no business
422 // knowing a workspace's own runners.
423 let mut scopes: Vec<Scope> = reads().filter(|scope| scope.resource() != Resource::Runners).collect();
API: notifications over REST and MCP, with notifications scopes424 // And answering what needs the person it works for: marking
425 // it done, subscribing, watching.
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step426 scopes.extend([
427 Scope::CodeWrite,
428 Scope::IssuesWrite,
429 Scope::PullRequestsWrite,
430 Scope::AgentsRun,
431 Scope::MemoryWrite,
API: notifications over REST and MCP, with notifications scopes432 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step433 ]);
434 normalize(&mut scopes);
435 Some(scopes)
436 }
437 Preset::Ci => Some(vec![
438 Scope::RepoRead,
439 Scope::CodeRead,
440 Scope::CodeWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member441 Scope::PackagesRead,
442 Scope::PackagesWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step443 Scope::WorkflowsRead,
444 Scope::WorkflowsWrite,
445 ]),
446 Preset::Full => None,
447 }
448 }
449}
450
451/// What an OAuth client gets when it asks for nothing in particular: the
452/// agent preset. Never an admin scope.
453pub fn oauth_default() -> Vec<Scope> {
454 Preset::Agent.scopes().unwrap_or_default()
455}
456
457/// Set on a [`crate::User`] resolved from an access token: what the token
458/// may do. Absent on a signed-in session, which may do whatever its person
459/// can.
460#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
461pub struct TokenAccess {
462 /// The token's id, as audit entries and errors name it.
463 #[serde(default)]
464 pub token_id: String,
465 /// Its scopes, as `resource:level`. Absent: full access, everything the
466 /// person (or workspace) can do.
467 #[serde(default, skip_serializing_if = "Option::is_none")]
468 pub scopes: Option<Vec<String>>,
469 /// Made before tokens had scopes: full access until someone narrows it.
470 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
471 pub legacy: bool,
AI Gateway: your own code calls models with a workspace token472 /// The token's name, as its owner gave it, so a log can say which
473 /// token made a request. Absent where whoever resolved it did not say.
474 #[serde(default, skip_serializing_if = "Option::is_none")]
475 pub name: Option<String>,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step476}
477
478impl TokenAccess {
479 /// Full access to everything: the access tokens made before scopes had.
480 pub fn full() -> Self {
481 TokenAccess::default()
482 }
483
484 pub fn is_full(&self) -> bool {
485 self.scopes.is_none()
486 }
487
488 /// The scopes it holds, or `None` for full access.
489 pub fn granted(&self) -> Option<Vec<Scope>> {
490 self.scopes
491 .as_ref()
492 .map(|scopes| scopes.iter().filter_map(|scope| Scope::parse(scope)).collect())
493 }
494
495 pub fn allows(&self, needed: Scope) -> bool {
496 match self.granted() {
497 None => true,
498 Some(granted) => granted.iter().any(|held| held.includes(needed)),
499 }
500 }
501}
502
503/// Every operation of the API and MCP server, with the scope it needs. An
504/// operation in [`NO_SCOPE`] needs none. The API checks that every one of
505/// its operations is in exactly one of the two.
506pub const OPERATIONS: &[(&str, Scope)] = &[
507 // Your account.
508 ("list_emails", Scope::AccountRead),
509 ("add_email", Scope::AccountWrite),
510 ("remove_email", Scope::AccountWrite),
511 ("update_email_settings", Scope::AccountWrite),
512 ("list_invites", Scope::AccountRead),
513 ("create_invite", Scope::AccountWrite),
514 ("revoke_invite", Scope::AccountWrite),
515 ("list_my_repo_invitations", Scope::AccountRead),
516 ("accept_repo_invitation", Scope::AccountWrite),
517 ("decline_repo_invitation", Scope::AccountWrite),
API: pinned projects over REST and MCP518 // Your pinned projects: a preference of your account.
519 ("list_pinned_projects", Scope::AccountRead),
520 ("pin_project", Scope::AccountWrite),
521 ("unpin_project", Scope::AccountWrite),
522 ("reorder_pinned_projects", Scope::AccountWrite),
API: notifications over REST and MCP, with notifications scopes523 // Your inbox: notifications, subscriptions and watching.
524 ("list_notifications", Scope::NotificationsRead),
525 ("get_notification_thread", Scope::NotificationsRead),
526 ("get_thread_subscription", Scope::NotificationsRead),
527 ("get_repo_subscription", Scope::NotificationsRead),
528 ("list_watched_repos", Scope::NotificationsRead),
529 ("mark_notifications_read", Scope::NotificationsWrite),
530 ("mark_thread_read", Scope::NotificationsWrite),
531 ("mark_thread_done", Scope::NotificationsWrite),
532 ("save_thread", Scope::NotificationsWrite),
533 ("snooze_thread", Scope::NotificationsWrite),
534 ("set_thread_subscription", Scope::NotificationsWrite),
535 ("delete_thread_subscription", Scope::NotificationsWrite),
536 ("set_repo_subscription", Scope::NotificationsWrite),
537 ("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 step538 // Workspaces, their invites and integrations.
539 ("create_workspace", Scope::WorkspaceAdmin),
540 ("delete_workspace", Scope::WorkspaceAdmin),
Merge branch 'worktree-agent-ad7c6d88d93adc817'541 ("get_workspace", Scope::WorkspaceRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily542 ("update_workspace", Scope::WorkspaceAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step543 ("list_workspace_invites", Scope::WorkspaceRead),
544 ("invite_member", Scope::WorkspaceAdmin),
545 ("revoke_workspace_invite", Scope::WorkspaceAdmin),
546 ("list_integrations", Scope::WorkspaceRead),
547 ("connect_integration", Scope::WorkspaceAdmin),
548 ("disconnect_integration", Scope::WorkspaceAdmin),
549 ("test_integration", Scope::WorkspaceAdmin),
550 ("get_model_routes", Scope::WorkspaceRead),
551 ("set_model_routes", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar552 // Teams: reading them, and managing them. A team's role on a
553 // repository is who has access.
554 ("list_teams", Scope::WorkspaceRead),
555 ("get_team", Scope::WorkspaceRead),
556 ("list_team_members", Scope::WorkspaceRead),
557 ("list_child_teams", Scope::WorkspaceRead),
558 ("list_team_repos", Scope::WorkspaceRead),
559 ("list_user_teams", Scope::WorkspaceRead),
560 ("create_team", Scope::WorkspaceAdmin),
561 ("update_team", Scope::WorkspaceAdmin),
562 ("delete_team", Scope::WorkspaceAdmin),
563 ("set_team_member", Scope::WorkspaceAdmin),
564 ("remove_team_member", Scope::WorkspaceAdmin),
565 ("set_team_review_assignment", Scope::WorkspaceAdmin),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit566 // A workspace's billing: usage, budget, AI credit and invoices.
567 ("get_usage", Scope::BillingRead),
568 ("get_budget", Scope::BillingRead),
569 ("get_ai_credit", Scope::BillingRead),
570 ("list_invoices", Scope::BillingRead),
571 ("get_billing_details", Scope::BillingRead),
572 ("set_budget", Scope::BillingWrite),
573 ("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 step574 // Repositories.
575 ("list_repos", Scope::RepoRead),
576 ("get_repo", Scope::RepoRead),
577 ("search", Scope::RepoRead),
578 ("list_events", Scope::RepoRead),
579 ("list_labels", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar580 ("list_milestones", Scope::RepoRead),
581 ("get_milestone", Scope::RepoRead),
582 ("create_label", Scope::IssuesWrite),
583 ("update_label", Scope::IssuesWrite),
584 ("delete_label", Scope::IssuesWrite),
585 ("add_default_labels", Scope::IssuesWrite),
586 ("create_milestone", Scope::IssuesWrite),
587 ("update_milestone", Scope::IssuesWrite),
588 ("delete_milestone", Scope::IssuesWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step589 ("get_repo_settings", Scope::RepoRead),
Fast pages, required checks on the branch, self-hosted runners, honest incidents590 ("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 step591 ("list_deleted_repos", Scope::RepoRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily592 ("list_security_alerts", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar593 ("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 step594 ("create_repo", Scope::RepoWrite),
595 ("update_repo", Scope::RepoWrite),
596 ("update_repo_settings", Scope::RepoWrite),
597 ("rename_branch", Scope::RepoWrite),
598 ("rename_repo", Scope::RepoAdmin),
599 ("transfer_repo", Scope::RepoAdmin),
600 ("archive_repo", Scope::RepoAdmin),
601 ("unarchive_repo", Scope::RepoAdmin),
602 ("set_repo_visibility", Scope::RepoAdmin),
603 ("delete_repo", Scope::RepoAdmin),
604 ("restore_repo", Scope::RepoAdmin),
605 ("purge_repo", Scope::RepoAdmin),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily606 // A dismissed secret is let through push protection.
607 ("dismiss_security_alert", Scope::RepoAdmin),
608 ("reopen_security_alert", Scope::RepoAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar609 // The security suite: alerts, push protection, patterns, code
610 // scanning, the supply chain and settings.
611 ("list_secret_scanning_alerts", Scope::SecurityRead),
612 ("get_secret_scanning_alert", Scope::SecurityRead),
613 ("list_secret_scanning_locations", Scope::SecurityRead),
614 ("list_bypass_requests", Scope::SecurityRead),
615 ("list_custom_patterns", Scope::SecurityRead),
616 ("list_code_scanning_alerts", Scope::SecurityRead),
617 ("get_code_scanning_alert", Scope::SecurityRead),
618 ("list_code_scanning_analyses", Scope::SecurityRead),
619 ("get_sarif_upload", Scope::SecurityRead),
620 ("list_vulnerability_alerts", Scope::SecurityRead),
621 ("get_vulnerability_alert", Scope::SecurityRead),
622 ("get_dependency_graph", Scope::SecurityRead),
623 ("get_sbom", Scope::SecurityRead),
624 ("compare_dependencies", Scope::SecurityRead),
625 ("get_security_settings", Scope::SecurityRead),
626 ("get_workspace_security_settings", Scope::SecurityRead),
627 ("get_security_overview", Scope::SecurityRead),
628 ("update_secret_scanning_alert", Scope::SecurityWrite),
629 ("bypass_push_protection", Scope::SecurityWrite),
630 ("check_secret_validity", Scope::SecurityWrite),
631 ("review_bypass_request", Scope::SecurityWrite),
632 ("create_custom_pattern", Scope::SecurityWrite),
633 ("update_custom_pattern", Scope::SecurityWrite),
634 ("delete_custom_pattern", Scope::SecurityWrite),
635 ("dry_run_custom_pattern", Scope::SecurityWrite),
636 ("update_code_scanning_alert", Scope::SecurityWrite),
637 ("upload_sarif", Scope::SecurityWrite),
638 ("update_vulnerability_alert", Scope::SecurityWrite),
639 ("fix_security_alert", Scope::SecurityWrite),
640 ("update_security_settings", Scope::SecurityWrite),
641 ("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 step642 // Issues and plans.
643 ("list_issues", Scope::IssuesRead),
644 ("get_issue", Scope::IssuesRead),
645 ("get_plan", Scope::IssuesRead),
646 ("create_issue", Scope::IssuesWrite),
647 ("update_issue", Scope::IssuesWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar648 ("list_issue_labels", Scope::IssuesRead),
649 ("add_issue_labels", Scope::IssuesWrite),
650 ("set_issue_labels", Scope::IssuesWrite),
651 ("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 step652 ("close_issue", Scope::IssuesWrite),
653 ("reopen_issue", Scope::IssuesWrite),
654 ("add_comment", Scope::IssuesWrite),
655 ("import_issue", Scope::IssuesWrite),
656 ("apply_plan", Scope::IssuesWrite),
657 // Pull requests.
658 ("list_pull_requests", Scope::PullRequestsRead),
659 ("get_pull_request", Scope::PullRequestsRead),
660 ("get_pull_request_changes", Scope::PullRequestsRead),
661 ("read_session", Scope::PullRequestsRead),
662 ("get_merge_queue", Scope::PullRequestsRead),
663 ("create_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar664 ("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 step665 ("record_session", Scope::PullRequestsWrite),
666 ("mark_pull_request_ready", Scope::PullRequestsWrite),
667 ("close_pull_request", Scope::PullRequestsWrite),
668 ("review_pull_request", Scope::PullRequestsWrite),
669 ("merge_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar670 ("request_reviewers", Scope::PullRequestsWrite),
671 ("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 step672 // g1t's agents.
673 ("assign_issue", Scope::AgentsRun),
674 ("delegate", Scope::AgentsRun),
675 ("plan_work", Scope::AgentsRun),
676 ("message_agent", Scope::AgentsRun),
677 ("answer_message", Scope::AgentsRun),
678 ("take_messages", Scope::AgentsRun),
679 // Workflows.
680 ("list_workflows", Scope::WorkflowsRead),
681 ("list_workflow_runs", Scope::WorkflowsRead),
682 ("get_workflow_run", Scope::WorkflowsRead),
683 ("get_job_logs", Scope::WorkflowsRead),
684 ("dispatch_workflow", Scope::WorkflowsWrite),
685 ("cancel_workflow_run", Scope::WorkflowsWrite),
686 ("rerun_workflow_run", Scope::WorkflowsWrite),
687 ("update_workflow", Scope::WorkflowsWrite),
688 // Memory and the context hub.
689 ("recall", Scope::MemoryRead),
690 ("search_context", Scope::MemoryRead),
691 ("get_entity", Scope::MemoryRead),
692 ("get_context", Scope::MemoryRead),
693 ("remember", Scope::MemoryWrite),
694 // Who has access.
695 ("list_collaborators", Scope::AccessRead),
696 ("get_collaborator_permission", Scope::AccessRead),
697 ("list_repo_invitations", Scope::AccessRead),
698 ("list_outside_collaborators", Scope::AccessRead),
699 ("add_collaborator", Scope::AccessAdmin),
700 ("update_collaborator", Scope::AccessAdmin),
701 ("remove_collaborator", Scope::AccessAdmin),
702 ("revoke_repo_invitation", Scope::AccessAdmin),
703 ("set_base_permission", Scope::AccessAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar704 ("set_team_repo", Scope::AccessAdmin),
705 ("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 step706 // Webhooks.
707 ("list_webhooks", Scope::WebhooksRead),
708 ("list_webhook_deliveries", Scope::WebhooksRead),
709 ("create_webhook", Scope::WebhooksAdmin),
710 ("update_webhook", Scope::WebhooksAdmin),
711 ("delete_webhook", Scope::WebhooksAdmin),
712 ("ping_webhook", Scope::WebhooksAdmin),
713 ("redeliver_webhook", Scope::WebhooksAdmin),
714 // Secrets and variables.
715 ("list_actions_secrets", Scope::SecretsRead),
716 ("list_actions_variables", Scope::SecretsRead),
717 ("set_actions_secret", Scope::SecretsAdmin),
718 ("delete_actions_secret", Scope::SecretsAdmin),
719 ("set_actions_variable", Scope::SecretsAdmin),
720 ("delete_actions_variable", Scope::SecretsAdmin),
Fast pages, required checks on the branch, self-hosted runners, honest incidents721 // Self-hosted runners.
722 ("list_runners", Scope::RunnersRead),
723 ("list_runner_groups", Scope::RunnersRead),
724 ("get_runner_settings", Scope::RunnersRead),
725 ("create_runner_registration_token", Scope::RunnersAdmin),
726 ("remove_runner", Scope::RunnersAdmin),
727 ("create_runner_group", Scope::RunnersAdmin),
728 ("update_runner_group", Scope::RunnersAdmin),
729 ("delete_runner_group", Scope::RunnersAdmin),
730 ("update_runner_settings", Scope::RunnersAdmin),
AI Gateway: your own code calls models with a workspace token731 // The AI Gateway. Sending a request to a model needs `models:write`,
732 // checked by the model proxy at models.g1t.sh, not here.
733 ("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 step734];
735
736/// Operations any token may use: saying who it is.
737pub const NO_SCOPE: &[&str] = &["whoami"];
738
739/// The scope `operation` needs. `None` for one in [`NO_SCOPE`]; an
740/// operation in neither list needs full access.
741pub fn scope_for(operation: &str) -> Option<Scope> {
742 OPERATIONS
743 .iter()
744 .find(|(name, _)| *name == operation)
745 .map(|(_, scope)| *scope)
746}
747
748/// What a token needs for `operation` with this input beyond its own
749/// scope: starting agents from an operation that can, and making a
750/// repository public or private.
751pub fn extra_scopes(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
752 let mut extra = Vec::new();
753 let assigns = input["assign"].as_bool() == Some(true)
754 || input["agent"].as_bool() == Some(true)
755 || input["assign_agent"].as_bool() == Some(true);
756 if assigns && matches!(operation, "apply_plan" | "import_issue" | "create_issue") {
757 extra.push(Scope::AgentsRun);
758 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar759 // Fixing an alert opens an issue and puts g1t on it.
760 if operation == "fix_security_alert" {
761 extra.extend([Scope::IssuesWrite, Scope::AgentsRun]);
762 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step763 // Opening the issue an agent is put on.
764 if operation == "delegate" {
765 extra.push(Scope::IssuesWrite);
766 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily767 // A workspace's base permission is who has access.
768 if operation == "update_workspace" && input.get("base_permission").is_some_and(|v| !v.is_null()) {
769 extra.push(Scope::AccessAdmin);
770 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step771 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())) {
772 extra.push(Scope::RepoAdmin);
773 }
774 extra
775}
776
777/// The scopes a call needs, its own first.
778pub fn needed(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
779 scope_for(operation)
780 .into_iter()
781 .chain(extra_scopes(operation, input))
782 .collect()
783}
784
785/// Whether `access` may use `operation` with `input`. The person's (or
786/// workspace's) role is checked after this, by the service that owns what
787/// was asked about.
788pub fn decide(access: &TokenAccess, operation: &str, input: &serde_json::Value) -> Decision {
789 let rule = if access.legacy { "token:legacy" } else { "token:scope" };
790 if access.scopes.is_some() {
791 let known = NO_SCOPE.contains(&operation) || scope_for(operation).is_some();
792 if !known {
793 return Decision::deny("token:scope", format!("This access token cannot use {operation}: it needs full access."));
794 }
795 if let Some(missing) = needed(operation, input).into_iter().find(|scope| !access.allows(*scope)) {
796 return Decision::deny(
797 "token:scope",
798 format!("This access token needs the {} scope to use {operation}.", missing.as_str()),
799 );
800 }
801 }
802 Decision::allow(rule)
803}
804
805/// Whether a token may clone or fetch (`write` false), or push to (`write`
806/// true), a repository with git. `public` is whether anyone may read it,
807/// which needs no scope.
808pub fn decide_git(access: &TokenAccess, write: bool, public: bool) -> Decision {
809 let needed = if write { Scope::CodeWrite } else { Scope::CodeRead };
810 if !access.allows(needed) && (write || !public) {
811 return Decision::deny(
812 "token:scope",
813 format!("This access token needs the {} scope to {} with git.", needed.as_str(), if write { "push" } else { "clone or fetch a private repository" }),
814 );
815 }
816 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
817}
818
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member819/// Whether a token may pull (`Level::Read`), push or publish
820/// (`Level::Write`), or delete (`Level::Delete`) packages. `public` is
821/// whether anyone may pull the package, which needs no scope.
822pub fn decide_packages(access: &TokenAccess, level: Level, public: bool) -> Decision {
823 let (needed, doing) = match level {
824 Level::Read => (Scope::PackagesRead, "pull a private package"),
825 Level::Delete | Level::Admin => (Scope::PackagesDelete, "delete packages"),
826 Level::Write | Level::Run => (Scope::PackagesWrite, "push or publish packages"),
827 };
828 if !access.allows(needed) && !(level == Level::Read && public) {
829 return Decision::deny(
830 "token:scope",
831 format!("This access token needs the {} scope to {doing}.", needed.as_str()),
832 );
833 }
834 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
835}
836
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step837#[cfg(test)]
838mod tests {
839 use super::*;
840 use serde_json::json;
841
842 fn token(scopes: &[Scope]) -> TokenAccess {
843 TokenAccess {
844 token_id: "tok_1".to_owned(),
845 scopes: Some(scopes.iter().map(|scope| scope.as_str().to_owned()).collect()),
846 legacy: false,
AI Gateway: your own code calls models with a workspace token847 name: None,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step848 }
849 }
850
851 #[test]
852 fn every_scope_reads_back_and_belongs_to_a_resource() {
853 for scope in Scope::ALL {
854 assert_eq!(Scope::parse(scope.as_str()), Some(scope));
855 assert!(scope.as_str().starts_with(scope.resource().as_str()));
856 assert!(scope.includes(scope));
857 }
858 assert_eq!(Scope::parse(" Issues:Write "), Some(Scope::IssuesWrite));
859 assert_eq!(Scope::parse("issues"), None);
860 }
861
862 #[test]
863 fn a_higher_level_includes_the_lower_ones_of_its_resource_only() {
864 assert!(Scope::RepoAdmin.includes(Scope::RepoRead));
865 assert!(Scope::RepoAdmin.includes(Scope::RepoWrite));
866 assert!(Scope::IssuesWrite.includes(Scope::IssuesRead));
867 assert!(!Scope::IssuesRead.includes(Scope::IssuesWrite));
868 assert!(!Scope::RepoAdmin.includes(Scope::CodeWrite));
869 assert!(!Scope::PullRequestsWrite.includes(Scope::IssuesWrite));
870 }
871
872 #[test]
873 fn operations_are_listed_once_and_never_also_free() {
874 let mut seen = std::collections::HashSet::new();
875 for (name, _) in OPERATIONS {
876 assert!(seen.insert(*name), "{name} twice");
877 assert!(!NO_SCOPE.contains(name), "{name}");
878 }
879 }
880
881 #[test]
882 fn scopes_are_parsed_from_oauth_text_leaving_out_unknown_ones() {
883 assert_eq!(
884 parse_scopes("issues:write repo:read,bogus:thing issues:write"),
885 vec![Scope::RepoRead, Scope::IssuesWrite]
886 );
887 assert_eq!(scopes_text(&[Scope::RepoRead, Scope::IssuesWrite]), "repo:read issues:write");
888 }
889
890 #[test]
891 fn the_oauth_default_is_the_agent_preset_and_never_admin() {
892 let scopes = oauth_default();
893 assert!(scopes.contains(&Scope::IssuesWrite));
894 assert!(scopes.contains(&Scope::PullRequestsWrite));
895 assert!(scopes.contains(&Scope::AgentsRun));
896 assert!(scopes.iter().all(|scope| !scope.dangerous()), "{scopes:?}");
897 for read in Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents898 // Every read but the machines work runs on.
899 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 step900 }
901 assert!(Preset::ReadOnly.scopes().unwrap().iter().all(|scope| scope.level() == Level::Read));
902 assert_eq!(Preset::Full.scopes(), None);
903 }
904
905 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit906 fn billing_is_read_by_presets_and_changed_by_none_but_full_access() {
907 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::BillingRead));
908 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
909 assert!(!preset.scopes().unwrap().contains(&Scope::BillingWrite), "{}", preset.as_str());
910 }
911 assert_eq!(scope_for("set_budget"), Some(Scope::BillingWrite));
912 assert_eq!(scope_for("buy_ai_credit"), Some(Scope::BillingWrite));
913 assert_eq!(scope_for("get_usage"), Some(Scope::BillingRead));
914 let reader = token(&[Scope::BillingRead]);
915 assert!(decide(&reader, "list_invoices", &json!({})).allowed);
916 assert!(decide(&reader, "set_budget", &json!({})).reason.unwrap().contains("billing:write"));
917 }
918
919 #[test]
AI Gateway: your own code calls models with a workspace token920 fn the_ai_gateway_spends_only_with_models_write_which_no_preset_gives() {
921 // Reading the log is a read like any other.
922 assert_eq!(scope_for("list_gateway_requests"), Some(Scope::ModelsRead));
923 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::ModelsRead));
924 // Sending requests spends the workspace's AI credit: chosen on purpose.
925 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
926 assert!(!preset.scopes().unwrap().contains(&Scope::ModelsWrite), "{}", preset.as_str());
927 }
928 assert!(Scope::ModelsWrite.includes(Scope::ModelsRead));
929 assert!(!Scope::ModelsWrite.dangerous());
930 assert!(token(&[Scope::ModelsWrite]).allows(Scope::ModelsWrite));
931 assert!(!token(&[Scope::BillingWrite]).allows(Scope::ModelsWrite));
932 assert!(TokenAccess::full().allows(Scope::ModelsWrite));
933 }
934
935 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step936 fn a_legacy_token_can_do_everything() {
937 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
938 for (operation, _) in OPERATIONS {
939 assert!(decide(&legacy, operation, &json!({})).allowed, "{operation}");
940 }
941 assert_eq!(decide(&legacy, "delete_repo", &json!({})).rule, "token:legacy");
942 }
943
944 #[test]
945 fn a_missing_scope_is_named() {
946 let read = token(&[Scope::IssuesRead]);
947 assert!(decide(&read, "get_issue", &json!({})).allowed);
948 assert!(decide(&read, "whoami", &json!({})).allowed);
949 let refused = decide(&read, "create_issue", &json!({}));
950 assert!(!refused.allowed);
951 assert_eq!(refused.reason.as_deref(), Some("This access token needs the issues:write scope to use create_issue."));
952 // An operation the table does not know needs full access.
953 assert!(!decide(&read, "something_new", &json!({})).allowed);
954 }
955
956 #[test]
957 fn starting_agents_from_another_operation_needs_agents_run() {
958 let writer = token(&[Scope::IssuesWrite]);
959 assert!(decide(&writer, "apply_plan", &json!({})).allowed);
960 let refused = decide(&writer, "apply_plan", &json!({ "assign": true }));
961 assert!(refused.reason.unwrap().contains("agents:run"));
962 let maintainer = token(&[Scope::RepoWrite]);
963 assert!(decide(&maintainer, "update_repo", &json!({ "description": "x" })).allowed);
964 assert!(!decide(&maintainer, "update_repo", &json!({ "private": true })).allowed);
965 }
966
967 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily968 fn a_workspaces_base_permission_needs_access_admin_too() {
969 let admin = token(&[Scope::WorkspaceAdmin]);
970 assert!(decide(&admin, "update_workspace", &json!({ "name": "Acme" })).allowed);
971 let refused = decide(&admin, "update_workspace", &json!({ "name": "Acme", "base_permission": "read" }));
972 assert!(refused.reason.unwrap().contains("access:admin"));
973 let both = token(&[Scope::WorkspaceAdmin, Scope::AccessAdmin]);
974 assert!(decide(&both, "update_workspace", &json!({ "base_permission": "read" })).allowed);
975 assert!(!decide(&token(&[Scope::WorkspaceRead]), "update_workspace", &json!({ "name": "Acme" })).allowed);
976 }
977
978 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step979 fn delegating_needs_both_agents_and_issues() {
980 let agents = token(&[Scope::AgentsRun]);
981 assert!(decide(&agents, "delegate", &json!({})).reason.unwrap().contains("issues:write"));
982 let both = token(&[Scope::AgentsRun, Scope::IssuesWrite]);
983 assert!(decide(&both, "delegate", &json!({})).allowed);
984 }
985
986 #[test]
987 fn git_push_needs_code_write_and_private_reads_need_code_read() {
988 let reader = token(&[Scope::CodeRead]);
989 assert!(decide_git(&reader, false, false).allowed);
990 let refused = decide_git(&reader, true, false);
991 assert!(!refused.allowed);
992 assert!(refused.reason.unwrap().contains("code:write"));
993 let issues = token(&[Scope::IssuesWrite]);
994 assert!(!decide_git(&issues, false, false).allowed);
995 assert!(decide_git(&issues, false, true).allowed, "public code needs no scope");
996 assert!(!decide_git(&issues, true, true).allowed, "pushing to public code still needs code:write");
997 let writer = token(&[Scope::CodeWrite]);
998 assert!(decide_git(&writer, true, false).allowed);
999 assert!(decide_git(&writer, false, false).allowed, "code:write includes code:read");
1000 assert!(decide_git(&TokenAccess::full(), true, false).allowed);
1001 }
1002
1003 #[test]
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1004 fn packages_need_their_own_scopes_and_public_pulls_none() {
1005 let reader = token(&[Scope::PackagesRead]);
1006 assert!(decide_packages(&reader, Level::Read, false).allowed);
1007 assert!(!decide_packages(&reader, Level::Write, false).allowed);
1008 let code = token(&[Scope::CodeWrite]);
1009 assert!(!decide_packages(&code, Level::Read, false).allowed, "code scopes are not package scopes");
1010 assert!(decide_packages(&code, Level::Read, true).allowed, "public packages pull with any token");
1011 let writer = token(&[Scope::PackagesWrite]);
1012 assert!(decide_packages(&writer, Level::Write, false).allowed);
1013 assert!(decide_packages(&writer, Level::Read, false).allowed, "packages:write includes packages:read");
1014 let refused = decide_packages(&writer, Level::Delete, false);
1015 assert!(refused.reason.unwrap().contains("packages:delete"));
1016 assert!(decide_packages(&token(&[Scope::PackagesDelete]), Level::Write, false).allowed);
1017 assert!(Scope::PackagesDelete.dangerous());
1018 // Tokens made before these scopes, and full-access ones, keep working.
1019 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
1020 assert!(decide_packages(&legacy, Level::Delete, false).allowed);
1021 assert!(decide_packages(&TokenAccess::full(), Level::Write, false).allowed);
1022 }
1023
1024 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1025 fn token_access_travels_as_json() {
1026 let access = token(&[Scope::IssuesRead]);
1027 let wire = serde_json::to_value(&access).unwrap();
1028 assert_eq!(wire["scopes"], json!(["issues:read"]));
1029 assert!(wire.get("resources").is_none());
1030 let back: TokenAccess = serde_json::from_value(wire).unwrap();
1031 assert_eq!(back, access);
1032 let full: TokenAccess = serde_json::from_value(json!({})).unwrap();
1033 assert!(full.is_full());
1034 // A reach written by an older version is ignored: a token reaches
1035 // whatever its owner can.
1036 let older: TokenAccess = serde_json::from_value(json!({
1037 "token_id": "tok_1",
1038 "scopes": ["issues:read"],
1039 "resources": { "kind": "repositories", "repositories": ["acme/rocket"] },
1040 }))
1041 .unwrap();
1042 assert_eq!(older, access);
1043 }
1044
1045 /// The site's copy of the table, `packages/contracts/src/scopes.ts`,
1046 /// lists the same scopes in the same order, the same operations with
1047 /// the same scopes, and the same presets.
1048 #[test]
1049 fn the_typescript_mirror_has_the_same_table() {
1050 let ts = include_str!("../../../packages/contracts/src/scopes.ts");
1051 let section = |start: &str| {
1052 ts.split_once(start)
1053 .and_then(|(_, rest)| rest.split_once("] as const"))
1054 .map(|(table, _)| table)
1055 .unwrap_or_else(|| panic!("{start} in scopes.ts"))
1056 };
1057 let scopes: Vec<&str> = section("export const SCOPES = [")
1058 .lines()
1059 .filter_map(|line| line.split_once("scope: \"").and_then(|(_, rest)| rest.split_once('"')).map(|(scope, _)| scope))
1060 .collect();
1061 let expected: Vec<&str> = Scope::ALL.iter().map(|scope| scope.as_str()).collect();
1062 assert_eq!(scopes, expected);
1063 let operations: Vec<(String, String)> = section("export const OPERATION_SCOPES = [")
1064 .lines()
1065 .filter_map(|line| {
1066 let mut quoted = line.split('"').skip(1).step_by(2);
1067 Some((quoted.next()?.to_owned(), quoted.next()?.to_owned()))
1068 })
1069 .collect();
1070 let expected: Vec<(String, String)> = OPERATIONS
1071 .iter()
1072 .map(|(name, scope)| ((*name).to_owned(), scope.as_str().to_owned()))
1073 .collect();
1074 assert_eq!(operations, expected);
1075 for preset in Preset::ALL {
1076 let list = section(&format!("{}: [", preset.as_str()));
1077 let mirrored: Vec<&str> = list
1078 .split(',')
1079 .map(|item| item.trim().trim_matches('"'))
1080 .filter(|item| !item.is_empty())
1081 .collect();
1082 let expected: Vec<&str> = preset
1083 .scopes()
1084 .map(|scopes| scopes.iter().map(|scope| scope.as_str()).collect())
1085 .unwrap_or_else(|| vec!["*"]);
1086 assert_eq!(mirrored, expected, "{}", preset.as_str());
1087 }
1088 }
1089}

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