Skip to content

g1t/crates/contracts/src/scopes.rs

1,321 lines58,845 bytesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1//! Scopes: what an access token may do on its owner's behalf.
2//!
3//! A personal access token, a workspace's token and an application signed
4//! in with OAuth each carry a set of scopes. A token reaches whatever the
5//! one it acts as can reach: a person's token, that person's workspaces and
6//! repositories; a workspace's token, that workspace. What a request may do
7//! is the intersection of two things: the role of whoever the token acts as
8//! (see [`crate::access`]) and the token's scopes.
9//!
10//! Each scope is a resource and a level, written `resource:level`, such as
11//! `issues:write`. A higher level of a resource includes the lower ones:
12//! `repo:admin` includes `repo:write`, which includes `repo:read`.
13//!
14//! This module is the one source of truth: the API (REST and MCP) and git
15//! enforce it, and identity stores it. `packages/contracts/src/scopes.ts`
16//! mirrors the table for the site; a test keeps the two the same.
17
18use serde::{Deserialize, Serialize};
19
20use crate::credentials::Decision;
21
22/// Something a token can be given access to.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24pub enum Resource {
25 Account,
API: notifications over REST and MCP, with notifications scopes26 Notifications,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step27 Workspace,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit28 Billing,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step29 Repo,
30 Code,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar31 Security,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member32 Packages,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step33 Issues,
34 PullRequests,
35 Agents,
36 Workflows,
Merge checks: statuses and check runs on every commit37 Checks,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9738 Deployments,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step39 Memory,
40 Access,
41 Webhooks,
42 Secrets,
Fast pages, required checks on the branch, self-hosted runners, honest incidents43 Runners,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens44 Models,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step45}
46
47impl Resource {
Merge checks: statuses and check runs on every commit48 pub const ALL: [Resource; 20] = [
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step49 Resource::Repo,
50 Resource::Code,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar51 Resource::Security,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member52 Resource::Packages,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step53 Resource::Issues,
54 Resource::PullRequests,
55 Resource::Agents,
56 Resource::Workflows,
Merge checks: statuses and check runs on every commit57 Resource::Checks,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9758 Resource::Deployments,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step59 Resource::Memory,
60 Resource::Account,
API: notifications over REST and MCP, with notifications scopes61 Resource::Notifications,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step62 Resource::Workspace,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit63 Resource::Billing,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step64 Resource::Access,
65 Resource::Webhooks,
66 Resource::Secrets,
Fast pages, required checks on the branch, self-hosted runners, honest incidents67 Resource::Runners,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens68 Resource::Models,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step69 ];
70
71 pub fn as_str(self) -> &'static str {
72 match self {
73 Resource::Account => "account",
API: notifications over REST and MCP, with notifications scopes74 Resource::Notifications => "notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step75 Resource::Workspace => "workspace",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit76 Resource::Billing => "billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step77 Resource::Repo => "repo",
78 Resource::Code => "code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar79 Resource::Security => "security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member80 Resource::Packages => "packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step81 Resource::Issues => "issues",
82 Resource::PullRequests => "pull_requests",
83 Resource::Agents => "agents",
84 Resource::Workflows => "workflows",
Merge checks: statuses and check runs on every commit85 Resource::Checks => "checks",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9786 Resource::Deployments => "deployments",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step87 Resource::Memory => "memory",
88 Resource::Access => "access",
89 Resource::Webhooks => "webhooks",
90 Resource::Secrets => "secrets",
Fast pages, required checks on the branch, self-hosted runners, honest incidents91 Resource::Runners => "runners",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens92 Resource::Models => "models",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step93 }
94 }
95
96 /// Its name, for people.
97 pub fn label(self) -> &'static str {
98 match self {
99 Resource::Account => "Your account",
API: notifications over REST and MCP, with notifications scopes100 Resource::Notifications => "Notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step101 Resource::Workspace => "Workspaces",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit102 Resource::Billing => "Billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step103 Resource::Repo => "Repositories",
104 Resource::Code => "Code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar105 Resource::Security => "Security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member106 Resource::Packages => "Packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step107 Resource::Issues => "Issues",
108 Resource::PullRequests => "Pull requests",
109 Resource::Agents => "g1t agents",
110 Resource::Workflows => "Workflows",
Merge checks: statuses and check runs on every commit111 Resource::Checks => "Checks and statuses",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97112 Resource::Deployments => "Deployments",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step113 Resource::Memory => "Memory and context",
114 Resource::Access => "Who has access",
115 Resource::Webhooks => "Webhooks",
116 Resource::Secrets => "Secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents117 Resource::Runners => "Self-hosted runners",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens118 Resource::Models => "AI Gateway",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step119 }
120 }
121}
122
123/// How much of a resource.
124#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
125pub enum Level {
126 Read,
127 Write,
128 /// Starting g1t's agents, which spends the workspace's money.
129 Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member130 /// Deleting what cannot be brought back, such as a package's versions.
131 Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step132 Admin,
133}
134
135impl Level {
136 pub fn as_str(self) -> &'static str {
137 match self {
138 Level::Read => "read",
139 Level::Write => "write",
140 Level::Run => "run",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member141 Level::Delete => "delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step142 Level::Admin => "admin",
143 }
144 }
145}
146
147/// One scope. Its text form, `resource:level`, is what tokens store, OAuth
148/// clients ask for, and errors name.
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
150pub enum Scope {
151 RepoRead,
152 RepoWrite,
153 RepoAdmin,
154 CodeRead,
155 CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar156 SecurityRead,
157 SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member158 PackagesRead,
159 PackagesWrite,
160 PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step161 IssuesRead,
162 IssuesWrite,
163 PullRequestsRead,
164 PullRequestsWrite,
165 AgentsRun,
166 WorkflowsRead,
167 WorkflowsWrite,
Merge checks: statuses and check runs on every commit168 ChecksRead,
169 ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97170 DeploymentsRead,
171 DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step172 MemoryRead,
173 MemoryWrite,
174 AccountRead,
175 AccountWrite,
API: notifications over REST and MCP, with notifications scopes176 NotificationsRead,
177 NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step178 WorkspaceRead,
179 WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit180 BillingRead,
181 BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step182 AccessRead,
183 AccessAdmin,
184 WebhooksRead,
185 WebhooksAdmin,
186 SecretsRead,
187 SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents188 RunnersRead,
189 RunnersAdmin,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens190 ModelsRead,
191 ModelsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step192}
193
194impl Scope {
195 /// Every scope, grouped by resource, least first.
Merge checks: statuses and check runs on every commit196 pub const ALL: [Scope; 41] = [
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step197 Scope::RepoRead,
198 Scope::RepoWrite,
199 Scope::RepoAdmin,
200 Scope::CodeRead,
201 Scope::CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar202 Scope::SecurityRead,
203 Scope::SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member204 Scope::PackagesRead,
205 Scope::PackagesWrite,
206 Scope::PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step207 Scope::IssuesRead,
208 Scope::IssuesWrite,
209 Scope::PullRequestsRead,
210 Scope::PullRequestsWrite,
211 Scope::AgentsRun,
212 Scope::WorkflowsRead,
213 Scope::WorkflowsWrite,
Merge checks: statuses and check runs on every commit214 Scope::ChecksRead,
215 Scope::ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97216 Scope::DeploymentsRead,
217 Scope::DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step218 Scope::MemoryRead,
219 Scope::MemoryWrite,
220 Scope::AccountRead,
221 Scope::AccountWrite,
API: notifications over REST and MCP, with notifications scopes222 Scope::NotificationsRead,
223 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step224 Scope::WorkspaceRead,
225 Scope::WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit226 Scope::BillingRead,
227 Scope::BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step228 Scope::AccessRead,
229 Scope::AccessAdmin,
230 Scope::WebhooksRead,
231 Scope::WebhooksAdmin,
232 Scope::SecretsRead,
233 Scope::SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents234 Scope::RunnersRead,
235 Scope::RunnersAdmin,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens236 Scope::ModelsRead,
237 Scope::ModelsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step238 ];
239
240 pub fn as_str(self) -> &'static str {
241 match self {
242 Scope::RepoRead => "repo:read",
243 Scope::RepoWrite => "repo:write",
244 Scope::RepoAdmin => "repo:admin",
245 Scope::CodeRead => "code:read",
246 Scope::CodeWrite => "code:write",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar247 Scope::SecurityRead => "security:read",
248 Scope::SecurityWrite => "security:write",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member249 Scope::PackagesRead => "packages:read",
250 Scope::PackagesWrite => "packages:write",
251 Scope::PackagesDelete => "packages:delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step252 Scope::IssuesRead => "issues:read",
253 Scope::IssuesWrite => "issues:write",
254 Scope::PullRequestsRead => "pull_requests:read",
255 Scope::PullRequestsWrite => "pull_requests:write",
256 Scope::AgentsRun => "agents:run",
257 Scope::WorkflowsRead => "workflows:read",
258 Scope::WorkflowsWrite => "workflows:write",
Merge checks: statuses and check runs on every commit259 Scope::ChecksRead => "checks:read",
260 Scope::ChecksWrite => "checks:write",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97261 Scope::DeploymentsRead => "deployments:read",
262 Scope::DeploymentsWrite => "deployments:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step263 Scope::MemoryRead => "memory:read",
264 Scope::MemoryWrite => "memory:write",
265 Scope::AccountRead => "account:read",
266 Scope::AccountWrite => "account:write",
API: notifications over REST and MCP, with notifications scopes267 Scope::NotificationsRead => "notifications:read",
268 Scope::NotificationsWrite => "notifications:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step269 Scope::WorkspaceRead => "workspace:read",
270 Scope::WorkspaceAdmin => "workspace:admin",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit271 Scope::BillingRead => "billing:read",
272 Scope::BillingWrite => "billing:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step273 Scope::AccessRead => "access:read",
274 Scope::AccessAdmin => "access:admin",
275 Scope::WebhooksRead => "webhooks:read",
276 Scope::WebhooksAdmin => "webhooks:admin",
277 Scope::SecretsRead => "secrets:read",
278 Scope::SecretsAdmin => "secrets:admin",
Fast pages, required checks on the branch, self-hosted runners, honest incidents279 Scope::RunnersRead => "runners:read",
280 Scope::RunnersAdmin => "runners:admin",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens281 Scope::ModelsRead => "models:read",
282 Scope::ModelsWrite => "models:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step283 }
284 }
285
286 pub fn parse(text: &str) -> Option<Scope> {
287 let text = text.trim().to_ascii_lowercase();
288 Scope::ALL.into_iter().find(|scope| scope.as_str() == text)
289 }
290
291 pub fn resource(self) -> Resource {
292 let name = self.as_str().split_once(':').map_or("", |(resource, _)| resource);
293 Resource::ALL
294 .into_iter()
295 .find(|resource| resource.as_str() == name)
296 .unwrap_or(Resource::Account)
297 }
298
299 pub fn level(self) -> Level {
300 match self.as_str().rsplit_once(':').map_or("", |(_, level)| level) {
301 "write" => Level::Write,
302 "run" => Level::Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member303 "delete" => Level::Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step304 "admin" => Level::Admin,
305 _ => Level::Read,
306 }
307 }
308
309 /// Whether holding `self` gives `other`: the same resource, at the same
310 /// level or a lower one.
311 pub fn includes(self, other: Scope) -> bool {
312 self.resource() == other.resource() && self.level() >= other.level()
313 }
314
315 /// Changes that are hard or impossible to undo, or that decide who can
316 /// reach what. Shown behind a warning wherever scopes are chosen.
317 pub fn dangerous(self) -> bool {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member318 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 step319 }
320
321 /// What it lets a token do, in plain words.
322 pub fn describe(self) -> &'static str {
323 match self {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97324 Scope::RepoRead => "See repositories, their settings, labels, timelines, releases, languages, contributors and security alerts, and search",
325 Scope::RepoWrite => "Create repositories, rename branches, change how pull requests merge and publish releases",
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge326 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 step327 Scope::CodeRead => "Clone and fetch private repositories with git",
328 Scope::CodeWrite => "Push commits with git",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar329 Scope::SecurityRead => "See secret scanning, code scanning and vulnerability alerts, custom patterns, the dependency graph and SBOM, and security settings",
330 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 member331 Scope::PackagesRead => "Pull container images and install private packages",
332 Scope::PackagesWrite => "Push container images and publish packages",
333 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 step334 Scope::IssuesRead => "Read issues, comments and plans",
335 Scope::IssuesWrite => "Open, edit, close and comment on issues",
336 Scope::PullRequestsRead => "Read pull requests, their changes, sessions and merge queues",
337 Scope::PullRequestsWrite => "Open, review, close and merge pull requests",
338 Scope::AgentsRun => "Put g1t agents to work and message them, which uses the workspace's money",
339 Scope::WorkflowsRead => "Read workflows, runs and logs",
340 Scope::WorkflowsWrite => "Run, cancel, rerun and turn workflows on or off",
Merge checks: statuses and check runs on every commit341 Scope::ChecksRead => "Read commits' statuses, check runs, check suites and annotations",
342 Scope::ChecksWrite => "Report statuses and check runs on commits, and ask for checks to run again",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97343 Scope::DeploymentsRead => "See deployments, their statuses and environments",
344 Scope::DeploymentsWrite => "Report deployments and their statuses, from any CI",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step345 Scope::MemoryRead => "Recall memory and search the workspace's context",
346 Scope::MemoryWrite => "Save memory for the next agent",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97347 Scope::AccountRead => "Read your email addresses, invites, invitations, pinned projects and stars",
348 Scope::AccountWrite => "Change your email addresses, make invites, answer invitations, pin projects and star repositories",
API: notifications over REST and MCP, with notifications scopes349 Scope::NotificationsRead => "See your inbox, its threads, and what you subscribe to and watch",
350 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 merge351 Scope::WorkspaceRead => "Read workspace settings, invites, integrations, model routes, teams and rulesets",
352 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 audit353 Scope::BillingRead => "See a workspace's usage, budget, AI credit and invoices",
354 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 step355 Scope::AccessRead => "See who has access to repositories",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar356 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 step357 Scope::WebhooksRead => "See webhooks and their deliveries",
358 Scope::WebhooksAdmin => "Create, change and delete webhooks",
359 Scope::SecretsRead => "List secrets (never their values) and read variables",
360 Scope::SecretsAdmin => "Set and delete secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents361 Scope::RunnersRead => "See self-hosted runners, their groups and where agents run",
362 Scope::RunnersAdmin => "Register and remove self-hosted runners, change their groups and settings",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens363 Scope::ModelsRead => "See the workspace's AI Gateway requests: their models, tokens, cost and status",
364 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 step365 }
366 }
367}
368
369impl Serialize for Scope {
370 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
371 serializer.serialize_str(self.as_str())
372 }
373}
374
375impl<'de> Deserialize<'de> for Scope {
376 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
377 let text = String::deserialize(deserializer)?;
378 Scope::parse(&text).ok_or_else(|| serde::de::Error::custom(format!("unknown scope {text}")))
379 }
380}
381
382/// Scopes as written in a token's row or an OAuth request: separated by
383/// spaces or commas. Unknown names are left out, so a client asking for a
384/// scope from a newer version gets the rest.
385pub fn parse_scopes(text: &str) -> Vec<Scope> {
386 let mut scopes: Vec<Scope> = text
387 .split(|c: char| c.is_whitespace() || c == ',')
388 .filter_map(Scope::parse)
389 .collect();
390 normalize(&mut scopes);
391 scopes
392}
393
394/// In table order, without repeats.
395pub fn normalize(scopes: &mut Vec<Scope>) {
396 let given = std::mem::take(scopes);
397 scopes.extend(Scope::ALL.into_iter().filter(|scope| given.contains(scope)));
398}
399
400/// Space-separated, as stored and as OAuth writes them.
401pub fn scopes_text(scopes: &[Scope]) -> String {
402 scopes.iter().map(|scope| scope.as_str()).collect::<Vec<_>>().join(" ")
403}
404
405/// What a token stores for full access, which is not a scope a client can
406/// ask for by name.
407pub const FULL_ACCESS: &str = "*";
408
409/// Starting points for choosing scopes.
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
411pub enum Preset {
412 ReadOnly,
413 Agent,
414 Ci,
415 Full,
416}
417
418impl Preset {
419 pub const ALL: [Preset; 4] = [Preset::ReadOnly, Preset::Agent, Preset::Ci, Preset::Full];
420
421 pub fn as_str(self) -> &'static str {
422 match self {
423 Preset::ReadOnly => "read_only",
424 Preset::Agent => "agent",
425 Preset::Ci => "ci",
426 Preset::Full => "full",
427 }
428 }
429
430 pub fn label(self) -> &'static str {
431 match self {
432 Preset::ReadOnly => "Read only",
433 Preset::Agent => "Agent",
434 Preset::Ci => "CI",
435 Preset::Full => "Full access",
436 }
437 }
438
439 /// Its scopes; `None` for full access.
440 pub fn scopes(self) -> Option<Vec<Scope>> {
441 let reads = || Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read);
442 match self {
443 Preset::ReadOnly => Some(reads().collect()),
444 Preset::Agent => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents445 // Not the machines work runs on: an agent has no business
446 // knowing a workspace's own runners.
447 let mut scopes: Vec<Scope> = reads().filter(|scope| scope.resource() != Resource::Runners).collect();
API: notifications over REST and MCP, with notifications scopes448 // And answering what needs the person it works for: marking
449 // it done, subscribing, watching.
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step450 scopes.extend([
451 Scope::CodeWrite,
452 Scope::IssuesWrite,
453 Scope::PullRequestsWrite,
454 Scope::AgentsRun,
455 Scope::MemoryWrite,
API: notifications over REST and MCP, with notifications scopes456 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step457 ]);
458 normalize(&mut scopes);
459 Some(scopes)
460 }
461 Preset::Ci => Some(vec![
462 Scope::RepoRead,
463 Scope::CodeRead,
464 Scope::CodeWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member465 Scope::PackagesRead,
466 Scope::PackagesWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step467 Scope::WorkflowsRead,
468 Scope::WorkflowsWrite,
Merge checks: statuses and check runs on every commit469 Scope::ChecksRead,
470 Scope::ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97471 Scope::DeploymentsRead,
472 Scope::DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step473 ]),
474 Preset::Full => None,
475 }
476 }
477}
478
479/// What an OAuth client gets when it asks for nothing in particular: the
480/// agent preset. Never an admin scope.
481pub fn oauth_default() -> Vec<Scope> {
482 Preset::Agent.scopes().unwrap_or_default()
483}
484
485/// Set on a [`crate::User`] resolved from an access token: what the token
486/// may do. Absent on a signed-in session, which may do whatever its person
487/// can.
488#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
489pub struct TokenAccess {
490 /// The token's id, as audit entries and errors name it.
491 #[serde(default)]
492 pub token_id: String,
493 /// Its scopes, as `resource:level`. Absent: full access, everything the
494 /// person (or workspace) can do.
495 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub scopes: Option<Vec<String>>,
497 /// Made before tokens had scopes: full access until someone narrows it.
498 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
499 pub legacy: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'500 /// Set on a workflow job's token (`G1T_TOKEN`): the one repository it
501 /// reaches, as `owner/name`. Every other is refused, whatever its owner
502 /// could reach.
503 #[serde(default, skip_serializing_if = "Option::is_none")]
504 pub repo: Option<String>,
505 /// Set on a workflow job's token: the run and job it was made for. The
506 /// audit log records its changes as that job's, and what it changes
507 /// starts no workflows (only `workflow_dispatch` and
508 /// `repository_dispatch` do), so a workflow cannot set itself off.
509 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub job: Option<JobToken>,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens511 /// The token's name, as its owner gave it, so a log can say which
512 /// token made a request. Absent where whoever resolved it did not say.
513 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub name: Option<String>,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step515}
516
Merge branch 'worktree-agent-a3abfcce648e87dca'517/// The workflow job a token was made for.
518#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
519pub struct JobToken {
520 /// The run, `run_…`.
521 pub run_id: String,
522 /// The job, `job_…`.
523 pub job_id: String,
524 /// Whether it may open pull requests and approve them, by its
525 /// repository's and workspace's choice ("Allow g1t Actions to create and
526 /// approve pull requests"). Off unless chosen.
527 #[serde(default)]
528 pub pull_requests: bool,
529}
530
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step531impl TokenAccess {
532 /// Full access to everything: the access tokens made before scopes had.
533 pub fn full() -> Self {
534 TokenAccess::default()
535 }
536
Merge branch 'worktree-agent-a3abfcce648e87dca'537 /// Whether it may reach the repository `owner/name`: every token but a
538 /// workflow job's, which reaches its own repository only.
539 pub fn reaches(&self, repo: &str) -> bool {
540 self.repo.as_deref().is_none_or(|only| only.eq_ignore_ascii_case(repo))
541 }
542
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step543 pub fn is_full(&self) -> bool {
544 self.scopes.is_none()
545 }
546
547 /// The scopes it holds, or `None` for full access.
548 pub fn granted(&self) -> Option<Vec<Scope>> {
549 self.scopes
550 .as_ref()
551 .map(|scopes| scopes.iter().filter_map(|scope| Scope::parse(scope)).collect())
552 }
553
554 pub fn allows(&self, needed: Scope) -> bool {
555 match self.granted() {
556 None => true,
557 Some(granted) => granted.iter().any(|held| held.includes(needed)),
558 }
559 }
560}
561
562/// Every operation of the API and MCP server, with the scope it needs. An
563/// operation in [`NO_SCOPE`] needs none. The API checks that every one of
564/// its operations is in exactly one of the two.
565pub const OPERATIONS: &[(&str, Scope)] = &[
566 // Your account.
567 ("list_emails", Scope::AccountRead),
568 ("add_email", Scope::AccountWrite),
569 ("remove_email", Scope::AccountWrite),
570 ("update_email_settings", Scope::AccountWrite),
571 ("list_invites", Scope::AccountRead),
572 ("create_invite", Scope::AccountWrite),
573 ("revoke_invite", Scope::AccountWrite),
574 ("list_my_repo_invitations", Scope::AccountRead),
575 ("accept_repo_invitation", Scope::AccountWrite),
576 ("decline_repo_invitation", Scope::AccountWrite),
API: pinned projects over REST and MCP577 // Your pinned projects: a preference of your account.
578 ("list_pinned_projects", Scope::AccountRead),
579 ("pin_project", Scope::AccountWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97580 // Your stars: a preference of your account.
581 ("list_starred", Scope::AccountRead),
582 ("check_starred", Scope::AccountRead),
583 ("star_repo", Scope::AccountWrite),
584 ("unstar_repo", Scope::AccountWrite),
API: pinned projects over REST and MCP585 ("unpin_project", Scope::AccountWrite),
586 ("reorder_pinned_projects", Scope::AccountWrite),
API: notifications over REST and MCP, with notifications scopes587 // Your inbox: notifications, subscriptions and watching.
588 ("list_notifications", Scope::NotificationsRead),
589 ("get_notification_thread", Scope::NotificationsRead),
590 ("get_thread_subscription", Scope::NotificationsRead),
591 ("get_repo_subscription", Scope::NotificationsRead),
592 ("list_watched_repos", Scope::NotificationsRead),
593 ("mark_notifications_read", Scope::NotificationsWrite),
594 ("mark_thread_read", Scope::NotificationsWrite),
595 ("mark_thread_done", Scope::NotificationsWrite),
596 ("save_thread", Scope::NotificationsWrite),
597 ("snooze_thread", Scope::NotificationsWrite),
598 ("set_thread_subscription", Scope::NotificationsWrite),
599 ("delete_thread_subscription", Scope::NotificationsWrite),
600 ("set_repo_subscription", Scope::NotificationsWrite),
601 ("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 step602 // Workspaces, their invites and integrations.
603 ("create_workspace", Scope::WorkspaceAdmin),
604 ("delete_workspace", Scope::WorkspaceAdmin),
Merge branch 'worktree-agent-ad7c6d88d93adc817'605 ("get_workspace", Scope::WorkspaceRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily606 ("update_workspace", Scope::WorkspaceAdmin),
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA607 // Its members, and who owns it.
608 ("list_members", Scope::WorkspaceRead),
609 ("update_member", Scope::WorkspaceAdmin),
610 ("remove_member", Scope::WorkspaceAdmin),
611 ("transfer_ownership", Scope::WorkspaceAdmin),
612 ("leave_workspace", Scope::AccountWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step613 ("list_workspace_invites", Scope::WorkspaceRead),
614 ("invite_member", Scope::WorkspaceAdmin),
615 ("revoke_workspace_invite", Scope::WorkspaceAdmin),
616 ("list_integrations", Scope::WorkspaceRead),
617 ("connect_integration", Scope::WorkspaceAdmin),
AI Gateway: OpenAI's format, open models, and your own providers618 ("update_integration", Scope::WorkspaceAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step619 ("disconnect_integration", Scope::WorkspaceAdmin),
620 ("test_integration", Scope::WorkspaceAdmin),
621 ("get_model_routes", Scope::WorkspaceRead),
622 ("set_model_routes", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar623 // Teams: reading them, and managing them. A team's role on a
624 // repository is who has access.
625 ("list_teams", Scope::WorkspaceRead),
626 ("get_team", Scope::WorkspaceRead),
627 ("list_team_members", Scope::WorkspaceRead),
628 ("list_child_teams", Scope::WorkspaceRead),
629 ("list_team_repos", Scope::WorkspaceRead),
630 ("list_user_teams", Scope::WorkspaceRead),
631 ("create_team", Scope::WorkspaceAdmin),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge632 ("list_workspace_rulesets", Scope::WorkspaceRead),
633 ("get_workspace_ruleset", Scope::WorkspaceRead),
634 ("list_workspace_rule_evaluations", Scope::WorkspaceRead),
635 ("create_workspace_ruleset", Scope::WorkspaceAdmin),
636 ("update_workspace_ruleset", Scope::WorkspaceAdmin),
637 ("delete_workspace_ruleset", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar638 ("update_team", Scope::WorkspaceAdmin),
639 ("delete_team", Scope::WorkspaceAdmin),
640 ("set_team_member", Scope::WorkspaceAdmin),
641 ("remove_team_member", Scope::WorkspaceAdmin),
642 ("set_team_review_assignment", Scope::WorkspaceAdmin),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit643 // A workspace's billing: usage, budget, AI credit and invoices.
644 ("get_usage", Scope::BillingRead),
645 ("get_budget", Scope::BillingRead),
646 ("get_ai_credit", Scope::BillingRead),
647 ("list_invoices", Scope::BillingRead),
648 ("get_billing_details", Scope::BillingRead),
649 ("set_budget", Scope::BillingWrite),
650 ("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 step651 // Repositories.
652 ("list_repos", Scope::RepoRead),
653 ("get_repo", Scope::RepoRead),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97654 // Projects follow their repositories.
655 ("list_projects", Scope::RepoRead),
656 ("get_project", Scope::RepoRead),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step657 ("search", Scope::RepoRead),
658 ("list_events", Scope::RepoRead),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97659 // What the default branch says about a repository, who starred it, and
660 // its releases.
661 ("get_languages", Scope::RepoRead),
662 ("list_contributors", Scope::RepoRead),
663 ("get_license", Scope::RepoRead),
664 ("list_stargazers", Scope::RepoRead),
665 ("list_releases", Scope::RepoRead),
666 ("get_latest_release", Scope::RepoRead),
667 ("get_release_by_tag", Scope::RepoRead),
668 ("get_release", Scope::RepoRead),
669 ("create_release", Scope::RepoWrite),
670 ("update_release", Scope::RepoWrite),
671 ("delete_release", Scope::RepoWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step672 ("list_labels", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar673 ("list_milestones", Scope::RepoRead),
674 ("get_milestone", Scope::RepoRead),
675 ("create_label", Scope::IssuesWrite),
676 ("update_label", Scope::IssuesWrite),
677 ("delete_label", Scope::IssuesWrite),
678 ("add_default_labels", Scope::IssuesWrite),
679 ("create_milestone", Scope::IssuesWrite),
680 ("update_milestone", Scope::IssuesWrite),
681 ("delete_milestone", Scope::IssuesWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step682 ("get_repo_settings", Scope::RepoRead),
Fast pages, required checks on the branch, self-hosted runners, honest incidents683 ("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 step684 ("list_deleted_repos", Scope::RepoRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily685 ("list_security_alerts", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar686 ("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 step687 ("create_repo", Scope::RepoWrite),
688 ("update_repo", Scope::RepoWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97689 ("update_project", Scope::RepoWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step690 ("update_repo_settings", Scope::RepoWrite),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge691 // Rulesets: reading them is reading the repository; changing them
692 // changes what everyone, agents included, may do, so it is admin.
693 ("list_repo_rulesets", Scope::RepoRead),
694 ("get_repo_ruleset", Scope::RepoRead),
695 ("get_branch_rules", Scope::RepoRead),
696 ("list_rule_evaluations", Scope::RepoRead),
697 ("create_repo_ruleset", Scope::RepoAdmin),
698 ("update_repo_ruleset", Scope::RepoAdmin),
699 ("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 step700 ("rename_branch", Scope::RepoWrite),
701 ("rename_repo", Scope::RepoAdmin),
702 ("transfer_repo", Scope::RepoAdmin),
703 ("archive_repo", Scope::RepoAdmin),
704 ("unarchive_repo", Scope::RepoAdmin),
705 ("set_repo_visibility", Scope::RepoAdmin),
706 ("delete_repo", Scope::RepoAdmin),
707 ("restore_repo", Scope::RepoAdmin),
708 ("purge_repo", Scope::RepoAdmin),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily709 // A dismissed secret is let through push protection.
710 ("dismiss_security_alert", Scope::RepoAdmin),
711 ("reopen_security_alert", Scope::RepoAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar712 // The security suite: alerts, push protection, patterns, code
713 // scanning, the supply chain and settings.
714 ("list_secret_scanning_alerts", Scope::SecurityRead),
715 ("get_secret_scanning_alert", Scope::SecurityRead),
716 ("list_secret_scanning_locations", Scope::SecurityRead),
717 ("list_bypass_requests", Scope::SecurityRead),
718 ("list_custom_patterns", Scope::SecurityRead),
719 ("list_code_scanning_alerts", Scope::SecurityRead),
720 ("get_code_scanning_alert", Scope::SecurityRead),
721 ("list_code_scanning_analyses", Scope::SecurityRead),
722 ("get_sarif_upload", Scope::SecurityRead),
723 ("list_vulnerability_alerts", Scope::SecurityRead),
724 ("get_vulnerability_alert", Scope::SecurityRead),
725 ("get_dependency_graph", Scope::SecurityRead),
726 ("get_sbom", Scope::SecurityRead),
727 ("compare_dependencies", Scope::SecurityRead),
728 ("get_security_settings", Scope::SecurityRead),
729 ("get_workspace_security_settings", Scope::SecurityRead),
730 ("get_security_overview", Scope::SecurityRead),
731 ("update_secret_scanning_alert", Scope::SecurityWrite),
732 ("bypass_push_protection", Scope::SecurityWrite),
733 ("check_secret_validity", Scope::SecurityWrite),
734 ("review_bypass_request", Scope::SecurityWrite),
735 ("create_custom_pattern", Scope::SecurityWrite),
736 ("update_custom_pattern", Scope::SecurityWrite),
737 ("delete_custom_pattern", Scope::SecurityWrite),
738 ("dry_run_custom_pattern", Scope::SecurityWrite),
739 ("update_code_scanning_alert", Scope::SecurityWrite),
740 ("upload_sarif", Scope::SecurityWrite),
741 ("update_vulnerability_alert", Scope::SecurityWrite),
742 ("fix_security_alert", Scope::SecurityWrite),
743 ("update_security_settings", Scope::SecurityWrite),
744 ("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 step745 // Issues and plans.
746 ("list_issues", Scope::IssuesRead),
747 ("get_issue", Scope::IssuesRead),
748 ("get_plan", Scope::IssuesRead),
749 ("create_issue", Scope::IssuesWrite),
750 ("update_issue", Scope::IssuesWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar751 ("list_issue_labels", Scope::IssuesRead),
752 ("add_issue_labels", Scope::IssuesWrite),
753 ("set_issue_labels", Scope::IssuesWrite),
754 ("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 step755 ("close_issue", Scope::IssuesWrite),
756 ("reopen_issue", Scope::IssuesWrite),
757 ("add_comment", Scope::IssuesWrite),
758 ("import_issue", Scope::IssuesWrite),
759 ("apply_plan", Scope::IssuesWrite),
760 // Pull requests.
761 ("list_pull_requests", Scope::PullRequestsRead),
762 ("get_pull_request", Scope::PullRequestsRead),
763 ("get_pull_request_changes", Scope::PullRequestsRead),
764 ("read_session", Scope::PullRequestsRead),
765 ("get_merge_queue", Scope::PullRequestsRead),
766 ("create_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar767 ("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 step768 ("record_session", Scope::PullRequestsWrite),
769 ("mark_pull_request_ready", Scope::PullRequestsWrite),
770 ("close_pull_request", Scope::PullRequestsWrite),
771 ("review_pull_request", Scope::PullRequestsWrite),
772 ("merge_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar773 ("request_reviewers", Scope::PullRequestsWrite),
774 ("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 step775 // g1t's agents.
776 ("assign_issue", Scope::AgentsRun),
777 ("delegate", Scope::AgentsRun),
778 ("plan_work", Scope::AgentsRun),
779 ("message_agent", Scope::AgentsRun),
780 ("answer_message", Scope::AgentsRun),
781 ("take_messages", Scope::AgentsRun),
782 // Workflows.
783 ("list_workflows", Scope::WorkflowsRead),
784 ("list_workflow_runs", Scope::WorkflowsRead),
785 ("get_workflow_run", Scope::WorkflowsRead),
786 ("get_job_logs", Scope::WorkflowsRead),
787 ("dispatch_workflow", Scope::WorkflowsWrite),
788 ("cancel_workflow_run", Scope::WorkflowsWrite),
789 ("rerun_workflow_run", Scope::WorkflowsWrite),
790 ("update_workflow", Scope::WorkflowsWrite),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2791 ("list_artifacts", Scope::WorkflowsRead),
792 ("list_workflow_run_artifacts", Scope::WorkflowsRead),
793 ("get_artifact", Scope::WorkflowsRead),
794 ("download_artifact", Scope::WorkflowsRead),
795 ("get_artifact_retention", Scope::WorkflowsRead),
796 ("delete_artifact", Scope::WorkflowsWrite),
797 ("set_artifact_retention", Scope::WorkflowsWrite),
Merge checks: statuses and check runs on every commit798 // Checks: statuses, check runs and check suites on commits.
799 ("list_commit_statuses", Scope::ChecksRead),
800 ("get_combined_status", Scope::ChecksRead),
801 ("list_check_runs_for_ref", Scope::ChecksRead),
802 ("get_check_run", Scope::ChecksRead),
803 ("list_check_run_annotations", Scope::ChecksRead),
804 ("list_check_suites_for_ref", Scope::ChecksRead),
805 ("get_check_suite", Scope::ChecksRead),
806 ("create_commit_status", Scope::ChecksWrite),
807 ("create_check_run", Scope::ChecksWrite),
808 ("update_check_run", Scope::ChecksWrite),
809 ("rerequest_check_run", Scope::ChecksWrite),
810 ("rerequest_check_suite", Scope::ChecksWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97811 // Deployments, wherever they run: reading them, and reporting them.
812 ("list_deployments", Scope::DeploymentsRead),
813 ("get_deployment", Scope::DeploymentsRead),
814 ("list_deployment_statuses", Scope::DeploymentsRead),
815 ("list_environments", Scope::DeploymentsRead),
816 ("get_environment", Scope::DeploymentsRead),
817 ("create_deployment", Scope::DeploymentsWrite),
818 ("create_deployment_status", Scope::DeploymentsWrite),
Merge branch 'worktree-agent-a3abfcce648e87dca'819 // What keeps runs safe: the runs environments hold and reviewing them,
820 // approving a pull request's run, and a repository's own rules for
821 // its environments and tokens, which are an admin's.
822 ("get_pending_deployments", Scope::WorkflowsRead),
823 ("review_pending_deployments", Scope::WorkflowsWrite),
824 ("approve_workflow_run", Scope::WorkflowsWrite),
825 ("get_workflow_permissions", Scope::RepoRead),
826 ("get_fork_pr_approval", Scope::RepoRead),
827 ("update_environment", Scope::RepoAdmin),
828 ("delete_environment", Scope::RepoAdmin),
829 ("set_workflow_permissions", Scope::RepoAdmin),
830 ("set_fork_pr_approval", Scope::RepoAdmin),
831 // Starting workflows from outside, as a push would.
832 ("create_repository_dispatch", Scope::CodeWrite),
833 // A workspace's policy for its repositories' tokens.
834 ("get_workspace_workflow_permissions", Scope::WorkspaceRead),
835 ("set_workspace_workflow_permissions", Scope::WorkspaceAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step836 // Memory and the context hub.
837 ("recall", Scope::MemoryRead),
838 ("search_context", Scope::MemoryRead),
839 ("get_entity", Scope::MemoryRead),
840 ("get_context", Scope::MemoryRead),
841 ("remember", Scope::MemoryWrite),
842 // Who has access.
843 ("list_collaborators", Scope::AccessRead),
844 ("get_collaborator_permission", Scope::AccessRead),
845 ("list_repo_invitations", Scope::AccessRead),
846 ("list_outside_collaborators", Scope::AccessRead),
847 ("add_collaborator", Scope::AccessAdmin),
848 ("update_collaborator", Scope::AccessAdmin),
849 ("remove_collaborator", Scope::AccessAdmin),
850 ("revoke_repo_invitation", Scope::AccessAdmin),
851 ("set_base_permission", Scope::AccessAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar852 ("set_team_repo", Scope::AccessAdmin),
853 ("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 step854 // Webhooks.
855 ("list_webhooks", Scope::WebhooksRead),
856 ("list_webhook_deliveries", Scope::WebhooksRead),
857 ("create_webhook", Scope::WebhooksAdmin),
858 ("update_webhook", Scope::WebhooksAdmin),
859 ("delete_webhook", Scope::WebhooksAdmin),
860 ("ping_webhook", Scope::WebhooksAdmin),
861 ("redeliver_webhook", Scope::WebhooksAdmin),
862 // Secrets and variables.
863 ("list_actions_secrets", Scope::SecretsRead),
864 ("list_actions_variables", Scope::SecretsRead),
865 ("set_actions_secret", Scope::SecretsAdmin),
866 ("delete_actions_secret", Scope::SecretsAdmin),
867 ("set_actions_variable", Scope::SecretsAdmin),
868 ("delete_actions_variable", Scope::SecretsAdmin),
Fast pages, required checks on the branch, self-hosted runners, honest incidents869 // Self-hosted runners.
870 ("list_runners", Scope::RunnersRead),
871 ("list_runner_groups", Scope::RunnersRead),
872 ("get_runner_settings", Scope::RunnersRead),
873 ("create_runner_registration_token", Scope::RunnersAdmin),
874 ("remove_runner", Scope::RunnersAdmin),
875 ("create_runner_group", Scope::RunnersAdmin),
876 ("update_runner_group", Scope::RunnersAdmin),
877 ("delete_runner_group", Scope::RunnersAdmin),
878 ("update_runner_settings", Scope::RunnersAdmin),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens879 // The AI Gateway. Sending a request to a model needs `models:write`,
880 // checked by the model proxy at models.g1t.sh, not here.
881 ("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 step882];
883
884/// Operations any token may use: saying who it is.
885pub const NO_SCOPE: &[&str] = &["whoami"];
886
887/// The scope `operation` needs. `None` for one in [`NO_SCOPE`]; an
888/// operation in neither list needs full access.
889pub fn scope_for(operation: &str) -> Option<Scope> {
890 OPERATIONS
891 .iter()
892 .find(|(name, _)| *name == operation)
893 .map(|(_, scope)| *scope)
894}
895
896/// What a token needs for `operation` with this input beyond its own
897/// scope: starting agents from an operation that can, and making a
898/// repository public or private.
899pub fn extra_scopes(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
900 let mut extra = Vec::new();
901 let assigns = input["assign"].as_bool() == Some(true)
902 || input["agent"].as_bool() == Some(true)
903 || input["assign_agent"].as_bool() == Some(true);
904 if assigns && matches!(operation, "apply_plan" | "import_issue" | "create_issue") {
905 extra.push(Scope::AgentsRun);
906 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar907 // Fixing an alert opens an issue and puts g1t on it.
908 if operation == "fix_security_alert" {
909 extra.extend([Scope::IssuesWrite, Scope::AgentsRun]);
910 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step911 // Opening the issue an agent is put on.
912 if operation == "delegate" {
913 extra.push(Scope::IssuesWrite);
914 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily915 // A workspace's base permission is who has access.
916 if operation == "update_workspace" && input.get("base_permission").is_some_and(|v| !v.is_null()) {
917 extra.push(Scope::AccessAdmin);
918 }
Merge checks: statuses and check runs on every commit919 // Asking a g1t Actions job or run to run again reruns its workflow.
920 if matches!(operation, "rerequest_check_run" | "rerequest_check_suite")
921 && input["id"].as_str().is_some_and(|id| id.starts_with("job_") || id.starts_with("run_"))
922 {
923 extra.push(Scope::WorkflowsWrite);
924 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step925 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())) {
926 extra.push(Scope::RepoAdmin);
927 }
928 extra
929}
930
931/// The scopes a call needs, its own first.
932pub fn needed(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
933 scope_for(operation)
934 .into_iter()
935 .chain(extra_scopes(operation, input))
936 .collect()
937}
938
939/// Whether `access` may use `operation` with `input`. The person's (or
940/// workspace's) role is checked after this, by the service that owns what
941/// was asked about.
942pub fn decide(access: &TokenAccess, operation: &str, input: &serde_json::Value) -> Decision {
943 let rule = if access.legacy { "token:legacy" } else { "token:scope" };
Merge branch 'worktree-agent-a3abfcce648e87dca'944 // A workflow job may open or approve pull requests only where its
945 // repository and workspace let it, as on GitHub.
946 if let Some(job) = &access.job
947 && !job.pull_requests
948 && (operation == "create_pull_request" || (operation == "review_pull_request" && input["verdict"].as_str() == Some("approve")))
949 {
950 return Decision::deny(
951 "token:pull-requests",
952 "A workflow job cannot open or approve pull requests here: an admin can allow it under Settings, Actions.",
953 );
954 }
955 if let Some(only) = access.repo.as_deref()
956 && !NO_SCOPE.contains(&operation)
957 {
958 match input["repo"].as_str() {
959 Some(repo) if access.reaches(repo) => {}
960 Some(repo) => {
961 return Decision::deny("token:repository", format!("This token is a workflow job's in {only}: it cannot reach {repo}."));
962 }
963 None => {
964 return Decision::deny("token:repository", format!("This token is a workflow job's: it reaches only {only}, and {operation} is not about one repository."));
965 }
966 }
967 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step968 if access.scopes.is_some() {
969 let known = NO_SCOPE.contains(&operation) || scope_for(operation).is_some();
970 if !known {
971 return Decision::deny("token:scope", format!("This access token cannot use {operation}: it needs full access."));
972 }
973 if let Some(missing) = needed(operation, input).into_iter().find(|scope| !access.allows(*scope)) {
974 return Decision::deny(
975 "token:scope",
976 format!("This access token needs the {} scope to use {operation}.", missing.as_str()),
977 );
978 }
979 }
980 Decision::allow(rule)
981}
982
Merge branch 'worktree-agent-a3abfcce648e87dca'983/// Whether a token may use the repository `owner/name` at all: a refusal
984/// for a workflow job's token in another repository, else `None`. Git and
985/// the package registries ask this before [`decide_git`] and
986/// [`decide_packages`].
987pub fn decide_repo(access: &TokenAccess, repo: &str) -> Option<Decision> {
988 let only = access.repo.as_deref()?;
989 (!access.reaches(repo)).then(|| Decision::deny("token:repository", format!("This token is a workflow job's in {only}: it cannot reach {repo}.")))
990}
991
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step992/// Whether a token may clone or fetch (`write` false), or push to (`write`
993/// true), a repository with git. `public` is whether anyone may read it,
994/// which needs no scope.
995pub fn decide_git(access: &TokenAccess, write: bool, public: bool) -> Decision {
996 let needed = if write { Scope::CodeWrite } else { Scope::CodeRead };
997 if !access.allows(needed) && (write || !public) {
998 return Decision::deny(
999 "token:scope",
1000 format!("This access token needs the {} scope to {} with git.", needed.as_str(), if write { "push" } else { "clone or fetch a private repository" }),
1001 );
1002 }
1003 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
1004}
1005
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1006/// Whether a token may pull (`Level::Read`), push or publish
1007/// (`Level::Write`), or delete (`Level::Delete`) packages. `public` is
1008/// whether anyone may pull the package, which needs no scope.
1009pub fn decide_packages(access: &TokenAccess, level: Level, public: bool) -> Decision {
1010 let (needed, doing) = match level {
1011 Level::Read => (Scope::PackagesRead, "pull a private package"),
1012 Level::Delete | Level::Admin => (Scope::PackagesDelete, "delete packages"),
1013 Level::Write | Level::Run => (Scope::PackagesWrite, "push or publish packages"),
1014 };
1015 if !access.allows(needed) && !(level == Level::Read && public) {
1016 return Decision::deny(
1017 "token:scope",
1018 format!("This access token needs the {} scope to {doing}.", needed.as_str()),
1019 );
1020 }
1021 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
1022}
1023
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1024#[cfg(test)]
1025mod tests {
1026 use super::*;
1027 use serde_json::json;
1028
1029 fn token(scopes: &[Scope]) -> TokenAccess {
1030 TokenAccess {
1031 token_id: "tok_1".to_owned(),
1032 scopes: Some(scopes.iter().map(|scope| scope.as_str().to_owned()).collect()),
1033 legacy: false,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1034 name: None,
Merge branch 'worktree-agent-a3abfcce648e87dca'1035 ..TokenAccess::default()
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1036 }
1037 }
1038
1039 #[test]
1040 fn every_scope_reads_back_and_belongs_to_a_resource() {
1041 for scope in Scope::ALL {
1042 assert_eq!(Scope::parse(scope.as_str()), Some(scope));
1043 assert!(scope.as_str().starts_with(scope.resource().as_str()));
1044 assert!(scope.includes(scope));
1045 }
1046 assert_eq!(Scope::parse(" Issues:Write "), Some(Scope::IssuesWrite));
1047 assert_eq!(Scope::parse("issues"), None);
1048 }
1049
1050 #[test]
1051 fn a_higher_level_includes_the_lower_ones_of_its_resource_only() {
1052 assert!(Scope::RepoAdmin.includes(Scope::RepoRead));
1053 assert!(Scope::RepoAdmin.includes(Scope::RepoWrite));
1054 assert!(Scope::IssuesWrite.includes(Scope::IssuesRead));
1055 assert!(!Scope::IssuesRead.includes(Scope::IssuesWrite));
1056 assert!(!Scope::RepoAdmin.includes(Scope::CodeWrite));
1057 assert!(!Scope::PullRequestsWrite.includes(Scope::IssuesWrite));
1058 }
1059
1060 #[test]
1061 fn operations_are_listed_once_and_never_also_free() {
1062 let mut seen = std::collections::HashSet::new();
1063 for (name, _) in OPERATIONS {
1064 assert!(seen.insert(*name), "{name} twice");
1065 assert!(!NO_SCOPE.contains(name), "{name}");
1066 }
1067 }
1068
1069 #[test]
1070 fn scopes_are_parsed_from_oauth_text_leaving_out_unknown_ones() {
1071 assert_eq!(
1072 parse_scopes("issues:write repo:read,bogus:thing issues:write"),
1073 vec![Scope::RepoRead, Scope::IssuesWrite]
1074 );
1075 assert_eq!(scopes_text(&[Scope::RepoRead, Scope::IssuesWrite]), "repo:read issues:write");
1076 }
1077
1078 #[test]
1079 fn the_oauth_default_is_the_agent_preset_and_never_admin() {
1080 let scopes = oauth_default();
1081 assert!(scopes.contains(&Scope::IssuesWrite));
1082 assert!(scopes.contains(&Scope::PullRequestsWrite));
1083 assert!(scopes.contains(&Scope::AgentsRun));
1084 assert!(scopes.iter().all(|scope| !scope.dangerous()), "{scopes:?}");
1085 for read in Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents1086 // Every read but the machines work runs on.
1087 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 step1088 }
1089 assert!(Preset::ReadOnly.scopes().unwrap().iter().all(|scope| scope.level() == Level::Read));
1090 assert_eq!(Preset::Full.scopes(), None);
1091 }
1092
1093 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1094 fn billing_is_read_by_presets_and_changed_by_none_but_full_access() {
1095 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::BillingRead));
1096 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
1097 assert!(!preset.scopes().unwrap().contains(&Scope::BillingWrite), "{}", preset.as_str());
1098 }
1099 assert_eq!(scope_for("set_budget"), Some(Scope::BillingWrite));
1100 assert_eq!(scope_for("buy_ai_credit"), Some(Scope::BillingWrite));
1101 assert_eq!(scope_for("get_usage"), Some(Scope::BillingRead));
1102 let reader = token(&[Scope::BillingRead]);
1103 assert!(decide(&reader, "list_invoices", &json!({})).allowed);
1104 assert!(decide(&reader, "set_budget", &json!({})).reason.unwrap().contains("billing:write"));
1105 }
1106
1107 #[test]
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1108 fn the_ai_gateway_spends_only_with_models_write_which_no_preset_gives() {
1109 // Reading the log is a read like any other.
1110 assert_eq!(scope_for("list_gateway_requests"), Some(Scope::ModelsRead));
1111 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::ModelsRead));
1112 // Sending requests spends the workspace's AI credit: chosen on purpose.
1113 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
1114 assert!(!preset.scopes().unwrap().contains(&Scope::ModelsWrite), "{}", preset.as_str());
1115 }
1116 assert!(Scope::ModelsWrite.includes(Scope::ModelsRead));
1117 assert!(!Scope::ModelsWrite.dangerous());
1118 assert!(token(&[Scope::ModelsWrite]).allows(Scope::ModelsWrite));
1119 assert!(!token(&[Scope::BillingWrite]).allows(Scope::ModelsWrite));
1120 assert!(TokenAccess::full().allows(Scope::ModelsWrite));
1121 }
1122
1123 #[test]
Merge checks: statuses and check runs on every commit1124 fn checks_are_reported_with_checks_write_which_ci_gets() {
1125 assert_eq!(scope_for("create_check_run"), Some(Scope::ChecksWrite));
1126 assert_eq!(scope_for("create_commit_status"), Some(Scope::ChecksWrite));
1127 assert_eq!(scope_for("list_check_runs_for_ref"), Some(Scope::ChecksRead));
1128 let ci = Preset::Ci.scopes().unwrap();
1129 assert!(ci.contains(&Scope::ChecksWrite));
1130 assert!(!Preset::Agent.scopes().unwrap().contains(&Scope::ChecksWrite));
1131 let reporter = token(&[Scope::ChecksWrite]);
1132 assert!(decide(&reporter, "update_check_run", &json!({ "id": "cr_1" })).allowed);
1133 assert!(decide(&reporter, "rerequest_check_run", &json!({ "id": "cr_1" })).allowed);
1134 // A g1t Actions job runs again as its workflow does.
1135 let refused = decide(&reporter, "rerequest_check_run", &json!({ "id": "job_1" }));
1136 assert!(refused.reason.unwrap().contains("workflows:write"));
1137 }
1138
1139 #[test]
Merge branch 'worktree-agent-a3abfcce648e87dca'1140 fn a_job_token_reaches_its_repository_only() {
1141 let job = TokenAccess {
1142 repo: Some("acme/web".into()),
1143 job: Some(JobToken { run_id: "run_1".into(), job_id: "job_1".into(), pull_requests: false }),
1144 ..token(&[Scope::RepoRead, Scope::IssuesWrite, Scope::IssuesRead, Scope::PullRequestsWrite])
1145 };
1146 assert!(decide(&job, "create_issue", &json!({ "repo": "acme/web" })).allowed);
1147 assert!(decide(&job, "create_issue", &json!({ "repo": "Acme/Web" })).allowed, "names compare without case");
1148 let elsewhere = decide(&job, "create_issue", &json!({ "repo": "acme/api" }));
1149 assert!(!elsewhere.allowed);
1150 assert_eq!(elsewhere.rule, "token:repository");
1151 // Nothing beyond the one repository, a workspace's listing included.
1152 assert!(!decide(&job, "list_repos", &json!({})).allowed);
1153 assert!(decide(&job, "whoami", &json!({})).allowed);
1154 // Its scopes still hold inside it.
1155 assert!(!decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).allowed);
1156 assert!(decide_repo(&job, "acme/web").is_none());
1157 assert!(!decide_repo(&job, "acme/api").unwrap().allowed);
1158 assert!(decide_repo(&token(&[Scope::CodeRead]), "acme/api").is_none(), "other tokens reach what their owner can");
1159 // Opening and approving pull requests is off unless allowed.
1160 assert_eq!(decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).rule, "token:pull-requests");
1161 assert!(!decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "approve" })).allowed);
1162 assert!(decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "request_changes" })).allowed);
1163 let allowed = TokenAccess { job: Some(JobToken { pull_requests: true, ..job.job.clone().unwrap() }), ..job.clone() };
1164 assert!(decide(&allowed, "create_pull_request", &json!({ "repo": "acme/web" })).allowed);
1165 }
1166
1167 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1168 fn a_legacy_token_can_do_everything() {
1169 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
1170 for (operation, _) in OPERATIONS {
1171 assert!(decide(&legacy, operation, &json!({})).allowed, "{operation}");
1172 }
1173 assert_eq!(decide(&legacy, "delete_repo", &json!({})).rule, "token:legacy");
1174 }
1175
1176 #[test]
1177 fn a_missing_scope_is_named() {
1178 let read = token(&[Scope::IssuesRead]);
1179 assert!(decide(&read, "get_issue", &json!({})).allowed);
1180 assert!(decide(&read, "whoami", &json!({})).allowed);
1181 let refused = decide(&read, "create_issue", &json!({}));
1182 assert!(!refused.allowed);
1183 assert_eq!(refused.reason.as_deref(), Some("This access token needs the issues:write scope to use create_issue."));
1184 // An operation the table does not know needs full access.
1185 assert!(!decide(&read, "something_new", &json!({})).allowed);
1186 }
1187
1188 #[test]
1189 fn starting_agents_from_another_operation_needs_agents_run() {
1190 let writer = token(&[Scope::IssuesWrite]);
1191 assert!(decide(&writer, "apply_plan", &json!({})).allowed);
1192 let refused = decide(&writer, "apply_plan", &json!({ "assign": true }));
1193 assert!(refused.reason.unwrap().contains("agents:run"));
1194 let maintainer = token(&[Scope::RepoWrite]);
1195 assert!(decide(&maintainer, "update_repo", &json!({ "description": "x" })).allowed);
1196 assert!(!decide(&maintainer, "update_repo", &json!({ "private": true })).allowed);
1197 }
1198
1199 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1200 fn a_workspaces_base_permission_needs_access_admin_too() {
1201 let admin = token(&[Scope::WorkspaceAdmin]);
1202 assert!(decide(&admin, "update_workspace", &json!({ "name": "Acme" })).allowed);
1203 let refused = decide(&admin, "update_workspace", &json!({ "name": "Acme", "base_permission": "read" }));
1204 assert!(refused.reason.unwrap().contains("access:admin"));
1205 let both = token(&[Scope::WorkspaceAdmin, Scope::AccessAdmin]);
1206 assert!(decide(&both, "update_workspace", &json!({ "base_permission": "read" })).allowed);
1207 assert!(!decide(&token(&[Scope::WorkspaceRead]), "update_workspace", &json!({ "name": "Acme" })).allowed);
1208 }
1209
1210 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1211 fn delegating_needs_both_agents_and_issues() {
1212 let agents = token(&[Scope::AgentsRun]);
1213 assert!(decide(&agents, "delegate", &json!({})).reason.unwrap().contains("issues:write"));
1214 let both = token(&[Scope::AgentsRun, Scope::IssuesWrite]);
1215 assert!(decide(&both, "delegate", &json!({})).allowed);
1216 }
1217
1218 #[test]
1219 fn git_push_needs_code_write_and_private_reads_need_code_read() {
1220 let reader = token(&[Scope::CodeRead]);
1221 assert!(decide_git(&reader, false, false).allowed);
1222 let refused = decide_git(&reader, true, false);
1223 assert!(!refused.allowed);
1224 assert!(refused.reason.unwrap().contains("code:write"));
1225 let issues = token(&[Scope::IssuesWrite]);
1226 assert!(!decide_git(&issues, false, false).allowed);
1227 assert!(decide_git(&issues, false, true).allowed, "public code needs no scope");
1228 assert!(!decide_git(&issues, true, true).allowed, "pushing to public code still needs code:write");
1229 let writer = token(&[Scope::CodeWrite]);
1230 assert!(decide_git(&writer, true, false).allowed);
1231 assert!(decide_git(&writer, false, false).allowed, "code:write includes code:read");
1232 assert!(decide_git(&TokenAccess::full(), true, false).allowed);
1233 }
1234
1235 #[test]
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1236 fn packages_need_their_own_scopes_and_public_pulls_none() {
1237 let reader = token(&[Scope::PackagesRead]);
1238 assert!(decide_packages(&reader, Level::Read, false).allowed);
1239 assert!(!decide_packages(&reader, Level::Write, false).allowed);
1240 let code = token(&[Scope::CodeWrite]);
1241 assert!(!decide_packages(&code, Level::Read, false).allowed, "code scopes are not package scopes");
1242 assert!(decide_packages(&code, Level::Read, true).allowed, "public packages pull with any token");
1243 let writer = token(&[Scope::PackagesWrite]);
1244 assert!(decide_packages(&writer, Level::Write, false).allowed);
1245 assert!(decide_packages(&writer, Level::Read, false).allowed, "packages:write includes packages:read");
1246 let refused = decide_packages(&writer, Level::Delete, false);
1247 assert!(refused.reason.unwrap().contains("packages:delete"));
1248 assert!(decide_packages(&token(&[Scope::PackagesDelete]), Level::Write, false).allowed);
1249 assert!(Scope::PackagesDelete.dangerous());
1250 // Tokens made before these scopes, and full-access ones, keep working.
1251 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
1252 assert!(decide_packages(&legacy, Level::Delete, false).allowed);
1253 assert!(decide_packages(&TokenAccess::full(), Level::Write, false).allowed);
1254 }
1255
1256 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1257 fn token_access_travels_as_json() {
1258 let access = token(&[Scope::IssuesRead]);
1259 let wire = serde_json::to_value(&access).unwrap();
1260 assert_eq!(wire["scopes"], json!(["issues:read"]));
1261 assert!(wire.get("resources").is_none());
1262 let back: TokenAccess = serde_json::from_value(wire).unwrap();
1263 assert_eq!(back, access);
1264 let full: TokenAccess = serde_json::from_value(json!({})).unwrap();
1265 assert!(full.is_full());
1266 // A reach written by an older version is ignored: a token reaches
1267 // whatever its owner can.
1268 let older: TokenAccess = serde_json::from_value(json!({
1269 "token_id": "tok_1",
1270 "scopes": ["issues:read"],
1271 "resources": { "kind": "repositories", "repositories": ["acme/rocket"] },
1272 }))
1273 .unwrap();
1274 assert_eq!(older, access);
1275 }
1276
1277 /// The site's copy of the table, `packages/contracts/src/scopes.ts`,
1278 /// lists the same scopes in the same order, the same operations with
1279 /// the same scopes, and the same presets.
1280 #[test]
1281 fn the_typescript_mirror_has_the_same_table() {
1282 let ts = include_str!("../../../packages/contracts/src/scopes.ts");
1283 let section = |start: &str| {
1284 ts.split_once(start)
1285 .and_then(|(_, rest)| rest.split_once("] as const"))
1286 .map(|(table, _)| table)
1287 .unwrap_or_else(|| panic!("{start} in scopes.ts"))
1288 };
1289 let scopes: Vec<&str> = section("export const SCOPES = [")
1290 .lines()
1291 .filter_map(|line| line.split_once("scope: \"").and_then(|(_, rest)| rest.split_once('"')).map(|(scope, _)| scope))
1292 .collect();
1293 let expected: Vec<&str> = Scope::ALL.iter().map(|scope| scope.as_str()).collect();
1294 assert_eq!(scopes, expected);
1295 let operations: Vec<(String, String)> = section("export const OPERATION_SCOPES = [")
1296 .lines()
1297 .filter_map(|line| {
1298 let mut quoted = line.split('"').skip(1).step_by(2);
1299 Some((quoted.next()?.to_owned(), quoted.next()?.to_owned()))
1300 })
1301 .collect();
1302 let expected: Vec<(String, String)> = OPERATIONS
1303 .iter()
1304 .map(|(name, scope)| ((*name).to_owned(), scope.as_str().to_owned()))
1305 .collect();
1306 assert_eq!(operations, expected);
1307 for preset in Preset::ALL {
1308 let list = section(&format!("{}: [", preset.as_str()));
1309 let mirrored: Vec<&str> = list
1310 .split(',')
1311 .map(|item| item.trim().trim_matches('"'))
1312 .filter(|item| !item.is_empty())
1313 .collect();
1314 let expected: Vec<&str> = preset
1315 .scopes()
1316 .map(|scopes| scopes.iter().map(|scope| scope.as_str()).collect())
1317 .unwrap_or_else(|| vec!["*"]);
1318 assert_eq!(mirrored, expected, "{}", preset.as_str());
1319 }
1320 }
1321}

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