Skip to content
1,838 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,
Token reach: workflow_files scope, fine-grained reach, workspace token cap37 WorkflowFiles,
Merge checks: statuses and check runs on every commit38 Checks,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9739 Deployments,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step40 Memory,
41 Access,
42 Webhooks,
43 Secrets,
Fast pages, required checks on the branch, self-hosted runners, honest incidents44 Runners,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens45 Models,
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet46 /// Artifacts mode's docs, slides, designs and dashboards (folios in
47 /// code). Not offered yet: see [`Resource::offered`].
48 Artifacts,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step49}
50
51impl Resource {
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet52 pub const ALL: [Resource; 22] = [
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step53 Resource::Repo,
54 Resource::Code,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar55 Resource::Security,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member56 Resource::Packages,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step57 Resource::Issues,
58 Resource::PullRequests,
59 Resource::Agents,
60 Resource::Workflows,
Token reach: workflow_files scope, fine-grained reach, workspace token cap61 Resource::WorkflowFiles,
Merge checks: statuses and check runs on every commit62 Resource::Checks,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9763 Resource::Deployments,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step64 Resource::Memory,
65 Resource::Account,
API: notifications over REST and MCP, with notifications scopes66 Resource::Notifications,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step67 Resource::Workspace,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit68 Resource::Billing,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step69 Resource::Access,
70 Resource::Webhooks,
71 Resource::Secrets,
Fast pages, required checks on the branch, self-hosted runners, honest incidents72 Resource::Runners,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens73 Resource::Models,
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet74 Resource::Artifacts,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step75 ];
76
77 pub fn as_str(self) -> &'static str {
78 match self {
79 Resource::Account => "account",
API: notifications over REST and MCP, with notifications scopes80 Resource::Notifications => "notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step81 Resource::Workspace => "workspace",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit82 Resource::Billing => "billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step83 Resource::Repo => "repo",
84 Resource::Code => "code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar85 Resource::Security => "security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member86 Resource::Packages => "packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step87 Resource::Issues => "issues",
88 Resource::PullRequests => "pull_requests",
89 Resource::Agents => "agents",
90 Resource::Workflows => "workflows",
Token reach: workflow_files scope, fine-grained reach, workspace token cap91 Resource::WorkflowFiles => "workflow_files",
Merge checks: statuses and check runs on every commit92 Resource::Checks => "checks",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9793 Resource::Deployments => "deployments",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step94 Resource::Memory => "memory",
95 Resource::Access => "access",
96 Resource::Webhooks => "webhooks",
97 Resource::Secrets => "secrets",
Fast pages, required checks on the branch, self-hosted runners, honest incidents98 Resource::Runners => "runners",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens99 Resource::Models => "models",
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet100 Resource::Artifacts => "artifacts",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step101 }
102 }
103
104 /// Its name, for people.
105 pub fn label(self) -> &'static str {
106 match self {
107 Resource::Account => "Your account",
API: notifications over REST and MCP, with notifications scopes108 Resource::Notifications => "Notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step109 Resource::Workspace => "Workspaces",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit110 Resource::Billing => "Billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step111 Resource::Repo => "Repositories",
112 Resource::Code => "Code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar113 Resource::Security => "Security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member114 Resource::Packages => "Packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step115 Resource::Issues => "Issues",
116 Resource::PullRequests => "Pull requests",
117 Resource::Agents => "g1t agents",
118 Resource::Workflows => "Workflows",
Token reach: workflow_files scope, fine-grained reach, workspace token cap119 Resource::WorkflowFiles => "Workflow files",
Merge checks: statuses and check runs on every commit120 Resource::Checks => "Checks and statuses",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97121 Resource::Deployments => "Deployments",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step122 Resource::Memory => "Memory and context",
123 Resource::Access => "Who has access",
124 Resource::Webhooks => "Webhooks",
125 Resource::Secrets => "Secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents126 Resource::Runners => "Self-hosted runners",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens127 Resource::Models => "AI Gateway",
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet128 Resource::Artifacts => "Artifacts",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step129 }
130 }
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet131
132 /// Whether tokens are offered it yet. A resource that is not is in the
133 /// table (so its scopes parse, and the TypeScript mirror lists it under
134 /// `UPCOMING_RESOURCES`) but nothing hands it out: presets, full
135 /// access, OAuth and the token form leave it out, and no operation
136 /// needs it. Artifacts is offered once its API ships (Phase 3 of
137 /// docs/ARTIFACTS_MODE.md).
138 pub fn offered(self) -> bool {
139 !matches!(self, Resource::Artifacts)
140 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step141}
142
143/// How much of a resource.
144#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
145pub enum Level {
146 Read,
147 Write,
148 /// Starting g1t's agents, which spends the workspace's money.
149 Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member150 /// Deleting what cannot be brought back, such as a package's versions.
151 Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step152 Admin,
153}
154
155impl Level {
156 pub fn as_str(self) -> &'static str {
157 match self {
158 Level::Read => "read",
159 Level::Write => "write",
160 Level::Run => "run",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member161 Level::Delete => "delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step162 Level::Admin => "admin",
163 }
164 }
165}
166
167/// One scope. Its text form, `resource:level`, is what tokens store, OAuth
168/// clients ask for, and errors name.
169#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
170pub enum Scope {
171 RepoRead,
172 RepoWrite,
173 RepoAdmin,
174 CodeRead,
175 CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar176 SecurityRead,
177 SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member178 PackagesRead,
179 PackagesWrite,
180 PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step181 IssuesRead,
182 IssuesWrite,
183 PullRequestsRead,
184 PullRequestsWrite,
185 AgentsRun,
186 WorkflowsRead,
187 WorkflowsWrite,
Token reach: workflow_files scope, fine-grained reach, workspace token cap188 WorkflowFilesWrite,
Merge checks: statuses and check runs on every commit189 ChecksRead,
190 ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97191 DeploymentsRead,
192 DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step193 MemoryRead,
194 MemoryWrite,
195 AccountRead,
196 AccountWrite,
API: notifications over REST and MCP, with notifications scopes197 NotificationsRead,
198 NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step199 WorkspaceRead,
200 WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit201 BillingRead,
202 BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step203 AccessRead,
204 AccessAdmin,
205 WebhooksRead,
206 WebhooksAdmin,
207 SecretsRead,
208 SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents209 RunnersRead,
210 RunnersAdmin,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens211 ModelsRead,
212 ModelsWrite,
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet213 ArtifactsRead,
214 ArtifactsWrite,
215 ArtifactsAdmin,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step216}
217
218impl Scope {
219 /// Every scope, grouped by resource, least first.
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet220 pub const ALL: [Scope; 45] = [
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step221 Scope::RepoRead,
222 Scope::RepoWrite,
223 Scope::RepoAdmin,
224 Scope::CodeRead,
225 Scope::CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar226 Scope::SecurityRead,
227 Scope::SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member228 Scope::PackagesRead,
229 Scope::PackagesWrite,
230 Scope::PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step231 Scope::IssuesRead,
232 Scope::IssuesWrite,
233 Scope::PullRequestsRead,
234 Scope::PullRequestsWrite,
235 Scope::AgentsRun,
236 Scope::WorkflowsRead,
237 Scope::WorkflowsWrite,
Token reach: workflow_files scope, fine-grained reach, workspace token cap238 Scope::WorkflowFilesWrite,
Merge checks: statuses and check runs on every commit239 Scope::ChecksRead,
240 Scope::ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97241 Scope::DeploymentsRead,
242 Scope::DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step243 Scope::MemoryRead,
244 Scope::MemoryWrite,
245 Scope::AccountRead,
246 Scope::AccountWrite,
API: notifications over REST and MCP, with notifications scopes247 Scope::NotificationsRead,
248 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step249 Scope::WorkspaceRead,
250 Scope::WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit251 Scope::BillingRead,
252 Scope::BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step253 Scope::AccessRead,
254 Scope::AccessAdmin,
255 Scope::WebhooksRead,
256 Scope::WebhooksAdmin,
257 Scope::SecretsRead,
258 Scope::SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents259 Scope::RunnersRead,
260 Scope::RunnersAdmin,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens261 Scope::ModelsRead,
262 Scope::ModelsWrite,
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet263 Scope::ArtifactsRead,
264 Scope::ArtifactsWrite,
265 Scope::ArtifactsAdmin,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step266 ];
267
268 pub fn as_str(self) -> &'static str {
269 match self {
270 Scope::RepoRead => "repo:read",
271 Scope::RepoWrite => "repo:write",
272 Scope::RepoAdmin => "repo:admin",
273 Scope::CodeRead => "code:read",
274 Scope::CodeWrite => "code:write",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar275 Scope::SecurityRead => "security:read",
276 Scope::SecurityWrite => "security:write",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member277 Scope::PackagesRead => "packages:read",
278 Scope::PackagesWrite => "packages:write",
279 Scope::PackagesDelete => "packages:delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step280 Scope::IssuesRead => "issues:read",
281 Scope::IssuesWrite => "issues:write",
282 Scope::PullRequestsRead => "pull_requests:read",
283 Scope::PullRequestsWrite => "pull_requests:write",
284 Scope::AgentsRun => "agents:run",
285 Scope::WorkflowsRead => "workflows:read",
286 Scope::WorkflowsWrite => "workflows:write",
Token reach: workflow_files scope, fine-grained reach, workspace token cap287 Scope::WorkflowFilesWrite => "workflow_files:write",
Merge checks: statuses and check runs on every commit288 Scope::ChecksRead => "checks:read",
289 Scope::ChecksWrite => "checks:write",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97290 Scope::DeploymentsRead => "deployments:read",
291 Scope::DeploymentsWrite => "deployments:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step292 Scope::MemoryRead => "memory:read",
293 Scope::MemoryWrite => "memory:write",
294 Scope::AccountRead => "account:read",
295 Scope::AccountWrite => "account:write",
API: notifications over REST and MCP, with notifications scopes296 Scope::NotificationsRead => "notifications:read",
297 Scope::NotificationsWrite => "notifications:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step298 Scope::WorkspaceRead => "workspace:read",
299 Scope::WorkspaceAdmin => "workspace:admin",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit300 Scope::BillingRead => "billing:read",
301 Scope::BillingWrite => "billing:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step302 Scope::AccessRead => "access:read",
303 Scope::AccessAdmin => "access:admin",
304 Scope::WebhooksRead => "webhooks:read",
305 Scope::WebhooksAdmin => "webhooks:admin",
306 Scope::SecretsRead => "secrets:read",
307 Scope::SecretsAdmin => "secrets:admin",
Fast pages, required checks on the branch, self-hosted runners, honest incidents308 Scope::RunnersRead => "runners:read",
309 Scope::RunnersAdmin => "runners:admin",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens310 Scope::ModelsRead => "models:read",
311 Scope::ModelsWrite => "models:write",
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet312 Scope::ArtifactsRead => "artifacts:read",
313 Scope::ArtifactsWrite => "artifacts:write",
314 Scope::ArtifactsAdmin => "artifacts:admin",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step315 }
316 }
317
318 pub fn parse(text: &str) -> Option<Scope> {
319 let text = text.trim().to_ascii_lowercase();
320 Scope::ALL.into_iter().find(|scope| scope.as_str() == text)
321 }
322
323 pub fn resource(self) -> Resource {
324 let name = self.as_str().split_once(':').map_or("", |(resource, _)| resource);
325 Resource::ALL
326 .into_iter()
327 .find(|resource| resource.as_str() == name)
328 .unwrap_or(Resource::Account)
329 }
330
331 pub fn level(self) -> Level {
332 match self.as_str().rsplit_once(':').map_or("", |(_, level)| level) {
333 "write" => Level::Write,
334 "run" => Level::Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member335 "delete" => Level::Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step336 "admin" => Level::Admin,
337 _ => Level::Read,
338 }
339 }
340
341 /// Whether holding `self` gives `other`: the same resource, at the same
342 /// level or a lower one.
343 pub fn includes(self, other: Scope) -> bool {
344 self.resource() == other.resource() && self.level() >= other.level()
345 }
346
347 /// Changes that are hard or impossible to undo, or that decide who can
348 /// reach what. Shown behind a warning wherever scopes are chosen.
349 pub fn dangerous(self) -> bool {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member350 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 step351 }
352
353 /// What it lets a token do, in plain words.
354 pub fn describe(self) -> &'static str {
355 match self {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97356 Scope::RepoRead => "See repositories, their settings, labels, timelines, releases, languages, contributors and security alerts, and search",
357 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 merge358 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 step359 Scope::CodeRead => "Clone and fetch private repositories with git",
360 Scope::CodeWrite => "Push commits with git",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar361 Scope::SecurityRead => "See secret scanning, code scanning and vulnerability alerts, custom patterns, the dependency graph and SBOM, and security settings",
362 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 member363 Scope::PackagesRead => "Pull container images and install private packages",
364 Scope::PackagesWrite => "Push container images and publish packages",
Merge packages: roles, Actions access, source label, soft delete, API365 Scope::PackagesDelete => "Delete and restore packages and their versions",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step366 Scope::IssuesRead => "Read issues, comments and plans",
367 Scope::IssuesWrite => "Open, edit, close and comment on issues",
368 Scope::PullRequestsRead => "Read pull requests, their changes, sessions and merge queues",
369 Scope::PullRequestsWrite => "Open, review, close and merge pull requests",
370 Scope::AgentsRun => "Put g1t agents to work and message them, which uses the workspace's money",
371 Scope::WorkflowsRead => "Read workflows, runs and logs",
372 Scope::WorkflowsWrite => "Run, cancel, rerun and turn workflows on or off",
Token reach: workflow_files scope, fine-grained reach, workspace token cap373 Scope::WorkflowFilesWrite => "Add, change and delete workflow files under .g1t/workflows and .github/workflows, with git or the API",
Merge checks: statuses and check runs on every commit374 Scope::ChecksRead => "Read commits' statuses, check runs, check suites and annotations",
375 Scope::ChecksWrite => "Report statuses and check runs on commits, and ask for checks to run again",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97376 Scope::DeploymentsRead => "See deployments, their statuses and environments",
377 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 step378 Scope::MemoryRead => "Recall memory and search the workspace's context",
379 Scope::MemoryWrite => "Save memory for the next agent",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97380 Scope::AccountRead => "Read your email addresses, invites, invitations, pinned projects and stars",
381 Scope::AccountWrite => "Change your email addresses, make invites, answer invitations, pin projects and star repositories",
API: notifications over REST and MCP, with notifications scopes382 Scope::NotificationsRead => "See your inbox, its threads, and what you subscribe to and watch",
383 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 merge384 Scope::WorkspaceRead => "Read workspace settings, invites, integrations, model routes, teams and rulesets",
385 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 audit386 Scope::BillingRead => "See a workspace's usage, budget, AI credit and invoices",
387 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 step388 Scope::AccessRead => "See who has access to repositories",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar389 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 step390 Scope::WebhooksRead => "See webhooks and their deliveries",
391 Scope::WebhooksAdmin => "Create, change and delete webhooks",
392 Scope::SecretsRead => "List secrets (never their values) and read variables",
393 Scope::SecretsAdmin => "Set and delete secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents394 Scope::RunnersRead => "See self-hosted runners, their groups and where agents run",
395 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 tokens396 Scope::ModelsRead => "See the workspace's AI Gateway requests: their models, tokens, cost and status",
397 Scope::ModelsWrite => "Send model requests through the AI Gateway, which uses the workspace's AI credit",
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet398 Scope::ArtifactsRead => "List, read and search artifacts you can see, their versions, and the numbers their dashboards show",
399 Scope::ArtifactsWrite => "Create, rename, move, edit, trash and restore artifacts, and propose changes to them",
400 Scope::ArtifactsAdmin => "Share artifacts, change who can open them, and delete them for good",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step401 }
402 }
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet403
404 /// Whether tokens are offered it yet: its resource's [`Resource::offered`].
405 pub fn offered(self) -> bool {
406 self.resource().offered()
407 }
408}
409
410/// Every scope tokens are offered, in table order: what OAuth advertises.
411pub fn offered_scopes() -> Vec<Scope> {
412 Scope::ALL.into_iter().filter(|scope| scope.offered()).collect()
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step413}
414
415impl Serialize for Scope {
416 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
417 serializer.serialize_str(self.as_str())
418 }
419}
420
421impl<'de> Deserialize<'de> for Scope {
422 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
423 let text = String::deserialize(deserializer)?;
424 Scope::parse(&text).ok_or_else(|| serde::de::Error::custom(format!("unknown scope {text}")))
425 }
426}
427
428/// Scopes as written in a token's row or an OAuth request: separated by
429/// spaces or commas. Unknown names are left out, so a client asking for a
430/// scope from a newer version gets the rest.
431pub fn parse_scopes(text: &str) -> Vec<Scope> {
432 let mut scopes: Vec<Scope> = text
433 .split(|c: char| c.is_whitespace() || c == ',')
434 .filter_map(Scope::parse)
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet435 .filter(|scope| scope.offered())
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step436 .collect();
437 normalize(&mut scopes);
438 scopes
439}
440
441/// In table order, without repeats.
442pub fn normalize(scopes: &mut Vec<Scope>) {
443 let given = std::mem::take(scopes);
444 scopes.extend(Scope::ALL.into_iter().filter(|scope| given.contains(scope)));
445}
446
447/// Space-separated, as stored and as OAuth writes them.
448pub fn scopes_text(scopes: &[Scope]) -> String {
449 scopes.iter().map(|scope| scope.as_str()).collect::<Vec<_>>().join(" ")
450}
451
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers452/// Where a resource sits on the token form, and which tokens may hold it.
453#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
454#[serde(rename_all = "snake_case")]
455pub enum ResourceGroup {
456 /// About repositories: what they hold and how they are run.
457 Repository,
458 /// About a workspace itself.
459 Workspace,
460 /// About the person: only a personal token may hold these.
461 Account,
462}
463
464impl ResourceGroup {
465 pub const ALL: [ResourceGroup; 3] = [ResourceGroup::Repository, ResourceGroup::Workspace, ResourceGroup::Account];
466
467 pub fn as_str(self) -> &'static str {
468 match self {
469 ResourceGroup::Repository => "repository",
470 ResourceGroup::Workspace => "workspace",
471 ResourceGroup::Account => "account",
472 }
473 }
474}
475
476impl Resource {
477 pub fn group(self) -> ResourceGroup {
478 match self {
479 Resource::Account | Resource::Notifications => ResourceGroup::Account,
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet480 Resource::Workspace | Resource::Billing | Resource::Runners | Resource::Models | Resource::Artifacts => ResourceGroup::Workspace,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers481 _ => ResourceGroup::Repository,
482 }
483 }
484
485 pub fn parse(text: &str) -> Option<Resource> {
486 let text = text.trim().to_ascii_lowercase();
487 Resource::ALL.into_iter().find(|resource| resource.as_str() == text)
488 }
489
490 /// Its scopes, least first.
491 pub fn scopes(self) -> Vec<Scope> {
492 Scope::ALL.into_iter().filter(|scope| scope.resource() == self).collect()
493 }
494}
495
496// --- Permissions --------------------------------------------------------------
497//
498// A token's permissions are its scopes read per resource: each resource
499// at none or one level (`{"issues": "write", "repo": "read"}`). A level
500// includes the ones below it, so the highest scope held of each resource
501// says everything; that is what a token stores. Personal tokens and a
502// workspace's own tokens are made, shown and checked this way alike.
503
504/// The highest scope of each resource held, in table order: the fewest
505/// scopes that give the same access, as tokens store them.
506pub fn top_scopes(scopes: &[Scope]) -> Vec<Scope> {
507 let mut top: Vec<Scope> = Vec::new();
508 for resource in Resource::ALL {
509 if let Some(best) = scopes.iter().filter(|scope| scope.resource() == resource).max_by_key(|scope| scope.level()) {
510 top.push(*best);
511 }
512 }
513 normalize(&mut top);
514 top
515}
516
517/// Every resource at its highest level: all a token can be given.
518pub fn everything() -> Vec<Scope> {
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet519 top_scopes(&offered_scopes())
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers520}
521
522/// Scopes as permissions: each resource held, by name, at its highest
523/// level held.
524pub fn permissions_of(scopes: &[Scope]) -> std::collections::BTreeMap<String, String> {
525 top_scopes(scopes)
526 .into_iter()
527 .map(|scope| (scope.resource().as_str().to_owned(), scope.level().as_str().to_owned()))
528 .collect()
529}
530
531/// Permissions as asked for (`{"issues": "write"}`, `none` or empty left
532/// out) into the scopes a token stores, or why they cannot be. `personal`
533/// is whether the token is a person's: only theirs may hold account ones.
534pub fn resolve_permissions(asked: &std::collections::BTreeMap<String, String>, personal: bool) -> Result<Vec<Scope>, String> {
535 let mut scopes = Vec::new();
536 for (name, level) in asked {
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet537 let Some(resource) = Resource::parse(name).filter(|resource| resource.offered()) else {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers538 return Err(format!("There is no permission called {name}."));
539 };
540 let level = level.trim().to_ascii_lowercase();
541 if level.is_empty() || level == "none" {
542 continue;
543 }
544 let Some(scope) = Scope::parse(&format!("{}:{level}", resource.as_str())) else {
545 let levels: Vec<&str> = resource.scopes().iter().map(|scope| scope.level().as_str()).collect();
546 return Err(format!("{} is none or {}, not {level}.", resource.as_str(), levels.join(", ")));
547 };
548 if resource.group() == ResourceGroup::Account && !personal {
549 return Err(format!("{} is about a person's account: a workspace's token cannot hold it.", resource.as_str()));
550 }
551 scopes.push(scope);
552 }
553 Ok(top_scopes(&scopes))
554}
555
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step556/// What a token stores for full access, which is not a scope a client can
557/// ask for by name.
558pub const FULL_ACCESS: &str = "*";
559
560/// Starting points for choosing scopes.
561#[derive(Clone, Copy, Debug, PartialEq, Eq)]
562pub enum Preset {
563 ReadOnly,
564 Agent,
565 Ci,
566 Full,
567}
568
569impl Preset {
570 pub const ALL: [Preset; 4] = [Preset::ReadOnly, Preset::Agent, Preset::Ci, Preset::Full];
571
572 pub fn as_str(self) -> &'static str {
573 match self {
574 Preset::ReadOnly => "read_only",
575 Preset::Agent => "agent",
576 Preset::Ci => "ci",
577 Preset::Full => "full",
578 }
579 }
580
581 pub fn label(self) -> &'static str {
582 match self {
583 Preset::ReadOnly => "Read only",
584 Preset::Agent => "Agent",
585 Preset::Ci => "CI",
586 Preset::Full => "Full access",
587 }
588 }
589
590 /// Its scopes; `None` for full access.
591 pub fn scopes(self) -> Option<Vec<Scope>> {
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet592 let reads = || Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read && scope.offered());
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step593 match self {
594 Preset::ReadOnly => Some(reads().collect()),
595 Preset::Agent => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents596 // Not the machines work runs on: an agent has no business
597 // knowing a workspace's own runners.
598 let mut scopes: Vec<Scope> = reads().filter(|scope| scope.resource() != Resource::Runners).collect();
API: notifications over REST and MCP, with notifications scopes599 // And answering what needs the person it works for: marking
600 // it done, subscribing, watching.
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step601 scopes.extend([
602 Scope::CodeWrite,
603 Scope::IssuesWrite,
604 Scope::PullRequestsWrite,
605 Scope::AgentsRun,
606 Scope::MemoryWrite,
API: notifications over REST and MCP, with notifications scopes607 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step608 ]);
609 normalize(&mut scopes);
610 Some(scopes)
611 }
612 Preset::Ci => Some(vec![
613 Scope::RepoRead,
614 Scope::CodeRead,
615 Scope::CodeWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member616 Scope::PackagesRead,
617 Scope::PackagesWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step618 Scope::WorkflowsRead,
619 Scope::WorkflowsWrite,
Merge checks: statuses and check runs on every commit620 Scope::ChecksRead,
621 Scope::ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97622 Scope::DeploymentsRead,
623 Scope::DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step624 ]),
625 Preset::Full => None,
626 }
627 }
628}
629
630/// What an OAuth client gets when it asks for nothing in particular: the
631/// agent preset. Never an admin scope.
632pub fn oauth_default() -> Vec<Scope> {
633 Preset::Agent.scopes().unwrap_or_default()
634}
635
636/// Set on a [`crate::User`] resolved from an access token: what the token
637/// may do. Absent on a signed-in session, which may do whatever its person
638/// can.
639#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
640pub struct TokenAccess {
641 /// The token's id, as audit entries and errors name it.
642 #[serde(default)]
643 pub token_id: String,
644 /// Its scopes, as `resource:level`. Absent: full access, everything the
645 /// person (or workspace) can do.
646 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub scopes: Option<Vec<String>>,
648 /// Made before tokens had scopes: full access until someone narrows it.
649 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
650 pub legacy: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'651 /// Set on a workflow job's token (`G1T_TOKEN`): the one repository it
652 /// reaches, as `owner/name`. Every other is refused, whatever its owner
653 /// could reach.
654 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub repo: Option<String>,
656 /// Set on a workflow job's token: the run and job it was made for. The
657 /// audit log records its changes as that job's, and what it changes
658 /// starts no workflows (only `workflow_dispatch` and
659 /// `repository_dispatch` do), so a workflow cannot set itself off.
660 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub job: Option<JobToken>,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens662 /// The token's name, as its owner gave it, so a log can say which
663 /// token made a request. Absent where whoever resolved it did not say.
664 #[serde(default, skip_serializing_if = "Option::is_none")]
665 pub name: Option<String>,
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers666 /// Set on a token narrowed to less than its owner can reach: one
667 /// workspace (all, selected or none of its private repositories), or
668 /// none at all (its owner's account and public repositories). Absent
669 /// on a token that reaches every workspace its owner can.
670 ///
671 /// The wire key is `fine_grained`, kept from before tokens were one
672 /// kind, so services deployed at different moments agree on it.
673 #[serde(rename = "fine_grained", default, skip_serializing_if = "Option::is_none")]
674 pub reach: Option<TokenReach>,
Token reach: workflow_files scope, fine-grained reach, workspace token cap675 /// Set on a workspace's own token that an owner gave Admin when making
676 /// it. Without it a workspace's token has Write on the workspace's
677 /// repositories, as a member would (see [`crate::access`]).
678 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
679 pub admin: bool,
680 /// Set on what a repository's deploy key resolves to: the key's id. Its
681 /// `repo` is the one repository it reaches.
682 #[serde(default, skip_serializing_if = "Option::is_none")]
683 pub deploy_key: Option<String>,
Merge branch 'worktree-agent-ad4439ce85a91ecb4' into integrate684 /// Set on a person's token whose owner let it use the website (g1t.sh)
685 /// as them, sent as `Authorization: Bearer`. Not a scope: no preset,
686 /// full access or OAuth grant includes it, and git, the API and MCP
687 /// ignore it.
688 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
689 pub website: bool,
Token reach: workflow_files scope, fine-grained reach, workspace token cap690}
691
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers692/// Which repositories a token reaches in the workspace it is made for.
Token reach: workflow_files scope, fine-grained reach, workspace token cap693#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
694#[serde(rename_all = "snake_case")]
695pub enum RepositorySelection {
696 /// Every repository of the workspace, ones made later included.
697 #[default]
698 All,
699 /// The repositories chosen, by id.
700 Selected,
701 /// None of the workspace's private repositories: public repositories,
702 /// read-only, and the workspace's own settings its permissions allow.
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers703 /// With no workspace: the owner's account and public repositories only.
Token reach: workflow_files scope, fine-grained reach, workspace token cap704 Public,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step705}
706
Token reach: workflow_files scope, fine-grained reach, workspace token cap707impl RepositorySelection {
708 pub fn as_str(self) -> &'static str {
709 match self {
710 RepositorySelection::All => "all",
711 RepositorySelection::Selected => "selected",
712 RepositorySelection::Public => "public",
713 }
714 }
715
716 pub fn parse(text: &str) -> Option<RepositorySelection> {
717 match text.trim().to_ascii_lowercase().as_str() {
718 "all" => Some(RepositorySelection::All),
719 "selected" => Some(RepositorySelection::Selected),
720 "public" | "public_only" | "none" => Some(RepositorySelection::Public),
721 _ => None,
722 }
723 }
724}
725
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers726/// What a narrowed token reaches, as identity resolves it on each use.
Token reach: workflow_files scope, fine-grained reach, workspace token cap727#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers728pub struct TokenReach {
729 /// The workspace whose repositories and settings it reaches, by slug as
730 /// it is now. Absent: its owner's account only, with public
731 /// repositories read-only.
Token reach: workflow_files scope, fine-grained reach, workspace token cap732 #[serde(default, skip_serializing_if = "Option::is_none")]
733 pub workspace: Option<String>,
734 #[serde(default)]
735 pub repositories: RepositorySelection,
736 /// With [`RepositorySelection::Selected`]: the repositories' ids.
737 #[serde(default, skip_serializing_if = "Vec::is_empty")]
738 pub repo_ids: Vec<String>,
739}
740
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers741impl TokenReach {
Token reach: workflow_files scope, fine-grained reach, workspace token cap742 /// Whether it reaches the repository with this id in the workspace
743 /// `namespace` for more than what anyone may do with a public one.
744 pub fn covers(&self, repo_id: &str, namespace: &str) -> bool {
745 let Some(workspace) = self.workspace.as_deref() else {
746 return false;
747 };
748 if !workspace.eq_ignore_ascii_case(namespace) {
749 return false;
750 }
751 match self.repositories {
752 RepositorySelection::All => true,
753 RepositorySelection::Selected => self.repo_ids.iter().any(|id| id == repo_id),
754 RepositorySelection::Public => false,
755 }
756 }
757
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers758 /// Whether it is made for the workspace `slug`.
Token reach: workflow_files scope, fine-grained reach, workspace token cap759 pub fn owned_by(&self, slug: &str) -> bool {
760 self.workspace.as_deref().is_some_and(|workspace| workspace.eq_ignore_ascii_case(slug))
761 }
762}
763
764/// Where workflow files live. Adding, changing or deleting a file under
765/// one, with git or through g1t, needs [`Scope::WorkflowFilesWrite`] from a
766/// token: what GitHub's `workflow` scope and `workflows` permission do.
767pub const WORKFLOW_DIRS: [&str; 2] = [".g1t/workflows/", ".github/workflows/"];
768
769/// Whether `path` is a workflow file, or a file in one's directory.
770pub fn is_workflow_file(path: &str) -> bool {
771 let path = path.trim_start_matches('/');
772 WORKFLOW_DIRS.iter().any(|dir| {
773 path.len() >= dir.len() && path.is_char_boundary(dir.len()) && path[..dir.len()].eq_ignore_ascii_case(dir)
774 }) || WORKFLOW_DIRS.iter().any(|dir| path.eq_ignore_ascii_case(dir.trim_end_matches('/')))
775}
776
777/// Whether a token may add, change or delete the files at `paths`: a
778/// refusal naming the first workflow file it may not touch, else `None`.
779/// A signed-in person (no token) is never refused here; their role decides.
780pub fn decide_workflow_files<'a>(access: Option<&TokenAccess>, paths: impl IntoIterator<Item = &'a str>) -> Option<Decision> {
781 let access = access?;
782 if access.allows(Scope::WorkflowFilesWrite) && access.job.is_none() {
783 return None;
784 }
785 let path = paths.into_iter().find(|path| is_workflow_file(path))?;
786 let why = if access.job.is_some() {
787 "a workflow job's token can never add or change workflow files".to_owned()
788 } else {
789 format!("it needs the {} scope", Scope::WorkflowFilesWrite.as_str())
790 };
791 Some(Decision::deny(
792 "token:workflows",
793 format!("This access token cannot change the workflow file {path}: {why}."),
794 ))
795}
796
Merge branch 'worktree-agent-a3abfcce648e87dca'797/// The workflow job a token was made for.
798#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
799pub struct JobToken {
800 /// The run, `run_…`.
801 pub run_id: String,
802 /// The job, `job_…`.
803 pub job_id: String,
804 /// Whether it may open pull requests and approve them, by its
805 /// repository's and workspace's choice ("Allow g1t Actions to create and
806 /// approve pull requests"). Off unless chosen.
807 #[serde(default)]
808 pub pull_requests: bool,
809}
810
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step811impl TokenAccess {
812 /// Full access to everything: the access tokens made before scopes had.
813 pub fn full() -> Self {
814 TokenAccess::default()
815 }
816
Merge branch 'worktree-agent-a3abfcce648e87dca'817 /// Whether it may reach the repository `owner/name`: every token but a
818 /// workflow job's, which reaches its own repository only.
819 pub fn reaches(&self, repo: &str) -> bool {
820 self.repo.as_deref().is_none_or(|only| only.eq_ignore_ascii_case(repo))
821 }
822
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step823 pub fn is_full(&self) -> bool {
824 self.scopes.is_none()
825 }
826
827 /// The scopes it holds, or `None` for full access.
828 pub fn granted(&self) -> Option<Vec<Scope>> {
829 self.scopes
830 .as_ref()
831 .map(|scopes| scopes.iter().filter_map(|scope| Scope::parse(scope)).collect())
832 }
833
834 pub fn allows(&self, needed: Scope) -> bool {
835 match self.granted() {
836 None => true,
837 Some(granted) => granted.iter().any(|held| held.includes(needed)),
838 }
839 }
Token reach: workflow_files scope, fine-grained reach, workspace token cap840
841 /// Whether it reaches the repository with this id in `namespace` for
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers842 /// more than reading a public one: every token but a narrowed one
843 /// outside its workspace or repository selection. Its owner's role
Token reach: workflow_files scope, fine-grained reach, workspace token cap844 /// still decides; see [`crate::access`].
845 pub fn covers_repo(&self, repo_id: &str, namespace: &str) -> bool {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers846 self.reach.as_ref().is_none_or(|reach| reach.covers(repo_id, namespace))
Token reach: workflow_files scope, fine-grained reach, workspace token cap847 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step848}
849
850/// Every operation of the API and MCP server, with the scope it needs. An
851/// operation in [`NO_SCOPE`] needs none. The API checks that every one of
852/// its operations is in exactly one of the two.
853pub const OPERATIONS: &[(&str, Scope)] = &[
854 // Your account.
855 ("list_emails", Scope::AccountRead),
856 ("add_email", Scope::AccountWrite),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)857 ("confirm_email", Scope::AccountWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step858 ("remove_email", Scope::AccountWrite),
859 ("update_email_settings", Scope::AccountWrite),
860 ("list_invites", Scope::AccountRead),
861 ("create_invite", Scope::AccountWrite),
862 ("revoke_invite", Scope::AccountWrite),
Merge workspace invitations: nobody joins a workspace without saying yes, people are found by username, your own invites can bring someone in, and nobody is left without a workspace (identity 0040)863 ("list_invitations", Scope::AccountRead),
864 ("accept_invitation", Scope::AccountWrite),
865 ("decline_invitation", Scope::AccountWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step866 ("list_my_repo_invitations", Scope::AccountRead),
867 ("accept_repo_invitation", Scope::AccountWrite),
868 ("decline_repo_invitation", Scope::AccountWrite),
API: pinned projects over REST and MCP869 // Your pinned projects: a preference of your account.
870 ("list_pinned_projects", Scope::AccountRead),
871 ("pin_project", Scope::AccountWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97872 // Your stars: a preference of your account.
873 ("list_starred", Scope::AccountRead),
874 ("check_starred", Scope::AccountRead),
875 ("star_repo", Scope::AccountWrite),
876 ("unstar_repo", Scope::AccountWrite),
API: pinned projects over REST and MCP877 ("unpin_project", Scope::AccountWrite),
878 ("reorder_pinned_projects", Scope::AccountWrite),
API: notifications over REST and MCP, with notifications scopes879 // Your inbox: notifications, subscriptions and watching.
880 ("list_notifications", Scope::NotificationsRead),
881 ("get_notification_thread", Scope::NotificationsRead),
882 ("get_thread_subscription", Scope::NotificationsRead),
883 ("get_repo_subscription", Scope::NotificationsRead),
884 ("list_watched_repos", Scope::NotificationsRead),
885 ("mark_notifications_read", Scope::NotificationsWrite),
886 ("mark_thread_read", Scope::NotificationsWrite),
887 ("mark_thread_done", Scope::NotificationsWrite),
888 ("save_thread", Scope::NotificationsWrite),
889 ("snooze_thread", Scope::NotificationsWrite),
890 ("set_thread_subscription", Scope::NotificationsWrite),
891 ("delete_thread_subscription", Scope::NotificationsWrite),
892 ("set_repo_subscription", Scope::NotificationsWrite),
893 ("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 step894 // Workspaces, their invites and integrations.
895 ("create_workspace", Scope::WorkspaceAdmin),
896 ("delete_workspace", Scope::WorkspaceAdmin),
Merge branch 'worktree-agent-ad7c6d88d93adc817'897 ("get_workspace", Scope::WorkspaceRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily898 ("update_workspace", Scope::WorkspaceAdmin),
Merge main (membership, two-factor, GitHub repo roles) into tokens899 // Its members, and who owns it.
900 ("list_members", Scope::WorkspaceRead),
901 ("update_member", Scope::WorkspaceAdmin),
902 ("remove_member", Scope::WorkspaceAdmin),
903 ("transfer_ownership", Scope::WorkspaceAdmin),
904 ("leave_workspace", Scope::AccountWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step905 ("list_workspace_invites", Scope::WorkspaceRead),
906 ("invite_member", Scope::WorkspaceAdmin),
907 ("revoke_workspace_invite", Scope::WorkspaceAdmin),
908 ("list_integrations", Scope::WorkspaceRead),
909 ("connect_integration", Scope::WorkspaceAdmin),
AI Gateway: OpenAI's format, open models, and your own providers910 ("update_integration", Scope::WorkspaceAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step911 ("disconnect_integration", Scope::WorkspaceAdmin),
912 ("test_integration", Scope::WorkspaceAdmin),
913 ("get_model_routes", Scope::WorkspaceRead),
914 ("set_model_routes", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar915 // Teams: reading them, and managing them. A team's role on a
916 // repository is who has access.
917 ("list_teams", Scope::WorkspaceRead),
918 ("get_team", Scope::WorkspaceRead),
919 ("list_team_members", Scope::WorkspaceRead),
920 ("list_child_teams", Scope::WorkspaceRead),
921 ("list_team_repos", Scope::WorkspaceRead),
922 ("list_user_teams", Scope::WorkspaceRead),
923 ("create_team", Scope::WorkspaceAdmin),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge924 ("list_workspace_rulesets", Scope::WorkspaceRead),
925 ("get_workspace_ruleset", Scope::WorkspaceRead),
926 ("list_workspace_rule_evaluations", Scope::WorkspaceRead),
927 ("create_workspace_ruleset", Scope::WorkspaceAdmin),
928 ("update_workspace_ruleset", Scope::WorkspaceAdmin),
929 ("delete_workspace_ruleset", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar930 ("update_team", Scope::WorkspaceAdmin),
931 ("delete_team", Scope::WorkspaceAdmin),
932 ("set_team_member", Scope::WorkspaceAdmin),
933 ("remove_team_member", Scope::WorkspaceAdmin),
934 ("set_team_review_assignment", Scope::WorkspaceAdmin),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit935 // A workspace's billing: usage, budget, AI credit and invoices.
936 ("get_usage", Scope::BillingRead),
937 ("get_budget", Scope::BillingRead),
938 ("get_ai_credit", Scope::BillingRead),
939 ("list_invoices", Scope::BillingRead),
940 ("get_billing_details", Scope::BillingRead),
941 ("set_budget", Scope::BillingWrite),
942 ("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 step943 // Repositories.
944 ("list_repos", Scope::RepoRead),
945 ("get_repo", Scope::RepoRead),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97946 // Projects follow their repositories.
947 ("list_projects", Scope::RepoRead),
948 ("get_project", Scope::RepoRead),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step949 ("search", Scope::RepoRead),
950 ("list_events", Scope::RepoRead),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97951 // What the default branch says about a repository, who starred it, and
952 // its releases.
953 ("get_languages", Scope::RepoRead),
954 ("list_contributors", Scope::RepoRead),
955 ("get_license", Scope::RepoRead),
956 ("list_stargazers", Scope::RepoRead),
957 ("list_releases", Scope::RepoRead),
958 ("get_latest_release", Scope::RepoRead),
959 ("get_release_by_tag", Scope::RepoRead),
960 ("get_release", Scope::RepoRead),
961 ("create_release", Scope::RepoWrite),
962 ("update_release", Scope::RepoWrite),
963 ("delete_release", Scope::RepoWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step964 ("list_labels", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar965 ("list_milestones", Scope::RepoRead),
966 ("get_milestone", Scope::RepoRead),
967 ("create_label", Scope::IssuesWrite),
968 ("update_label", Scope::IssuesWrite),
969 ("delete_label", Scope::IssuesWrite),
970 ("add_default_labels", Scope::IssuesWrite),
971 ("create_milestone", Scope::IssuesWrite),
972 ("update_milestone", Scope::IssuesWrite),
973 ("delete_milestone", Scope::IssuesWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step974 ("get_repo_settings", Scope::RepoRead),
Fast pages, required checks on the branch, self-hosted runners, honest incidents975 ("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 step976 ("list_deleted_repos", Scope::RepoRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily977 ("list_security_alerts", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar978 ("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 step979 ("create_repo", Scope::RepoWrite),
980 ("update_repo", Scope::RepoWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97981 ("update_project", Scope::RepoWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step982 ("update_repo_settings", Scope::RepoWrite),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge983 // Rulesets: reading them is reading the repository; changing them
984 // changes what everyone, agents included, may do, so it is admin.
985 ("list_repo_rulesets", Scope::RepoRead),
986 ("get_repo_ruleset", Scope::RepoRead),
987 ("get_branch_rules", Scope::RepoRead),
988 ("list_rule_evaluations", Scope::RepoRead),
989 ("create_repo_ruleset", Scope::RepoAdmin),
990 ("update_repo_ruleset", Scope::RepoAdmin),
991 ("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 step992 ("rename_branch", Scope::RepoWrite),
993 ("rename_repo", Scope::RepoAdmin),
994 ("transfer_repo", Scope::RepoAdmin),
995 ("archive_repo", Scope::RepoAdmin),
996 ("unarchive_repo", Scope::RepoAdmin),
997 ("set_repo_visibility", Scope::RepoAdmin),
998 ("delete_repo", Scope::RepoAdmin),
999 ("restore_repo", Scope::RepoAdmin),
1000 ("purge_repo", Scope::RepoAdmin),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1001 // A dismissed secret is let through push protection.
1002 ("dismiss_security_alert", Scope::RepoAdmin),
1003 ("reopen_security_alert", Scope::RepoAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1004 // The security suite: alerts, push protection, patterns, code
1005 // scanning, the supply chain and settings.
1006 ("list_secret_scanning_alerts", Scope::SecurityRead),
1007 ("get_secret_scanning_alert", Scope::SecurityRead),
1008 ("list_secret_scanning_locations", Scope::SecurityRead),
1009 ("list_bypass_requests", Scope::SecurityRead),
1010 ("list_custom_patterns", Scope::SecurityRead),
1011 ("list_code_scanning_alerts", Scope::SecurityRead),
1012 ("get_code_scanning_alert", Scope::SecurityRead),
1013 ("list_code_scanning_analyses", Scope::SecurityRead),
1014 ("get_sarif_upload", Scope::SecurityRead),
1015 ("list_vulnerability_alerts", Scope::SecurityRead),
1016 ("get_vulnerability_alert", Scope::SecurityRead),
1017 ("get_dependency_graph", Scope::SecurityRead),
1018 ("get_sbom", Scope::SecurityRead),
1019 ("compare_dependencies", Scope::SecurityRead),
1020 ("get_security_settings", Scope::SecurityRead),
1021 ("get_workspace_security_settings", Scope::SecurityRead),
1022 ("get_security_overview", Scope::SecurityRead),
1023 ("update_secret_scanning_alert", Scope::SecurityWrite),
1024 ("bypass_push_protection", Scope::SecurityWrite),
1025 ("check_secret_validity", Scope::SecurityWrite),
1026 ("review_bypass_request", Scope::SecurityWrite),
1027 ("create_custom_pattern", Scope::SecurityWrite),
1028 ("update_custom_pattern", Scope::SecurityWrite),
1029 ("delete_custom_pattern", Scope::SecurityWrite),
1030 ("dry_run_custom_pattern", Scope::SecurityWrite),
1031 ("update_code_scanning_alert", Scope::SecurityWrite),
1032 ("upload_sarif", Scope::SecurityWrite),
1033 ("update_vulnerability_alert", Scope::SecurityWrite),
1034 ("fix_security_alert", Scope::SecurityWrite),
1035 ("update_security_settings", Scope::SecurityWrite),
1036 ("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 step1037 // Issues and plans.
1038 ("list_issues", Scope::IssuesRead),
1039 ("get_issue", Scope::IssuesRead),
1040 ("get_plan", Scope::IssuesRead),
1041 ("create_issue", Scope::IssuesWrite),
1042 ("update_issue", Scope::IssuesWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1043 ("list_issue_labels", Scope::IssuesRead),
1044 ("add_issue_labels", Scope::IssuesWrite),
1045 ("set_issue_labels", Scope::IssuesWrite),
1046 ("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 step1047 ("close_issue", Scope::IssuesWrite),
1048 ("reopen_issue", Scope::IssuesWrite),
1049 ("add_comment", Scope::IssuesWrite),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1050 ("edit_comment", Scope::IssuesWrite),
1051 ("delete_comment", Scope::IssuesWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1052 ("import_issue", Scope::IssuesWrite),
1053 ("apply_plan", Scope::IssuesWrite),
1054 // Pull requests.
1055 ("list_pull_requests", Scope::PullRequestsRead),
1056 ("get_pull_request", Scope::PullRequestsRead),
1057 ("get_pull_request_changes", Scope::PullRequestsRead),
1058 ("read_session", Scope::PullRequestsRead),
1059 ("get_merge_queue", Scope::PullRequestsRead),
1060 ("create_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1061 ("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 step1062 ("record_session", Scope::PullRequestsWrite),
1063 ("mark_pull_request_ready", Scope::PullRequestsWrite),
1064 ("close_pull_request", Scope::PullRequestsWrite),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1065 ("reopen_pull_request", Scope::PullRequestsWrite),
1066 ("convert_pull_request_to_draft", Scope::PullRequestsWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1067 ("review_pull_request", Scope::PullRequestsWrite),
1068 ("merge_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1069 ("request_reviewers", Scope::PullRequestsWrite),
1070 ("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 step1071 // g1t's agents.
1072 ("assign_issue", Scope::AgentsRun),
1073 ("delegate", Scope::AgentsRun),
1074 ("plan_work", Scope::AgentsRun),
1075 ("message_agent", Scope::AgentsRun),
1076 ("answer_message", Scope::AgentsRun),
1077 ("take_messages", Scope::AgentsRun),
1078 // Workflows.
1079 ("list_workflows", Scope::WorkflowsRead),
1080 ("list_workflow_runs", Scope::WorkflowsRead),
1081 ("get_workflow_run", Scope::WorkflowsRead),
1082 ("get_job_logs", Scope::WorkflowsRead),
1083 ("dispatch_workflow", Scope::WorkflowsWrite),
1084 ("cancel_workflow_run", Scope::WorkflowsWrite),
1085 ("rerun_workflow_run", Scope::WorkflowsWrite),
1086 ("update_workflow", Scope::WorkflowsWrite),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R21087 ("list_artifacts", Scope::WorkflowsRead),
1088 ("list_workflow_run_artifacts", Scope::WorkflowsRead),
1089 ("get_artifact", Scope::WorkflowsRead),
1090 ("download_artifact", Scope::WorkflowsRead),
1091 ("get_artifact_retention", Scope::WorkflowsRead),
1092 ("delete_artifact", Scope::WorkflowsWrite),
1093 ("set_artifact_retention", Scope::WorkflowsWrite),
Merge checks: statuses and check runs on every commit1094 // Checks: statuses, check runs and check suites on commits.
1095 ("list_commit_statuses", Scope::ChecksRead),
1096 ("get_combined_status", Scope::ChecksRead),
1097 ("list_check_runs_for_ref", Scope::ChecksRead),
1098 ("get_check_run", Scope::ChecksRead),
1099 ("list_check_run_annotations", Scope::ChecksRead),
1100 ("list_check_suites_for_ref", Scope::ChecksRead),
1101 ("get_check_suite", Scope::ChecksRead),
1102 ("create_commit_status", Scope::ChecksWrite),
1103 ("create_check_run", Scope::ChecksWrite),
1104 ("update_check_run", Scope::ChecksWrite),
1105 ("rerequest_check_run", Scope::ChecksWrite),
1106 ("rerequest_check_suite", Scope::ChecksWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971107 // Deployments, wherever they run: reading them, and reporting them.
1108 ("list_deployments", Scope::DeploymentsRead),
1109 ("get_deployment", Scope::DeploymentsRead),
1110 ("list_deployment_statuses", Scope::DeploymentsRead),
1111 ("list_environments", Scope::DeploymentsRead),
1112 ("get_environment", Scope::DeploymentsRead),
1113 ("create_deployment", Scope::DeploymentsWrite),
1114 ("create_deployment_status", Scope::DeploymentsWrite),
Merge branch 'worktree-agent-a3abfcce648e87dca'1115 // What keeps runs safe: the runs environments hold and reviewing them,
1116 // approving a pull request's run, and a repository's own rules for
1117 // its environments and tokens, which are an admin's.
1118 ("get_pending_deployments", Scope::WorkflowsRead),
1119 ("review_pending_deployments", Scope::WorkflowsWrite),
1120 ("approve_workflow_run", Scope::WorkflowsWrite),
1121 ("get_workflow_permissions", Scope::RepoRead),
1122 ("get_fork_pr_approval", Scope::RepoRead),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1123 ("get_actions_access", Scope::RepoRead),
Merge branch 'worktree-agent-a3abfcce648e87dca'1124 ("update_environment", Scope::RepoAdmin),
1125 ("delete_environment", Scope::RepoAdmin),
1126 ("set_workflow_permissions", Scope::RepoAdmin),
1127 ("set_fork_pr_approval", Scope::RepoAdmin),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1128 ("set_actions_access", Scope::RepoAdmin),
Merge branch 'worktree-agent-a3abfcce648e87dca'1129 // Starting workflows from outside, as a push would.
1130 ("create_repository_dispatch", Scope::CodeWrite),
1131 // A workspace's policy for its repositories' tokens.
1132 ("get_workspace_workflow_permissions", Scope::WorkspaceRead),
1133 ("set_workspace_workflow_permissions", Scope::WorkspaceAdmin),
API and MCP for a workspace's personal access token rules, members' tokens and approvals1134 // A workspace's rules for personal access tokens, and the members'
1135 // tokens that reach it: who has access.
1136 ("get_token_policy", Scope::WorkspaceRead),
1137 ("set_token_policy", Scope::WorkspaceAdmin),
1138 ("list_member_tokens", Scope::AccessRead),
1139 ("list_token_requests", Scope::AccessRead),
1140 ("review_token_request", Scope::AccessAdmin),
1141 ("revoke_member_token", Scope::AccessAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1142 // Memory and the context hub.
1143 ("recall", Scope::MemoryRead),
1144 ("search_context", Scope::MemoryRead),
1145 ("get_entity", Scope::MemoryRead),
1146 ("get_context", Scope::MemoryRead),
1147 ("remember", Scope::MemoryWrite),
1148 // Who has access.
1149 ("list_collaborators", Scope::AccessRead),
1150 ("get_collaborator_permission", Scope::AccessRead),
1151 ("list_repo_invitations", Scope::AccessRead),
1152 ("list_outside_collaborators", Scope::AccessRead),
1153 ("add_collaborator", Scope::AccessAdmin),
1154 ("update_collaborator", Scope::AccessAdmin),
1155 ("remove_collaborator", Scope::AccessAdmin),
1156 ("revoke_repo_invitation", Scope::AccessAdmin),
1157 ("set_base_permission", Scope::AccessAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1158 ("set_team_repo", Scope::AccessAdmin),
1159 ("remove_team_repo", Scope::AccessAdmin),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1160 // Deploy keys: each lets a machine reach one repository, so they
1161 // are part of who has access.
1162 ("list_deploy_keys", Scope::AccessRead),
1163 ("get_deploy_key", Scope::AccessRead),
1164 ("create_deploy_key", Scope::AccessAdmin),
1165 ("delete_deploy_key", Scope::AccessAdmin),
Merge branch 'mirroring' into artifacts-mode1166 // Mirroring: a repository's links to other hosts. Reading them is
1167 // reading the repository; syncing writes code; the rest is an admin's.
1168 ("get_mirror", Scope::RepoRead),
1169 ("sync_mirror", Scope::CodeWrite),
1170 ("get_hand_back_plan", Scope::RepoAdmin),
1171 ("take_over_mirror", Scope::RepoAdmin),
1172 ("set_ci_failover", Scope::RepoAdmin),
1173 ("hand_back_mirror", Scope::RepoAdmin),
1174 ("move_mirror_to_g1t", Scope::RepoAdmin),
1175 ("add_mirror_remote", Scope::RepoAdmin),
1176 ("update_mirror_remote", Scope::RepoAdmin),
1177 ("remove_mirror_remote", Scope::RepoAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1178 // Webhooks.
1179 ("list_webhooks", Scope::WebhooksRead),
1180 ("list_webhook_deliveries", Scope::WebhooksRead),
1181 ("create_webhook", Scope::WebhooksAdmin),
1182 ("update_webhook", Scope::WebhooksAdmin),
1183 ("delete_webhook", Scope::WebhooksAdmin),
1184 ("ping_webhook", Scope::WebhooksAdmin),
1185 ("redeliver_webhook", Scope::WebhooksAdmin),
1186 // Secrets and variables.
1187 ("list_actions_secrets", Scope::SecretsRead),
1188 ("list_actions_variables", Scope::SecretsRead),
1189 ("set_actions_secret", Scope::SecretsAdmin),
1190 ("delete_actions_secret", Scope::SecretsAdmin),
1191 ("set_actions_variable", Scope::SecretsAdmin),
1192 ("delete_actions_variable", Scope::SecretsAdmin),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1193 // Self-hosted runners.
1194 ("list_runners", Scope::RunnersRead),
1195 ("list_runner_groups", Scope::RunnersRead),
1196 ("get_runner_settings", Scope::RunnersRead),
1197 ("create_runner_registration_token", Scope::RunnersAdmin),
1198 ("remove_runner", Scope::RunnersAdmin),
1199 ("create_runner_group", Scope::RunnersAdmin),
1200 ("update_runner_group", Scope::RunnersAdmin),
1201 ("delete_runner_group", Scope::RunnersAdmin),
1202 ("update_runner_settings", Scope::RunnersAdmin),
Merge packages: roles, Actions access, source label, soft delete, API1203 // Packages: reading them, their versions and who may use them needs
1204 // `packages:read`; changing their settings, access and Manage Actions
1205 // access `packages:write` (and the Admin role on the package, which the
1206 // packages service checks); deleting and restoring packages and
1207 // versions `packages:delete`, as the registries' own deletes do.
1208 ("list_packages", Scope::PackagesRead),
1209 ("get_package", Scope::PackagesRead),
1210 ("list_package_versions", Scope::PackagesRead),
1211 ("get_package_version", Scope::PackagesRead),
1212 ("list_package_access", Scope::PackagesRead),
1213 ("list_package_actions_access", Scope::PackagesRead),
1214 ("update_package", Scope::PackagesWrite),
1215 ("link_package", Scope::PackagesWrite),
1216 ("unlink_package", Scope::PackagesWrite),
1217 ("set_package_access", Scope::PackagesWrite),
1218 ("remove_package_access", Scope::PackagesWrite),
1219 ("set_package_actions_access", Scope::PackagesWrite),
1220 ("remove_package_actions_access", Scope::PackagesWrite),
1221 ("delete_package", Scope::PackagesDelete),
1222 ("restore_package", Scope::PackagesDelete),
1223 ("delete_package_version", Scope::PackagesDelete),
1224 ("restore_package_version", Scope::PackagesDelete),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1225 // The AI Gateway. Sending a request to a model needs `models:write`,
1226 // checked by the model proxy at models.g1t.sh, not here.
1227 ("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 step1228];
1229
1230/// Operations any token may use: saying who it is.
1231pub const NO_SCOPE: &[&str] = &["whoami"];
1232
1233/// The scope `operation` needs. `None` for one in [`NO_SCOPE`]; an
1234/// operation in neither list needs full access.
1235pub fn scope_for(operation: &str) -> Option<Scope> {
1236 OPERATIONS
1237 .iter()
1238 .find(|(name, _)| *name == operation)
1239 .map(|(_, scope)| *scope)
1240}
1241
1242/// What a token needs for `operation` with this input beyond its own
1243/// scope: starting agents from an operation that can, and making a
1244/// repository public or private.
1245pub fn extra_scopes(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
1246 let mut extra = Vec::new();
1247 let assigns = input["assign"].as_bool() == Some(true)
1248 || input["agent"].as_bool() == Some(true)
1249 || input["assign_agent"].as_bool() == Some(true);
1250 if assigns && matches!(operation, "apply_plan" | "import_issue" | "create_issue") {
1251 extra.push(Scope::AgentsRun);
1252 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1253 // Fixing an alert opens an issue and puts g1t on it.
1254 if operation == "fix_security_alert" {
1255 extra.extend([Scope::IssuesWrite, Scope::AgentsRun]);
1256 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1257 // Opening the issue an agent is put on.
1258 if operation == "delegate" {
1259 extra.push(Scope::IssuesWrite);
1260 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1261 // A workspace's base permission is who has access.
1262 if operation == "update_workspace" && input.get("base_permission").is_some_and(|v| !v.is_null()) {
1263 extra.push(Scope::AccessAdmin);
1264 }
Merge checks: statuses and check runs on every commit1265 // Asking a g1t Actions job or run to run again reruns its workflow.
1266 if matches!(operation, "rerequest_check_run" | "rerequest_check_suite")
1267 && input["id"].as_str().is_some_and(|id| id.starts_with("job_") || id.starts_with("run_"))
1268 {
1269 extra.push(Scope::WorkflowsWrite);
1270 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1271 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())) {
1272 extra.push(Scope::RepoAdmin);
1273 }
1274 extra
1275}
1276
1277/// The scopes a call needs, its own first.
1278pub fn needed(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
1279 scope_for(operation)
1280 .into_iter()
1281 .chain(extra_scopes(operation, input))
1282 .collect()
1283}
1284
1285/// Whether `access` may use `operation` with `input`. The person's (or
1286/// workspace's) role is checked after this, by the service that owns what
1287/// was asked about.
1288pub fn decide(access: &TokenAccess, operation: &str, input: &serde_json::Value) -> Decision {
1289 let rule = if access.legacy { "token:legacy" } else { "token:scope" };
Merge branch 'worktree-agent-a3abfcce648e87dca'1290 // A workflow job may open or approve pull requests only where its
1291 // repository and workspace let it, as on GitHub.
1292 if let Some(job) = &access.job
1293 && !job.pull_requests
1294 && (operation == "create_pull_request" || (operation == "review_pull_request" && input["verdict"].as_str() == Some("approve")))
1295 {
1296 return Decision::deny(
1297 "token:pull-requests",
1298 "A workflow job cannot open or approve pull requests here: an admin can allow it under Settings, Actions.",
1299 );
1300 }
1301 if let Some(only) = access.repo.as_deref()
1302 && !NO_SCOPE.contains(&operation)
1303 {
1304 match input["repo"].as_str() {
1305 Some(repo) if access.reaches(repo) => {}
1306 Some(repo) => {
1307 return Decision::deny("token:repository", format!("This token is a workflow job's in {only}: it cannot reach {repo}."));
1308 }
1309 None => {
1310 return Decision::deny("token:repository", format!("This token is a workflow job's: it reaches only {only}, and {operation} is not about one repository."));
1311 }
1312 }
1313 }
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1314 // A token made for one workspace (or none) only reads outside it:
1315 // public repositories, as anyone may. Inside it, its repository
1316 // selection is checked with its owner's role (`access::granted`).
1317 if let Some(reach) = &access.reach
Token reach: workflow_files scope, fine-grained reach, workspace token cap1318 && let Some(repo) = input["repo"].as_str()
1319 && !NO_SCOPE.contains(&operation)
1320 {
1321 let namespace = repo.split('/').next().unwrap_or_default();
1322 let changes = needed(operation, input).iter().any(|scope| scope.level() != Level::Read);
1323 if changes && !reach.owned_by(namespace) {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1324 let made_for = reach.workspace.as_deref().map_or_else(|| "your account only".to_owned(), |workspace| format!("the workspace {workspace}"));
Token reach: workflow_files scope, fine-grained reach, workspace token cap1325 return Decision::deny(
1326 "token:resource-owner",
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1327 format!("This access token is made for {made_for}: elsewhere it can only read public repositories, and {repo} is not in its reach."),
Token reach: workflow_files scope, fine-grained reach, workspace token cap1328 );
1329 }
1330 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1331 if access.scopes.is_some() {
1332 let known = NO_SCOPE.contains(&operation) || scope_for(operation).is_some();
1333 if !known {
1334 return Decision::deny("token:scope", format!("This access token cannot use {operation}: it needs full access."));
1335 }
1336 if let Some(missing) = needed(operation, input).into_iter().find(|scope| !access.allows(*scope)) {
1337 return Decision::deny(
1338 "token:scope",
1339 format!("This access token needs the {} scope to use {operation}.", missing.as_str()),
1340 );
1341 }
1342 }
1343 Decision::allow(rule)
1344}
1345
Merge branch 'worktree-agent-a3abfcce648e87dca'1346/// Whether a token may use the repository `owner/name` at all: a refusal
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1347/// for a workflow job's token or a deploy key in another repository,
1348/// else `None`. Git and
Merge branch 'worktree-agent-a3abfcce648e87dca'1349/// the package registries ask this before [`decide_git`] and
1350/// [`decide_packages`].
1351pub fn decide_repo(access: &TokenAccess, repo: &str) -> Option<Decision> {
1352 let only = access.repo.as_deref()?;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1353 let why = if access.deploy_key.is_some() {
1354 format!("This deploy key is for {only}: it cannot reach {repo}.")
1355 } else {
1356 format!("This token is a workflow job's in {only}: it cannot reach {repo}.")
1357 };
1358 (!access.reaches(repo)).then(|| Decision::deny("token:repository", why))
Merge branch 'worktree-agent-a3abfcce648e87dca'1359}
1360
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1361/// Whether a token may clone or fetch (`write` false), or push to (`write`
1362/// true), a repository with git. `public` is whether anyone may read it,
1363/// which needs no scope.
1364pub fn decide_git(access: &TokenAccess, write: bool, public: bool) -> Decision {
1365 let needed = if write { Scope::CodeWrite } else { Scope::CodeRead };
1366 if !access.allows(needed) && (write || !public) {
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1367 if access.deploy_key.is_some() {
1368 return Decision::deny(
1369 "token:scope",
1370 "This deploy key is read-only. An admin of the repository can add it again with write access to push with it.",
1371 );
1372 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1373 return Decision::deny(
1374 "token:scope",
1375 format!("This access token needs the {} scope to {} with git.", needed.as_str(), if write { "push" } else { "clone or fetch a private repository" }),
1376 );
1377 }
1378 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
1379}
1380
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1381/// Whether a token may pull (`Level::Read`), push or publish
1382/// (`Level::Write`), or delete (`Level::Delete`) packages. `public` is
1383/// whether anyone may pull the package, which needs no scope.
1384pub fn decide_packages(access: &TokenAccess, level: Level, public: bool) -> Decision {
1385 let (needed, doing) = match level {
1386 Level::Read => (Scope::PackagesRead, "pull a private package"),
1387 Level::Delete | Level::Admin => (Scope::PackagesDelete, "delete packages"),
1388 Level::Write | Level::Run => (Scope::PackagesWrite, "push or publish packages"),
1389 };
1390 if !access.allows(needed) && !(level == Level::Read && public) {
1391 return Decision::deny(
1392 "token:scope",
1393 format!("This access token needs the {} scope to {doing}.", needed.as_str()),
1394 );
1395 }
1396 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
1397}
1398
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1399#[cfg(test)]
1400mod tests {
1401 use super::*;
1402 use serde_json::json;
1403
1404 fn token(scopes: &[Scope]) -> TokenAccess {
1405 TokenAccess {
1406 token_id: "tok_1".to_owned(),
1407 scopes: Some(scopes.iter().map(|scope| scope.as_str().to_owned()).collect()),
1408 legacy: false,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1409 name: None,
Merge branch 'worktree-agent-a3abfcce648e87dca'1410 ..TokenAccess::default()
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1411 }
1412 }
1413
1414 #[test]
1415 fn every_scope_reads_back_and_belongs_to_a_resource() {
1416 for scope in Scope::ALL {
1417 assert_eq!(Scope::parse(scope.as_str()), Some(scope));
1418 assert!(scope.as_str().starts_with(scope.resource().as_str()));
1419 assert!(scope.includes(scope));
1420 }
1421 assert_eq!(Scope::parse(" Issues:Write "), Some(Scope::IssuesWrite));
1422 assert_eq!(Scope::parse("issues"), None);
1423 }
1424
1425 #[test]
1426 fn a_higher_level_includes_the_lower_ones_of_its_resource_only() {
1427 assert!(Scope::RepoAdmin.includes(Scope::RepoRead));
1428 assert!(Scope::RepoAdmin.includes(Scope::RepoWrite));
1429 assert!(Scope::IssuesWrite.includes(Scope::IssuesRead));
1430 assert!(!Scope::IssuesRead.includes(Scope::IssuesWrite));
1431 assert!(!Scope::RepoAdmin.includes(Scope::CodeWrite));
1432 assert!(!Scope::PullRequestsWrite.includes(Scope::IssuesWrite));
1433 }
1434
1435 #[test]
1436 fn operations_are_listed_once_and_never_also_free() {
1437 let mut seen = std::collections::HashSet::new();
1438 for (name, _) in OPERATIONS {
1439 assert!(seen.insert(*name), "{name} twice");
1440 assert!(!NO_SCOPE.contains(name), "{name}");
1441 }
1442 }
1443
1444 #[test]
1445 fn scopes_are_parsed_from_oauth_text_leaving_out_unknown_ones() {
1446 assert_eq!(
1447 parse_scopes("issues:write repo:read,bogus:thing issues:write"),
1448 vec![Scope::RepoRead, Scope::IssuesWrite]
1449 );
1450 assert_eq!(scopes_text(&[Scope::RepoRead, Scope::IssuesWrite]), "repo:read issues:write");
1451 }
1452
1453 #[test]
1454 fn the_oauth_default_is_the_agent_preset_and_never_admin() {
1455 let scopes = oauth_default();
1456 assert!(scopes.contains(&Scope::IssuesWrite));
1457 assert!(scopes.contains(&Scope::PullRequestsWrite));
1458 assert!(scopes.contains(&Scope::AgentsRun));
1459 assert!(scopes.iter().all(|scope| !scope.dangerous()), "{scopes:?}");
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet1460 for read in Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read && scope.offered()) {
1461 // Every read offered but the machines work runs on.
Fast pages, required checks on the branch, self-hosted runners, honest incidents1462 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 step1463 }
1464 assert!(Preset::ReadOnly.scopes().unwrap().iter().all(|scope| scope.level() == Level::Read));
1465 assert_eq!(Preset::Full.scopes(), None);
1466 }
1467
1468 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1469 fn billing_is_read_by_presets_and_changed_by_none_but_full_access() {
1470 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::BillingRead));
1471 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
1472 assert!(!preset.scopes().unwrap().contains(&Scope::BillingWrite), "{}", preset.as_str());
1473 }
1474 assert_eq!(scope_for("set_budget"), Some(Scope::BillingWrite));
1475 assert_eq!(scope_for("buy_ai_credit"), Some(Scope::BillingWrite));
1476 assert_eq!(scope_for("get_usage"), Some(Scope::BillingRead));
1477 let reader = token(&[Scope::BillingRead]);
1478 assert!(decide(&reader, "list_invoices", &json!({})).allowed);
1479 assert!(decide(&reader, "set_budget", &json!({})).reason.unwrap().contains("billing:write"));
1480 }
1481
1482 #[test]
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1483 fn the_ai_gateway_spends_only_with_models_write_which_no_preset_gives() {
1484 // Reading the log is a read like any other.
1485 assert_eq!(scope_for("list_gateway_requests"), Some(Scope::ModelsRead));
1486 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::ModelsRead));
1487 // Sending requests spends the workspace's AI credit: chosen on purpose.
1488 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
1489 assert!(!preset.scopes().unwrap().contains(&Scope::ModelsWrite), "{}", preset.as_str());
1490 }
1491 assert!(Scope::ModelsWrite.includes(Scope::ModelsRead));
1492 assert!(!Scope::ModelsWrite.dangerous());
1493 assert!(token(&[Scope::ModelsWrite]).allows(Scope::ModelsWrite));
1494 assert!(!token(&[Scope::BillingWrite]).allows(Scope::ModelsWrite));
1495 assert!(TokenAccess::full().allows(Scope::ModelsWrite));
1496 }
1497
1498 #[test]
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet1499 fn artifacts_scopes_exist_but_are_not_offered_yet() {
1500 for scope in [Scope::ArtifactsRead, Scope::ArtifactsWrite, Scope::ArtifactsAdmin] {
1501 assert_eq!(scope.resource(), Resource::Artifacts);
1502 assert!(!scope.offered());
1503 assert_eq!(Scope::parse(scope.as_str()), Some(scope));
1504 // Nothing hands it out: not presets, full access, OAuth or the form.
1505 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
1506 assert!(!preset.scopes().unwrap().contains(&scope), "{}", preset.as_str());
1507 }
1508 assert!(!everything().contains(&scope));
1509 assert!(!offered_scopes().contains(&scope));
1510 assert!(!oauth_default().contains(&scope));
1511 assert!(parse_scopes(scope.as_str()).is_empty());
1512 // And no operation needs it yet.
1513 assert!(OPERATIONS.iter().all(|(_, needed)| *needed != scope));
1514 }
1515 assert!(Scope::ArtifactsAdmin.includes(Scope::ArtifactsWrite));
1516 assert!(Scope::ArtifactsAdmin.dangerous());
1517 assert_eq!(Resource::Artifacts.group(), ResourceGroup::Workspace);
1518 let asked = std::collections::BTreeMap::from([("artifacts".to_owned(), "read".to_owned())]);
1519 assert_eq!(resolve_permissions(&asked, true), Err("There is no permission called artifacts.".to_owned()));
1520 assert_eq!(offered_scopes().len(), Scope::ALL.len() - 3);
1521 }
1522
1523 #[test]
Merge checks: statuses and check runs on every commit1524 fn checks_are_reported_with_checks_write_which_ci_gets() {
1525 assert_eq!(scope_for("create_check_run"), Some(Scope::ChecksWrite));
1526 assert_eq!(scope_for("create_commit_status"), Some(Scope::ChecksWrite));
1527 assert_eq!(scope_for("list_check_runs_for_ref"), Some(Scope::ChecksRead));
1528 let ci = Preset::Ci.scopes().unwrap();
1529 assert!(ci.contains(&Scope::ChecksWrite));
1530 assert!(!Preset::Agent.scopes().unwrap().contains(&Scope::ChecksWrite));
1531 let reporter = token(&[Scope::ChecksWrite]);
1532 assert!(decide(&reporter, "update_check_run", &json!({ "id": "cr_1" })).allowed);
1533 assert!(decide(&reporter, "rerequest_check_run", &json!({ "id": "cr_1" })).allowed);
1534 // A g1t Actions job runs again as its workflow does.
1535 let refused = decide(&reporter, "rerequest_check_run", &json!({ "id": "job_1" }));
1536 assert!(refused.reason.unwrap().contains("workflows:write"));
1537 }
1538
1539 #[test]
Merge branch 'worktree-agent-a3abfcce648e87dca'1540 fn a_job_token_reaches_its_repository_only() {
1541 let job = TokenAccess {
1542 repo: Some("acme/web".into()),
1543 job: Some(JobToken { run_id: "run_1".into(), job_id: "job_1".into(), pull_requests: false }),
1544 ..token(&[Scope::RepoRead, Scope::IssuesWrite, Scope::IssuesRead, Scope::PullRequestsWrite])
1545 };
1546 assert!(decide(&job, "create_issue", &json!({ "repo": "acme/web" })).allowed);
1547 assert!(decide(&job, "create_issue", &json!({ "repo": "Acme/Web" })).allowed, "names compare without case");
1548 let elsewhere = decide(&job, "create_issue", &json!({ "repo": "acme/api" }));
1549 assert!(!elsewhere.allowed);
1550 assert_eq!(elsewhere.rule, "token:repository");
1551 // Nothing beyond the one repository, a workspace's listing included.
1552 assert!(!decide(&job, "list_repos", &json!({})).allowed);
1553 assert!(decide(&job, "whoami", &json!({})).allowed);
1554 // Its scopes still hold inside it.
1555 assert!(!decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).allowed);
1556 assert!(decide_repo(&job, "acme/web").is_none());
1557 assert!(!decide_repo(&job, "acme/api").unwrap().allowed);
1558 assert!(decide_repo(&token(&[Scope::CodeRead]), "acme/api").is_none(), "other tokens reach what their owner can");
1559 // Opening and approving pull requests is off unless allowed.
1560 assert_eq!(decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).rule, "token:pull-requests");
1561 assert!(!decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "approve" })).allowed);
1562 assert!(decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "request_changes" })).allowed);
1563 let allowed = TokenAccess { job: Some(JobToken { pull_requests: true, ..job.job.clone().unwrap() }), ..job.clone() };
1564 assert!(decide(&allowed, "create_pull_request", &json!({ "repo": "acme/web" })).allowed);
1565 }
1566
1567 #[test]
Workflow files need workflow_files:write from a token; fine-grained permission table1568 fn workflow_files_need_their_own_scope() {
1569 for path in [".g1t/workflows/ci.yml", ".github/workflows/deploy.yaml", "/.github/workflows/x.yml", ".GitHub/Workflows/ci.yml", ".github/workflows"] {
1570 assert!(is_workflow_file(path), "{path}");
1571 }
1572 for path in ["README.md", ".github/CODEOWNERS", ".github/workflowsx/ci.yml", "docs/.github/workflows/ci.yml", ".g1t/actions/ci.yml"] {
1573 assert!(!is_workflow_file(path), "{path}");
1574 }
1575 let code = token(&[Scope::CodeWrite]);
1576 let refused = decide_workflow_files(Some(&code), ["README.md", ".github/workflows/ci.yml"]).unwrap();
1577 assert_eq!(refused.rule, "token:workflows");
1578 assert!(refused.reason.as_deref().unwrap().contains(".github/workflows/ci.yml"));
1579 assert!(refused.reason.as_deref().unwrap().contains("workflow_files:write"));
1580 assert!(decide_workflow_files(Some(&code), ["README.md"]).is_none());
1581 assert!(decide_workflow_files(Some(&token(&[Scope::CodeWrite, Scope::WorkflowFilesWrite])), [".g1t/workflows/ci.yml"]).is_none());
1582 assert!(decide_workflow_files(Some(&TokenAccess::full()), [".g1t/workflows/ci.yml"]).is_none(), "full access");
1583 assert!(decide_workflow_files(None, [".g1t/workflows/ci.yml"]).is_none(), "a signed-in person");
1584 // A job's token never may, as GITHUB_TOKEN never may.
1585 let job = TokenAccess { job: Some(JobToken::default()), ..TokenAccess::full() };
1586 assert!(decide_workflow_files(Some(&job), [".g1t/workflows/ci.yml"]).unwrap().reason.unwrap().contains("job"));
1587 // Nothing in a preset changes workflow files but full access.
1588 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
1589 assert!(!preset.scopes().unwrap().contains(&Scope::WorkflowFilesWrite), "{}", preset.as_str());
1590 }
1591 assert!(!Scope::WorkflowFilesWrite.includes(Scope::WorkflowsWrite) && !Scope::WorkflowsWrite.includes(Scope::WorkflowFilesWrite));
1592 }
1593
1594 #[test]
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1595 fn a_narrowed_token_only_reads_outside_its_workspace() {
1596 let reach = TokenReach { workspace: Some("acme".into()), repositories: RepositorySelection::All, repo_ids: Vec::new() };
1597 let fine = TokenAccess { reach: Some(reach), ..token(&[Scope::RepoRead, Scope::IssuesRead, Scope::IssuesWrite]) };
Workflow files need workflow_files:write from a token; fine-grained permission table1598 assert!(decide(&fine, "create_issue", &json!({ "repo": "acme/web" })).allowed);
1599 assert!(decide(&fine, "create_issue", &json!({ "repo": "Acme/web" })).allowed);
1600 let elsewhere = decide(&fine, "create_issue", &json!({ "repo": "globex/site" }));
1601 assert_eq!(elsewhere.rule, "token:resource-owner");
1602 assert!(elsewhere.reason.unwrap().contains("acme"));
1603 assert!(decide(&fine, "get_issue", &json!({ "repo": "globex/site" })).allowed, "public repositories elsewhere read");
1604 assert!(!decide(&fine, "create_pull_request", &json!({ "repo": "acme/web" })).allowed, "its scopes still hold");
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1605 let mine = TokenAccess { reach: Some(TokenReach::default()), ..token(&[Scope::IssuesWrite]) };
Workflow files need workflow_files:write from a token; fine-grained permission table1606 assert!(decide(&mine, "create_issue", &json!({ "repo": "acme/web" })).reason.unwrap().contains("your account"));
1607 assert!(fine.covers_repo("rep_1", "acme") && !fine.covers_repo("rep_1", "globex"));
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1608 let selected = TokenReach { workspace: Some("acme".into()), repositories: RepositorySelection::Selected, repo_ids: vec!["rep_1".into()] };
Workflow files need workflow_files:write from a token; fine-grained permission table1609 assert!(selected.covers("rep_1", "ACME") && !selected.covers("rep_2", "acme"));
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1610 let public = TokenReach { repositories: RepositorySelection::Public, ..selected.clone() };
Workflow files need workflow_files:write from a token; fine-grained permission table1611 assert!(!public.covers("rep_1", "acme") && public.owned_by("acme"));
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1612 assert!(token(&[]).covers_repo("rep_1", "anything"), "a token for every workspace reaches what its owner can");
Workflow files need workflow_files:write from a token; fine-grained permission table1613 assert_eq!(RepositorySelection::parse("public_only"), Some(RepositorySelection::Public));
1614 }
1615
1616 #[test]
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1617 fn permissions_are_scopes_read_per_resource() {
1618 let asked: std::collections::BTreeMap<String, String> =
1619 [("issues", "write"), ("repo", "read"), ("code", "none"), ("packages", "delete")].iter().map(|(a, b)| ((*a).to_owned(), (*b).to_owned())).collect();
1620 let scopes = resolve_permissions(&asked, true).unwrap();
1621 assert_eq!(scopes, vec![Scope::RepoRead, Scope::PackagesDelete, Scope::IssuesWrite]);
1622 let back = permissions_of(&scopes);
1623 assert_eq!(back.get("issues").map(String::as_str), Some("write"));
1624 assert_eq!(back.get("packages").map(String::as_str), Some("delete"));
1625 assert!(!back.contains_key("code"));
1626 // Lower levels held beside a higher one say nothing more.
1627 assert_eq!(top_scopes(&[Scope::RepoRead, Scope::RepoAdmin, Scope::RepoWrite]), vec![Scope::RepoAdmin]);
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet1628 // Every offered resource's top, and nothing a level can lose.
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1629 let all = everything();
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet1630 assert_eq!(all.len(), Resource::ALL.into_iter().filter(|resource| resource.offered()).count());
1631 for scope in offered_scopes() {
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1632 assert!(all.iter().any(|held| held.includes(scope)), "{scope:?}");
1633 }
1634 }
1635
1636 #[test]
1637 fn permissions_are_checked_by_name_level_and_owner() {
1638 let one = |name: &str, level: &str| -> std::collections::BTreeMap<String, String> { [(name.to_owned(), level.to_owned())].into() };
1639 assert!(resolve_permissions(&one("wiki", "read"), true).unwrap_err().contains("wiki"));
1640 assert!(resolve_permissions(&one("issues", "admin"), true).unwrap_err().contains("read, write"));
1641 assert!(resolve_permissions(&one("workflow_files", "read"), true).is_err(), "workflow files are written only");
1642 assert!(resolve_permissions(&one("notifications", "read"), false).unwrap_err().contains("account"));
1643 assert_eq!(resolve_permissions(&one("notifications", "read"), true).unwrap(), vec![Scope::NotificationsRead]);
1644 assert_eq!(resolve_permissions(&one("agents", "run"), false).unwrap(), vec![Scope::AgentsRun]);
1645 for resource in Resource::ALL {
1646 assert_eq!(Resource::parse(resource.as_str()), Some(resource));
1647 assert!(!resource.scopes().is_empty());
1648 }
1649 }
1650
1651 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1652 fn a_legacy_token_can_do_everything() {
1653 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
1654 for (operation, _) in OPERATIONS {
1655 assert!(decide(&legacy, operation, &json!({})).allowed, "{operation}");
1656 }
1657 assert_eq!(decide(&legacy, "delete_repo", &json!({})).rule, "token:legacy");
1658 }
1659
1660 #[test]
1661 fn a_missing_scope_is_named() {
1662 let read = token(&[Scope::IssuesRead]);
1663 assert!(decide(&read, "get_issue", &json!({})).allowed);
1664 assert!(decide(&read, "whoami", &json!({})).allowed);
1665 let refused = decide(&read, "create_issue", &json!({}));
1666 assert!(!refused.allowed);
1667 assert_eq!(refused.reason.as_deref(), Some("This access token needs the issues:write scope to use create_issue."));
1668 // An operation the table does not know needs full access.
1669 assert!(!decide(&read, "something_new", &json!({})).allowed);
1670 }
1671
1672 #[test]
1673 fn starting_agents_from_another_operation_needs_agents_run() {
1674 let writer = token(&[Scope::IssuesWrite]);
1675 assert!(decide(&writer, "apply_plan", &json!({})).allowed);
1676 let refused = decide(&writer, "apply_plan", &json!({ "assign": true }));
1677 assert!(refused.reason.unwrap().contains("agents:run"));
1678 let maintainer = token(&[Scope::RepoWrite]);
1679 assert!(decide(&maintainer, "update_repo", &json!({ "description": "x" })).allowed);
1680 assert!(!decide(&maintainer, "update_repo", &json!({ "private": true })).allowed);
1681 }
1682
1683 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1684 fn a_workspaces_base_permission_needs_access_admin_too() {
1685 let admin = token(&[Scope::WorkspaceAdmin]);
1686 assert!(decide(&admin, "update_workspace", &json!({ "name": "Acme" })).allowed);
1687 let refused = decide(&admin, "update_workspace", &json!({ "name": "Acme", "base_permission": "read" }));
1688 assert!(refused.reason.unwrap().contains("access:admin"));
1689 let both = token(&[Scope::WorkspaceAdmin, Scope::AccessAdmin]);
1690 assert!(decide(&both, "update_workspace", &json!({ "base_permission": "read" })).allowed);
1691 assert!(!decide(&token(&[Scope::WorkspaceRead]), "update_workspace", &json!({ "name": "Acme" })).allowed);
1692 }
1693
1694 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1695 fn delegating_needs_both_agents_and_issues() {
1696 let agents = token(&[Scope::AgentsRun]);
1697 assert!(decide(&agents, "delegate", &json!({})).reason.unwrap().contains("issues:write"));
1698 let both = token(&[Scope::AgentsRun, Scope::IssuesWrite]);
1699 assert!(decide(&both, "delegate", &json!({})).allowed);
1700 }
1701
1702 #[test]
1703 fn git_push_needs_code_write_and_private_reads_need_code_read() {
1704 let reader = token(&[Scope::CodeRead]);
1705 assert!(decide_git(&reader, false, false).allowed);
1706 let refused = decide_git(&reader, true, false);
1707 assert!(!refused.allowed);
1708 assert!(refused.reason.unwrap().contains("code:write"));
1709 let issues = token(&[Scope::IssuesWrite]);
1710 assert!(!decide_git(&issues, false, false).allowed);
1711 assert!(decide_git(&issues, false, true).allowed, "public code needs no scope");
1712 assert!(!decide_git(&issues, true, true).allowed, "pushing to public code still needs code:write");
1713 let writer = token(&[Scope::CodeWrite]);
1714 assert!(decide_git(&writer, true, false).allowed);
1715 assert!(decide_git(&writer, false, false).allowed, "code:write includes code:read");
1716 assert!(decide_git(&TokenAccess::full(), true, false).allowed);
1717 }
1718
1719 #[test]
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1720 fn packages_need_their_own_scopes_and_public_pulls_none() {
1721 let reader = token(&[Scope::PackagesRead]);
1722 assert!(decide_packages(&reader, Level::Read, false).allowed);
1723 assert!(!decide_packages(&reader, Level::Write, false).allowed);
1724 let code = token(&[Scope::CodeWrite]);
1725 assert!(!decide_packages(&code, Level::Read, false).allowed, "code scopes are not package scopes");
1726 assert!(decide_packages(&code, Level::Read, true).allowed, "public packages pull with any token");
1727 let writer = token(&[Scope::PackagesWrite]);
1728 assert!(decide_packages(&writer, Level::Write, false).allowed);
1729 assert!(decide_packages(&writer, Level::Read, false).allowed, "packages:write includes packages:read");
1730 let refused = decide_packages(&writer, Level::Delete, false);
1731 assert!(refused.reason.unwrap().contains("packages:delete"));
1732 assert!(decide_packages(&token(&[Scope::PackagesDelete]), Level::Write, false).allowed);
1733 assert!(Scope::PackagesDelete.dangerous());
1734 // Tokens made before these scopes, and full-access ones, keep working.
1735 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
1736 assert!(decide_packages(&legacy, Level::Delete, false).allowed);
1737 assert!(decide_packages(&TokenAccess::full(), Level::Write, false).allowed);
1738 }
1739
1740 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1741 fn token_access_travels_as_json() {
1742 let access = token(&[Scope::IssuesRead]);
1743 let wire = serde_json::to_value(&access).unwrap();
1744 assert_eq!(wire["scopes"], json!(["issues:read"]));
1745 assert!(wire.get("resources").is_none());
1746 let back: TokenAccess = serde_json::from_value(wire).unwrap();
1747 assert_eq!(back, access);
1748 let full: TokenAccess = serde_json::from_value(json!({})).unwrap();
1749 assert!(full.is_full());
1750 // A reach written by an older version is ignored: a token reaches
1751 // whatever its owner can.
1752 let older: TokenAccess = serde_json::from_value(json!({
1753 "token_id": "tok_1",
1754 "scopes": ["issues:read"],
1755 "resources": { "kind": "repositories", "repositories": ["acme/rocket"] },
1756 }))
1757 .unwrap();
1758 assert_eq!(older, access);
Merge branch 'worktree-agent-ad4439ce85a91ecb4' into integrate1759 // Using the website is off unless set, and said only when on.
1760 assert!(!access.website);
1761 assert!(wire_of(&access).get("website").is_none());
1762 let website = TokenAccess { website: true, ..access };
1763 assert_eq!(wire_of(&website)["website"], json!(true));
1764 // Never part of full access.
1765 assert!(!TokenAccess::full().website);
1766 }
1767
1768 fn wire_of(access: &TokenAccess) -> serde_json::Value {
1769 serde_json::to_value(access).unwrap()
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1770 }
1771
1772 /// The site's copy of the table, `packages/contracts/src/scopes.ts`,
1773 /// lists the same scopes in the same order, the same operations with
1774 /// the same scopes, and the same presets.
1775 #[test]
1776 fn the_typescript_mirror_has_the_same_table() {
1777 let ts = include_str!("../../../packages/contracts/src/scopes.ts");
1778 let section = |start: &str| {
1779 ts.split_once(start)
1780 .and_then(|(_, rest)| rest.split_once("] as const"))
1781 .map(|(table, _)| table)
1782 .unwrap_or_else(|| panic!("{start} in scopes.ts"))
1783 };
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet1784 let names = |table: &str| -> Vec<String> {
1785 section(table)
1786 .lines()
1787 .filter_map(|line| line.split_once("scope: \"").and_then(|(_, rest)| rest.split_once('"')).map(|(scope, _)| scope.to_owned()))
1788 .collect()
1789 };
1790 // Offered scopes in `SCOPES`, the rest in `UPCOMING_SCOPES`.
1791 let offered: Vec<String> = Scope::ALL.iter().filter(|scope| scope.offered()).map(|scope| scope.as_str().to_owned()).collect();
1792 let upcoming: Vec<String> = Scope::ALL.iter().filter(|scope| !scope.offered()).map(|scope| scope.as_str().to_owned()).collect();
1793 assert_eq!(names("export const SCOPES = ["), offered);
1794 assert_eq!(names("export const UPCOMING_SCOPES = ["), upcoming);
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1795 let operations: Vec<(String, String)> = section("export const OPERATION_SCOPES = [")
1796 .lines()
1797 .filter_map(|line| {
1798 let mut quoted = line.split('"').skip(1).step_by(2);
1799 Some((quoted.next()?.to_owned(), quoted.next()?.to_owned()))
1800 })
1801 .collect();
1802 let expected: Vec<(String, String)> = OPERATIONS
1803 .iter()
1804 .map(|(name, scope)| ((*name).to_owned(), scope.as_str().to_owned()))
1805 .collect();
1806 assert_eq!(operations, expected);
1807 for preset in Preset::ALL {
1808 let list = section(&format!("{}: [", preset.as_str()));
1809 let mirrored: Vec<&str> = list
1810 .split(',')
1811 .map(|item| item.trim().trim_matches('"'))
1812 .filter(|item| !item.is_empty())
1813 .collect();
1814 let expected: Vec<&str> = preset
1815 .scopes()
1816 .map(|scopes| scopes.iter().map(|scope| scope.as_str()).collect())
1817 .unwrap_or_else(|| vec!["*"]);
1818 assert_eq!(mirrored, expected, "{}", preset.as_str());
1819 }
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet1820 // Each resource with its group, in the same order: offered ones in
1821 // `SCOPE_RESOURCES`, the rest in `UPCOMING_RESOURCES`.
1822 for (table, offered) in [("export const SCOPE_RESOURCES", true), ("export const UPCOMING_RESOURCES", false)] {
1823 let resources = ts
1824 .split_once(table)
1825 .and_then(|(_, rest)| rest.split_once("
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1826];"))
Artifacts contracts: folios, their four kinds, dashboard datasets, folio events and the artifacts scopes are typed and validated the same in TypeScript and Rust, with nothing using them yet1827 .map(|(table, _)| table)
1828 .unwrap_or_else(|| panic!("{table} in scopes.ts"));
1829 let rows: Vec<&str> = resources.lines().filter(|line| line.trim_start().starts_with("{ resource:")).collect();
1830 let expected: Vec<Resource> = Resource::ALL.into_iter().filter(|resource| resource.offered() == offered).collect();
1831 assert_eq!(rows.len(), expected.len(), "{table}");
1832 for (row, resource) in rows.iter().zip(expected) {
1833 assert!(row.contains(&format!("resource: \"{}\"", resource.as_str())), "{row}");
1834 assert!(row.contains(&format!("group: \"{}\"", resource.group().as_str())), "{row}");
1835 }
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1836 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1837 }
1838}

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