Skip to content
1,876 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
Merge main into Artifacts Phase 247 /// code): the `artifact` MCP tool and `/workspaces/{ws}/artifacts`.
48 /// Not workflow runs' artifacts, which are `workflows:*`.
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 yet49 Artifacts,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step50}
51
52impl 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 yet53 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 step54 Resource::Repo,
55 Resource::Code,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar56 Resource::Security,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member57 Resource::Packages,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step58 Resource::Issues,
59 Resource::PullRequests,
60 Resource::Agents,
61 Resource::Workflows,
Token reach: workflow_files scope, fine-grained reach, workspace token cap62 Resource::WorkflowFiles,
Merge checks: statuses and check runs on every commit63 Resource::Checks,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9764 Resource::Deployments,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step65 Resource::Memory,
66 Resource::Account,
API: notifications over REST and MCP, with notifications scopes67 Resource::Notifications,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step68 Resource::Workspace,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit69 Resource::Billing,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step70 Resource::Access,
71 Resource::Webhooks,
72 Resource::Secrets,
Fast pages, required checks on the branch, self-hosted runners, honest incidents73 Resource::Runners,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens74 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 yet75 Resource::Artifacts,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step76 ];
77
78 pub fn as_str(self) -> &'static str {
79 match self {
80 Resource::Account => "account",
API: notifications over REST and MCP, with notifications scopes81 Resource::Notifications => "notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step82 Resource::Workspace => "workspace",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit83 Resource::Billing => "billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step84 Resource::Repo => "repo",
85 Resource::Code => "code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar86 Resource::Security => "security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member87 Resource::Packages => "packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step88 Resource::Issues => "issues",
89 Resource::PullRequests => "pull_requests",
90 Resource::Agents => "agents",
91 Resource::Workflows => "workflows",
Token reach: workflow_files scope, fine-grained reach, workspace token cap92 Resource::WorkflowFiles => "workflow_files",
Merge checks: statuses and check runs on every commit93 Resource::Checks => "checks",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9794 Resource::Deployments => "deployments",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step95 Resource::Memory => "memory",
96 Resource::Access => "access",
97 Resource::Webhooks => "webhooks",
98 Resource::Secrets => "secrets",
Fast pages, required checks on the branch, self-hosted runners, honest incidents99 Resource::Runners => "runners",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens100 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 yet101 Resource::Artifacts => "artifacts",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step102 }
103 }
104
105 /// Its name, for people.
106 pub fn label(self) -> &'static str {
107 match self {
108 Resource::Account => "Your account",
API: notifications over REST and MCP, with notifications scopes109 Resource::Notifications => "Notifications",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step110 Resource::Workspace => "Workspaces",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit111 Resource::Billing => "Billing",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step112 Resource::Repo => "Repositories",
113 Resource::Code => "Code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar114 Resource::Security => "Security",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member115 Resource::Packages => "Packages",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step116 Resource::Issues => "Issues",
117 Resource::PullRequests => "Pull requests",
118 Resource::Agents => "g1t agents",
119 Resource::Workflows => "Workflows",
Token reach: workflow_files scope, fine-grained reach, workspace token cap120 Resource::WorkflowFiles => "Workflow files",
Merge checks: statuses and check runs on every commit121 Resource::Checks => "Checks and statuses",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97122 Resource::Deployments => "Deployments",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step123 Resource::Memory => "Memory and context",
124 Resource::Access => "Who has access",
125 Resource::Webhooks => "Webhooks",
126 Resource::Secrets => "Secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents127 Resource::Runners => "Self-hosted runners",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens128 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 yet129 Resource::Artifacts => "Artifacts",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step130 }
131 }
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 yet132
Merge main into Artifacts Phase 2133 /// Whether tokens are offered it yet. A resource being built can be in
134 /// the table before its API ships (so its scopes parse, and the
135 /// TypeScript mirror lists it under `UPCOMING_RESOURCES`) while nothing
136 /// hands it out: presets, full access, OAuth and the token form leave
137 /// it out, and no operation needs it. Every resource is offered now;
138 /// Artifacts was the last, until Phase 3 of docs/ARTIFACTS_MODE.md.
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 yet139 pub fn offered(self) -> bool {
Merge main into Artifacts Phase 2140 let _ = self;
141 true
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 yet142 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step143}
144
145/// How much of a resource.
146#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
147pub enum Level {
148 Read,
149 Write,
150 /// Starting g1t's agents, which spends the workspace's money.
151 Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member152 /// Deleting what cannot be brought back, such as a package's versions.
153 Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step154 Admin,
155}
156
157impl Level {
158 pub fn as_str(self) -> &'static str {
159 match self {
160 Level::Read => "read",
161 Level::Write => "write",
162 Level::Run => "run",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member163 Level::Delete => "delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step164 Level::Admin => "admin",
165 }
166 }
167}
168
169/// One scope. Its text form, `resource:level`, is what tokens store, OAuth
170/// clients ask for, and errors name.
171#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
172pub enum Scope {
173 RepoRead,
174 RepoWrite,
175 RepoAdmin,
176 CodeRead,
177 CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar178 SecurityRead,
179 SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member180 PackagesRead,
181 PackagesWrite,
182 PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step183 IssuesRead,
184 IssuesWrite,
185 PullRequestsRead,
186 PullRequestsWrite,
187 AgentsRun,
188 WorkflowsRead,
189 WorkflowsWrite,
Token reach: workflow_files scope, fine-grained reach, workspace token cap190 WorkflowFilesWrite,
Merge checks: statuses and check runs on every commit191 ChecksRead,
192 ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97193 DeploymentsRead,
194 DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step195 MemoryRead,
196 MemoryWrite,
197 AccountRead,
198 AccountWrite,
API: notifications over REST and MCP, with notifications scopes199 NotificationsRead,
200 NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step201 WorkspaceRead,
202 WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit203 BillingRead,
204 BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step205 AccessRead,
206 AccessAdmin,
207 WebhooksRead,
208 WebhooksAdmin,
209 SecretsRead,
210 SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents211 RunnersRead,
212 RunnersAdmin,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens213 ModelsRead,
214 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 yet215 ArtifactsRead,
216 ArtifactsWrite,
217 ArtifactsAdmin,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step218}
219
220impl Scope {
221 /// 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 yet222 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 step223 Scope::RepoRead,
224 Scope::RepoWrite,
225 Scope::RepoAdmin,
226 Scope::CodeRead,
227 Scope::CodeWrite,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar228 Scope::SecurityRead,
229 Scope::SecurityWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member230 Scope::PackagesRead,
231 Scope::PackagesWrite,
232 Scope::PackagesDelete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step233 Scope::IssuesRead,
234 Scope::IssuesWrite,
235 Scope::PullRequestsRead,
236 Scope::PullRequestsWrite,
237 Scope::AgentsRun,
238 Scope::WorkflowsRead,
239 Scope::WorkflowsWrite,
Token reach: workflow_files scope, fine-grained reach, workspace token cap240 Scope::WorkflowFilesWrite,
Merge checks: statuses and check runs on every commit241 Scope::ChecksRead,
242 Scope::ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97243 Scope::DeploymentsRead,
244 Scope::DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step245 Scope::MemoryRead,
246 Scope::MemoryWrite,
247 Scope::AccountRead,
248 Scope::AccountWrite,
API: notifications over REST and MCP, with notifications scopes249 Scope::NotificationsRead,
250 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step251 Scope::WorkspaceRead,
252 Scope::WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit253 Scope::BillingRead,
254 Scope::BillingWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step255 Scope::AccessRead,
256 Scope::AccessAdmin,
257 Scope::WebhooksRead,
258 Scope::WebhooksAdmin,
259 Scope::SecretsRead,
260 Scope::SecretsAdmin,
Fast pages, required checks on the branch, self-hosted runners, honest incidents261 Scope::RunnersRead,
262 Scope::RunnersAdmin,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens263 Scope::ModelsRead,
264 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 yet265 Scope::ArtifactsRead,
266 Scope::ArtifactsWrite,
267 Scope::ArtifactsAdmin,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step268 ];
269
270 pub fn as_str(self) -> &'static str {
271 match self {
272 Scope::RepoRead => "repo:read",
273 Scope::RepoWrite => "repo:write",
274 Scope::RepoAdmin => "repo:admin",
275 Scope::CodeRead => "code:read",
276 Scope::CodeWrite => "code:write",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar277 Scope::SecurityRead => "security:read",
278 Scope::SecurityWrite => "security:write",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member279 Scope::PackagesRead => "packages:read",
280 Scope::PackagesWrite => "packages:write",
281 Scope::PackagesDelete => "packages:delete",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step282 Scope::IssuesRead => "issues:read",
283 Scope::IssuesWrite => "issues:write",
284 Scope::PullRequestsRead => "pull_requests:read",
285 Scope::PullRequestsWrite => "pull_requests:write",
286 Scope::AgentsRun => "agents:run",
287 Scope::WorkflowsRead => "workflows:read",
288 Scope::WorkflowsWrite => "workflows:write",
Token reach: workflow_files scope, fine-grained reach, workspace token cap289 Scope::WorkflowFilesWrite => "workflow_files:write",
Merge checks: statuses and check runs on every commit290 Scope::ChecksRead => "checks:read",
291 Scope::ChecksWrite => "checks:write",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97292 Scope::DeploymentsRead => "deployments:read",
293 Scope::DeploymentsWrite => "deployments:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step294 Scope::MemoryRead => "memory:read",
295 Scope::MemoryWrite => "memory:write",
296 Scope::AccountRead => "account:read",
297 Scope::AccountWrite => "account:write",
API: notifications over REST and MCP, with notifications scopes298 Scope::NotificationsRead => "notifications:read",
299 Scope::NotificationsWrite => "notifications:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step300 Scope::WorkspaceRead => "workspace:read",
301 Scope::WorkspaceAdmin => "workspace:admin",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit302 Scope::BillingRead => "billing:read",
303 Scope::BillingWrite => "billing:write",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step304 Scope::AccessRead => "access:read",
305 Scope::AccessAdmin => "access:admin",
306 Scope::WebhooksRead => "webhooks:read",
307 Scope::WebhooksAdmin => "webhooks:admin",
308 Scope::SecretsRead => "secrets:read",
309 Scope::SecretsAdmin => "secrets:admin",
Fast pages, required checks on the branch, self-hosted runners, honest incidents310 Scope::RunnersRead => "runners:read",
311 Scope::RunnersAdmin => "runners:admin",
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens312 Scope::ModelsRead => "models:read",
313 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 yet314 Scope::ArtifactsRead => "artifacts:read",
315 Scope::ArtifactsWrite => "artifacts:write",
316 Scope::ArtifactsAdmin => "artifacts:admin",
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step317 }
318 }
319
320 pub fn parse(text: &str) -> Option<Scope> {
321 let text = text.trim().to_ascii_lowercase();
322 Scope::ALL.into_iter().find(|scope| scope.as_str() == text)
323 }
324
325 pub fn resource(self) -> Resource {
326 let name = self.as_str().split_once(':').map_or("", |(resource, _)| resource);
327 Resource::ALL
328 .into_iter()
329 .find(|resource| resource.as_str() == name)
330 .unwrap_or(Resource::Account)
331 }
332
333 pub fn level(self) -> Level {
334 match self.as_str().rsplit_once(':').map_or("", |(_, level)| level) {
335 "write" => Level::Write,
336 "run" => Level::Run,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member337 "delete" => Level::Delete,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step338 "admin" => Level::Admin,
339 _ => Level::Read,
340 }
341 }
342
343 /// Whether holding `self` gives `other`: the same resource, at the same
344 /// level or a lower one.
345 pub fn includes(self, other: Scope) -> bool {
346 self.resource() == other.resource() && self.level() >= other.level()
347 }
348
349 /// Changes that are hard or impossible to undo, or that decide who can
350 /// reach what. Shown behind a warning wherever scopes are chosen.
351 pub fn dangerous(self) -> bool {
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member352 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 step353 }
354
355 /// What it lets a token do, in plain words.
356 pub fn describe(self) -> &'static str {
357 match self {
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97358 Scope::RepoRead => "See repositories, their settings, labels, timelines, releases, languages, contributors and security alerts, and search",
359 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 merge360 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 step361 Scope::CodeRead => "Clone and fetch private repositories with git",
362 Scope::CodeWrite => "Push commits with git",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar363 Scope::SecurityRead => "See secret scanning, code scanning and vulnerability alerts, custom patterns, the dependency graph and SBOM, and security settings",
364 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 member365 Scope::PackagesRead => "Pull container images and install private packages",
366 Scope::PackagesWrite => "Push container images and publish packages",
Merge packages: roles, Actions access, source label, soft delete, API367 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 step368 Scope::IssuesRead => "Read issues, comments and plans",
369 Scope::IssuesWrite => "Open, edit, close and comment on issues",
370 Scope::PullRequestsRead => "Read pull requests, their changes, sessions and merge queues",
371 Scope::PullRequestsWrite => "Open, review, close and merge pull requests",
372 Scope::AgentsRun => "Put g1t agents to work and message them, which uses the workspace's money",
373 Scope::WorkflowsRead => "Read workflows, runs and logs",
374 Scope::WorkflowsWrite => "Run, cancel, rerun and turn workflows on or off",
Token reach: workflow_files scope, fine-grained reach, workspace token cap375 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 commit376 Scope::ChecksRead => "Read commits' statuses, check runs, check suites and annotations",
377 Scope::ChecksWrite => "Report statuses and check runs on commits, and ask for checks to run again",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97378 Scope::DeploymentsRead => "See deployments, their statuses and environments",
379 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 step380 Scope::MemoryRead => "Recall memory and search the workspace's context",
381 Scope::MemoryWrite => "Save memory for the next agent",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97382 Scope::AccountRead => "Read your email addresses, invites, invitations, pinned projects and stars",
383 Scope::AccountWrite => "Change your email addresses, make invites, answer invitations, pin projects and star repositories",
API: notifications over REST and MCP, with notifications scopes384 Scope::NotificationsRead => "See your inbox, its threads, and what you subscribe to and watch",
385 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 merge386 Scope::WorkspaceRead => "Read workspace settings, invites, integrations, model routes, teams and rulesets",
387 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 audit388 Scope::BillingRead => "See a workspace's usage, budget, AI credit and invoices",
389 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 step390 Scope::AccessRead => "See who has access to repositories",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar391 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 step392 Scope::WebhooksRead => "See webhooks and their deliveries",
393 Scope::WebhooksAdmin => "Create, change and delete webhooks",
394 Scope::SecretsRead => "List secrets (never their values) and read variables",
395 Scope::SecretsAdmin => "Set and delete secrets and variables",
Fast pages, required checks on the branch, self-hosted runners, honest incidents396 Scope::RunnersRead => "See self-hosted runners, their groups and where agents run",
397 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 tokens398 Scope::ModelsRead => "See the workspace's AI Gateway requests: their models, tokens, cost and status",
399 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 yet400 Scope::ArtifactsRead => "List, read and search artifacts you can see, their versions, and the numbers their dashboards show",
401 Scope::ArtifactsWrite => "Create, rename, move, edit, trash and restore artifacts, and propose changes to them",
402 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 step403 }
404 }
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 yet405
406 /// Whether tokens are offered it yet: its resource's [`Resource::offered`].
407 pub fn offered(self) -> bool {
408 self.resource().offered()
409 }
410}
411
412/// Every scope tokens are offered, in table order: what OAuth advertises.
413pub fn offered_scopes() -> Vec<Scope> {
414 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 step415}
416
417impl Serialize for Scope {
418 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
419 serializer.serialize_str(self.as_str())
420 }
421}
422
423impl<'de> Deserialize<'de> for Scope {
424 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
425 let text = String::deserialize(deserializer)?;
426 Scope::parse(&text).ok_or_else(|| serde::de::Error::custom(format!("unknown scope {text}")))
427 }
428}
429
430/// Scopes as written in a token's row or an OAuth request: separated by
431/// spaces or commas. Unknown names are left out, so a client asking for a
432/// scope from a newer version gets the rest.
433pub fn parse_scopes(text: &str) -> Vec<Scope> {
434 let mut scopes: Vec<Scope> = text
435 .split(|c: char| c.is_whitespace() || c == ',')
436 .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 yet437 .filter(|scope| scope.offered())
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step438 .collect();
439 normalize(&mut scopes);
440 scopes
441}
442
443/// In table order, without repeats.
444pub fn normalize(scopes: &mut Vec<Scope>) {
445 let given = std::mem::take(scopes);
446 scopes.extend(Scope::ALL.into_iter().filter(|scope| given.contains(scope)));
447}
448
449/// Space-separated, as stored and as OAuth writes them.
450pub fn scopes_text(scopes: &[Scope]) -> String {
451 scopes.iter().map(|scope| scope.as_str()).collect::<Vec<_>>().join(" ")
452}
453
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers454/// Where a resource sits on the token form, and which tokens may hold it.
455#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
456#[serde(rename_all = "snake_case")]
457pub enum ResourceGroup {
458 /// About repositories: what they hold and how they are run.
459 Repository,
460 /// About a workspace itself.
461 Workspace,
462 /// About the person: only a personal token may hold these.
463 Account,
464}
465
466impl ResourceGroup {
467 pub const ALL: [ResourceGroup; 3] = [ResourceGroup::Repository, ResourceGroup::Workspace, ResourceGroup::Account];
468
469 pub fn as_str(self) -> &'static str {
470 match self {
471 ResourceGroup::Repository => "repository",
472 ResourceGroup::Workspace => "workspace",
473 ResourceGroup::Account => "account",
474 }
475 }
476}
477
478impl Resource {
479 pub fn group(self) -> ResourceGroup {
480 match self {
481 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 yet482 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 managers483 _ => ResourceGroup::Repository,
484 }
485 }
486
487 pub fn parse(text: &str) -> Option<Resource> {
488 let text = text.trim().to_ascii_lowercase();
489 Resource::ALL.into_iter().find(|resource| resource.as_str() == text)
490 }
491
492 /// Its scopes, least first.
493 pub fn scopes(self) -> Vec<Scope> {
494 Scope::ALL.into_iter().filter(|scope| scope.resource() == self).collect()
495 }
496}
497
498// --- Permissions --------------------------------------------------------------
499//
500// A token's permissions are its scopes read per resource: each resource
501// at none or one level (`{"issues": "write", "repo": "read"}`). A level
502// includes the ones below it, so the highest scope held of each resource
503// says everything; that is what a token stores. Personal tokens and a
504// workspace's own tokens are made, shown and checked this way alike.
505
506/// The highest scope of each resource held, in table order: the fewest
507/// scopes that give the same access, as tokens store them.
508pub fn top_scopes(scopes: &[Scope]) -> Vec<Scope> {
509 let mut top: Vec<Scope> = Vec::new();
510 for resource in Resource::ALL {
511 if let Some(best) = scopes.iter().filter(|scope| scope.resource() == resource).max_by_key(|scope| scope.level()) {
512 top.push(*best);
513 }
514 }
515 normalize(&mut top);
516 top
517}
518
519/// Every resource at its highest level: all a token can be given.
520pub 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 yet521 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 managers522}
523
524/// Scopes as permissions: each resource held, by name, at its highest
525/// level held.
526pub fn permissions_of(scopes: &[Scope]) -> std::collections::BTreeMap<String, String> {
527 top_scopes(scopes)
528 .into_iter()
529 .map(|scope| (scope.resource().as_str().to_owned(), scope.level().as_str().to_owned()))
530 .collect()
531}
532
533/// Permissions as asked for (`{"issues": "write"}`, `none` or empty left
534/// out) into the scopes a token stores, or why they cannot be. `personal`
535/// is whether the token is a person's: only theirs may hold account ones.
536pub fn resolve_permissions(asked: &std::collections::BTreeMap<String, String>, personal: bool) -> Result<Vec<Scope>, String> {
537 let mut scopes = Vec::new();
538 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 yet539 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 managers540 return Err(format!("There is no permission called {name}."));
541 };
542 let level = level.trim().to_ascii_lowercase();
543 if level.is_empty() || level == "none" {
544 continue;
545 }
546 let Some(scope) = Scope::parse(&format!("{}:{level}", resource.as_str())) else {
547 let levels: Vec<&str> = resource.scopes().iter().map(|scope| scope.level().as_str()).collect();
548 return Err(format!("{} is none or {}, not {level}.", resource.as_str(), levels.join(", ")));
549 };
550 if resource.group() == ResourceGroup::Account && !personal {
551 return Err(format!("{} is about a person's account: a workspace's token cannot hold it.", resource.as_str()));
552 }
553 scopes.push(scope);
554 }
555 Ok(top_scopes(&scopes))
556}
557
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step558/// What a token stores for full access, which is not a scope a client can
559/// ask for by name.
560pub const FULL_ACCESS: &str = "*";
561
562/// Starting points for choosing scopes.
563#[derive(Clone, Copy, Debug, PartialEq, Eq)]
564pub enum Preset {
565 ReadOnly,
566 Agent,
567 Ci,
568 Full,
569}
570
571impl Preset {
572 pub const ALL: [Preset; 4] = [Preset::ReadOnly, Preset::Agent, Preset::Ci, Preset::Full];
573
574 pub fn as_str(self) -> &'static str {
575 match self {
576 Preset::ReadOnly => "read_only",
577 Preset::Agent => "agent",
578 Preset::Ci => "ci",
579 Preset::Full => "full",
580 }
581 }
582
583 pub fn label(self) -> &'static str {
584 match self {
585 Preset::ReadOnly => "Read only",
586 Preset::Agent => "Agent",
587 Preset::Ci => "CI",
588 Preset::Full => "Full access",
589 }
590 }
591
592 /// Its scopes; `None` for full access.
593 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 yet594 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 step595 match self {
596 Preset::ReadOnly => Some(reads().collect()),
597 Preset::Agent => {
Fast pages, required checks on the branch, self-hosted runners, honest incidents598 // Not the machines work runs on: an agent has no business
599 // knowing a workspace's own runners.
600 let mut scopes: Vec<Scope> = reads().filter(|scope| scope.resource() != Resource::Runners).collect();
API: notifications over REST and MCP, with notifications scopes601 // And answering what needs the person it works for: marking
602 // it done, subscribing, watching.
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step603 scopes.extend([
604 Scope::CodeWrite,
605 Scope::IssuesWrite,
606 Scope::PullRequestsWrite,
607 Scope::AgentsRun,
608 Scope::MemoryWrite,
API: notifications over REST and MCP, with notifications scopes609 Scope::NotificationsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step610 ]);
611 normalize(&mut scopes);
612 Some(scopes)
613 }
614 Preset::Ci => Some(vec![
615 Scope::RepoRead,
616 Scope::CodeRead,
617 Scope::CodeWrite,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member618 Scope::PackagesRead,
619 Scope::PackagesWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step620 Scope::WorkflowsRead,
621 Scope::WorkflowsWrite,
Merge checks: statuses and check runs on every commit622 Scope::ChecksRead,
623 Scope::ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97624 Scope::DeploymentsRead,
625 Scope::DeploymentsWrite,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step626 ]),
627 Preset::Full => None,
628 }
629 }
630}
631
632/// What an OAuth client gets when it asks for nothing in particular: the
633/// agent preset. Never an admin scope.
634pub fn oauth_default() -> Vec<Scope> {
635 Preset::Agent.scopes().unwrap_or_default()
636}
637
638/// Set on a [`crate::User`] resolved from an access token: what the token
639/// may do. Absent on a signed-in session, which may do whatever its person
640/// can.
641#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
642pub struct TokenAccess {
643 /// The token's id, as audit entries and errors name it.
644 #[serde(default)]
645 pub token_id: String,
646 /// Its scopes, as `resource:level`. Absent: full access, everything the
647 /// person (or workspace) can do.
648 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub scopes: Option<Vec<String>>,
650 /// Made before tokens had scopes: full access until someone narrows it.
651 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
652 pub legacy: bool,
Merge branch 'worktree-agent-a3abfcce648e87dca'653 /// Set on a workflow job's token (`G1T_TOKEN`): the one repository it
654 /// reaches, as `owner/name`. Every other is refused, whatever its owner
655 /// could reach.
656 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub repo: Option<String>,
658 /// Set on a workflow job's token: the run and job it was made for. The
659 /// audit log records its changes as that job's, and what it changes
660 /// starts no workflows (only `workflow_dispatch` and
661 /// `repository_dispatch` do), so a workflow cannot set itself off.
662 #[serde(default, skip_serializing_if = "Option::is_none")]
663 pub job: Option<JobToken>,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens664 /// The token's name, as its owner gave it, so a log can say which
665 /// token made a request. Absent where whoever resolved it did not say.
666 #[serde(default, skip_serializing_if = "Option::is_none")]
667 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 managers668 /// Set on a token narrowed to less than its owner can reach: one
669 /// workspace (all, selected or none of its private repositories), or
670 /// none at all (its owner's account and public repositories). Absent
671 /// on a token that reaches every workspace its owner can.
672 ///
673 /// The wire key is `fine_grained`, kept from before tokens were one
674 /// kind, so services deployed at different moments agree on it.
675 #[serde(rename = "fine_grained", default, skip_serializing_if = "Option::is_none")]
676 pub reach: Option<TokenReach>,
Token reach: workflow_files scope, fine-grained reach, workspace token cap677 /// Set on a workspace's own token that an owner gave Admin when making
678 /// it. Without it a workspace's token has Write on the workspace's
679 /// repositories, as a member would (see [`crate::access`]).
680 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
681 pub admin: bool,
682 /// Set on what a repository's deploy key resolves to: the key's id. Its
683 /// `repo` is the one repository it reaches.
684 #[serde(default, skip_serializing_if = "Option::is_none")]
685 pub deploy_key: Option<String>,
Merge main into Artifacts Phase 2686 /// Set on a person's token whose owner let it use the website (g1t.sh)
687 /// as them, sent as `Authorization: Bearer`. Not a scope: no preset,
688 /// full access or OAuth grant includes it, and git, the API and MCP
689 /// ignore it.
690 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
691 pub website: bool,
Token reach: workflow_files scope, fine-grained reach, workspace token cap692}
693
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers694/// Which repositories a token reaches in the workspace it is made for.
Token reach: workflow_files scope, fine-grained reach, workspace token cap695#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
696#[serde(rename_all = "snake_case")]
697pub enum RepositorySelection {
698 /// Every repository of the workspace, ones made later included.
699 #[default]
700 All,
701 /// The repositories chosen, by id.
702 Selected,
703 /// None of the workspace's private repositories: public repositories,
704 /// 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 managers705 /// With no workspace: the owner's account and public repositories only.
Token reach: workflow_files scope, fine-grained reach, workspace token cap706 Public,
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step707}
708
Token reach: workflow_files scope, fine-grained reach, workspace token cap709impl RepositorySelection {
710 pub fn as_str(self) -> &'static str {
711 match self {
712 RepositorySelection::All => "all",
713 RepositorySelection::Selected => "selected",
714 RepositorySelection::Public => "public",
715 }
716 }
717
718 pub fn parse(text: &str) -> Option<RepositorySelection> {
719 match text.trim().to_ascii_lowercase().as_str() {
720 "all" => Some(RepositorySelection::All),
721 "selected" => Some(RepositorySelection::Selected),
722 "public" | "public_only" | "none" => Some(RepositorySelection::Public),
723 _ => None,
724 }
725 }
726}
727
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers728/// What a narrowed token reaches, as identity resolves it on each use.
Token reach: workflow_files scope, fine-grained reach, workspace token cap729#[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 managers730pub struct TokenReach {
731 /// The workspace whose repositories and settings it reaches, by slug as
732 /// it is now. Absent: its owner's account only, with public
733 /// repositories read-only.
Token reach: workflow_files scope, fine-grained reach, workspace token cap734 #[serde(default, skip_serializing_if = "Option::is_none")]
735 pub workspace: Option<String>,
736 #[serde(default)]
737 pub repositories: RepositorySelection,
738 /// With [`RepositorySelection::Selected`]: the repositories' ids.
739 #[serde(default, skip_serializing_if = "Vec::is_empty")]
740 pub repo_ids: Vec<String>,
741}
742
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers743impl TokenReach {
Token reach: workflow_files scope, fine-grained reach, workspace token cap744 /// Whether it reaches the repository with this id in the workspace
745 /// `namespace` for more than what anyone may do with a public one.
746 pub fn covers(&self, repo_id: &str, namespace: &str) -> bool {
747 let Some(workspace) = self.workspace.as_deref() else {
748 return false;
749 };
750 if !workspace.eq_ignore_ascii_case(namespace) {
751 return false;
752 }
753 match self.repositories {
754 RepositorySelection::All => true,
755 RepositorySelection::Selected => self.repo_ids.iter().any(|id| id == repo_id),
756 RepositorySelection::Public => false,
757 }
758 }
759
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers760 /// Whether it is made for the workspace `slug`.
Token reach: workflow_files scope, fine-grained reach, workspace token cap761 pub fn owned_by(&self, slug: &str) -> bool {
762 self.workspace.as_deref().is_some_and(|workspace| workspace.eq_ignore_ascii_case(slug))
763 }
764}
765
766/// Where workflow files live. Adding, changing or deleting a file under
767/// one, with git or through g1t, needs [`Scope::WorkflowFilesWrite`] from a
768/// token: what GitHub's `workflow` scope and `workflows` permission do.
769pub const WORKFLOW_DIRS: [&str; 2] = [".g1t/workflows/", ".github/workflows/"];
770
771/// Whether `path` is a workflow file, or a file in one's directory.
772pub fn is_workflow_file(path: &str) -> bool {
773 let path = path.trim_start_matches('/');
774 WORKFLOW_DIRS.iter().any(|dir| {
775 path.len() >= dir.len() && path.is_char_boundary(dir.len()) && path[..dir.len()].eq_ignore_ascii_case(dir)
776 }) || WORKFLOW_DIRS.iter().any(|dir| path.eq_ignore_ascii_case(dir.trim_end_matches('/')))
777}
778
779/// Whether a token may add, change or delete the files at `paths`: a
780/// refusal naming the first workflow file it may not touch, else `None`.
781/// A signed-in person (no token) is never refused here; their role decides.
782pub fn decide_workflow_files<'a>(access: Option<&TokenAccess>, paths: impl IntoIterator<Item = &'a str>) -> Option<Decision> {
783 let access = access?;
784 if access.allows(Scope::WorkflowFilesWrite) && access.job.is_none() {
785 return None;
786 }
787 let path = paths.into_iter().find(|path| is_workflow_file(path))?;
788 let why = if access.job.is_some() {
789 "a workflow job's token can never add or change workflow files".to_owned()
790 } else {
791 format!("it needs the {} scope", Scope::WorkflowFilesWrite.as_str())
792 };
793 Some(Decision::deny(
794 "token:workflows",
795 format!("This access token cannot change the workflow file {path}: {why}."),
796 ))
797}
798
Merge branch 'worktree-agent-a3abfcce648e87dca'799/// The workflow job a token was made for.
800#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
801pub struct JobToken {
802 /// The run, `run_…`.
803 pub run_id: String,
804 /// The job, `job_…`.
805 pub job_id: String,
806 /// Whether it may open pull requests and approve them, by its
807 /// repository's and workspace's choice ("Allow g1t Actions to create and
808 /// approve pull requests"). Off unless chosen.
809 #[serde(default)]
810 pub pull_requests: bool,
811}
812
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step813impl TokenAccess {
814 /// Full access to everything: the access tokens made before scopes had.
815 pub fn full() -> Self {
816 TokenAccess::default()
817 }
818
Merge branch 'worktree-agent-a3abfcce648e87dca'819 /// Whether it may reach the repository `owner/name`: every token but a
820 /// workflow job's, which reaches its own repository only.
821 pub fn reaches(&self, repo: &str) -> bool {
822 self.repo.as_deref().is_none_or(|only| only.eq_ignore_ascii_case(repo))
823 }
824
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step825 pub fn is_full(&self) -> bool {
826 self.scopes.is_none()
827 }
828
829 /// The scopes it holds, or `None` for full access.
830 pub fn granted(&self) -> Option<Vec<Scope>> {
831 self.scopes
832 .as_ref()
833 .map(|scopes| scopes.iter().filter_map(|scope| Scope::parse(scope)).collect())
834 }
835
836 pub fn allows(&self, needed: Scope) -> bool {
837 match self.granted() {
838 None => true,
839 Some(granted) => granted.iter().any(|held| held.includes(needed)),
840 }
841 }
Token reach: workflow_files scope, fine-grained reach, workspace token cap842
843 /// 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 managers844 /// more than reading a public one: every token but a narrowed one
845 /// outside its workspace or repository selection. Its owner's role
Token reach: workflow_files scope, fine-grained reach, workspace token cap846 /// still decides; see [`crate::access`].
847 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 managers848 self.reach.as_ref().is_none_or(|reach| reach.covers(repo_id, namespace))
Token reach: workflow_files scope, fine-grained reach, workspace token cap849 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step850}
851
852/// Every operation of the API and MCP server, with the scope it needs. An
853/// operation in [`NO_SCOPE`] needs none. The API checks that every one of
854/// its operations is in exactly one of the two.
855pub const OPERATIONS: &[(&str, Scope)] = &[
856 // Your account.
857 ("list_emails", Scope::AccountRead),
858 ("add_email", Scope::AccountWrite),
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)859 ("confirm_email", Scope::AccountWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step860 ("remove_email", Scope::AccountWrite),
861 ("update_email_settings", Scope::AccountWrite),
862 ("list_invites", Scope::AccountRead),
863 ("create_invite", Scope::AccountWrite),
864 ("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)865 ("list_invitations", Scope::AccountRead),
866 ("accept_invitation", Scope::AccountWrite),
867 ("decline_invitation", Scope::AccountWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step868 ("list_my_repo_invitations", Scope::AccountRead),
869 ("accept_repo_invitation", Scope::AccountWrite),
870 ("decline_repo_invitation", Scope::AccountWrite),
API: pinned projects over REST and MCP871 // Your pinned projects: a preference of your account.
872 ("list_pinned_projects", Scope::AccountRead),
873 ("pin_project", Scope::AccountWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97874 // Your stars: a preference of your account.
875 ("list_starred", Scope::AccountRead),
876 ("check_starred", Scope::AccountRead),
877 ("star_repo", Scope::AccountWrite),
878 ("unstar_repo", Scope::AccountWrite),
API: pinned projects over REST and MCP879 ("unpin_project", Scope::AccountWrite),
880 ("reorder_pinned_projects", Scope::AccountWrite),
API: notifications over REST and MCP, with notifications scopes881 // Your inbox: notifications, subscriptions and watching.
882 ("list_notifications", Scope::NotificationsRead),
883 ("get_notification_thread", Scope::NotificationsRead),
884 ("get_thread_subscription", Scope::NotificationsRead),
885 ("get_repo_subscription", Scope::NotificationsRead),
886 ("list_watched_repos", Scope::NotificationsRead),
887 ("mark_notifications_read", Scope::NotificationsWrite),
888 ("mark_thread_read", Scope::NotificationsWrite),
889 ("mark_thread_done", Scope::NotificationsWrite),
890 ("save_thread", Scope::NotificationsWrite),
891 ("snooze_thread", Scope::NotificationsWrite),
892 ("set_thread_subscription", Scope::NotificationsWrite),
893 ("delete_thread_subscription", Scope::NotificationsWrite),
894 ("set_repo_subscription", Scope::NotificationsWrite),
895 ("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 step896 // Workspaces, their invites and integrations.
897 ("create_workspace", Scope::WorkspaceAdmin),
898 ("delete_workspace", Scope::WorkspaceAdmin),
Merge branch 'worktree-agent-ad7c6d88d93adc817'899 ("get_workspace", Scope::WorkspaceRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily900 ("update_workspace", Scope::WorkspaceAdmin),
Merge main (membership, two-factor, GitHub repo roles) into tokens901 // Its members, and who owns it.
902 ("list_members", Scope::WorkspaceRead),
903 ("update_member", Scope::WorkspaceAdmin),
904 ("remove_member", Scope::WorkspaceAdmin),
905 ("transfer_ownership", Scope::WorkspaceAdmin),
906 ("leave_workspace", Scope::AccountWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step907 ("list_workspace_invites", Scope::WorkspaceRead),
908 ("invite_member", Scope::WorkspaceAdmin),
909 ("revoke_workspace_invite", Scope::WorkspaceAdmin),
910 ("list_integrations", Scope::WorkspaceRead),
911 ("connect_integration", Scope::WorkspaceAdmin),
AI Gateway: OpenAI's format, open models, and your own providers912 ("update_integration", Scope::WorkspaceAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step913 ("disconnect_integration", Scope::WorkspaceAdmin),
914 ("test_integration", Scope::WorkspaceAdmin),
915 ("get_model_routes", Scope::WorkspaceRead),
916 ("set_model_routes", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar917 // Teams: reading them, and managing them. A team's role on a
918 // repository is who has access.
919 ("list_teams", Scope::WorkspaceRead),
920 ("get_team", Scope::WorkspaceRead),
921 ("list_team_members", Scope::WorkspaceRead),
922 ("list_child_teams", Scope::WorkspaceRead),
923 ("list_team_repos", Scope::WorkspaceRead),
924 ("list_user_teams", Scope::WorkspaceRead),
925 ("create_team", Scope::WorkspaceAdmin),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge926 ("list_workspace_rulesets", Scope::WorkspaceRead),
927 ("get_workspace_ruleset", Scope::WorkspaceRead),
928 ("list_workspace_rule_evaluations", Scope::WorkspaceRead),
929 ("create_workspace_ruleset", Scope::WorkspaceAdmin),
930 ("update_workspace_ruleset", Scope::WorkspaceAdmin),
931 ("delete_workspace_ruleset", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar932 ("update_team", Scope::WorkspaceAdmin),
933 ("delete_team", Scope::WorkspaceAdmin),
934 ("set_team_member", Scope::WorkspaceAdmin),
935 ("remove_team_member", Scope::WorkspaceAdmin),
936 ("set_team_review_assignment", Scope::WorkspaceAdmin),
Usage, Billing settings and prepaid AI credit; fixes from the UX audit937 // A workspace's billing: usage, budget, AI credit and invoices.
938 ("get_usage", Scope::BillingRead),
939 ("get_budget", Scope::BillingRead),
940 ("get_ai_credit", Scope::BillingRead),
941 ("list_invoices", Scope::BillingRead),
942 ("get_billing_details", Scope::BillingRead),
943 ("set_budget", Scope::BillingWrite),
944 ("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 step945 // Repositories.
946 ("list_repos", Scope::RepoRead),
947 ("get_repo", Scope::RepoRead),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97948 // Projects follow their repositories.
949 ("list_projects", Scope::RepoRead),
950 ("get_project", Scope::RepoRead),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step951 ("search", Scope::RepoRead),
952 ("list_events", Scope::RepoRead),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97953 // What the default branch says about a repository, who starred it, and
954 // its releases.
955 ("get_languages", Scope::RepoRead),
956 ("list_contributors", Scope::RepoRead),
957 ("get_license", Scope::RepoRead),
958 ("list_stargazers", Scope::RepoRead),
959 ("list_releases", Scope::RepoRead),
960 ("get_latest_release", Scope::RepoRead),
961 ("get_release_by_tag", Scope::RepoRead),
962 ("get_release", Scope::RepoRead),
963 ("create_release", Scope::RepoWrite),
964 ("update_release", Scope::RepoWrite),
965 ("delete_release", Scope::RepoWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step966 ("list_labels", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar967 ("list_milestones", Scope::RepoRead),
968 ("get_milestone", Scope::RepoRead),
969 ("create_label", Scope::IssuesWrite),
970 ("update_label", Scope::IssuesWrite),
971 ("delete_label", Scope::IssuesWrite),
972 ("add_default_labels", Scope::IssuesWrite),
973 ("create_milestone", Scope::IssuesWrite),
974 ("update_milestone", Scope::IssuesWrite),
975 ("delete_milestone", Scope::IssuesWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step976 ("get_repo_settings", Scope::RepoRead),
Fast pages, required checks on the branch, self-hosted runners, honest incidents977 ("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 step978 ("list_deleted_repos", Scope::RepoRead),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily979 ("list_security_alerts", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar980 ("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 step981 ("create_repo", Scope::RepoWrite),
982 ("update_repo", Scope::RepoWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97983 ("update_project", Scope::RepoWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step984 ("update_repo_settings", Scope::RepoWrite),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge985 // Rulesets: reading them is reading the repository; changing them
986 // changes what everyone, agents included, may do, so it is admin.
987 ("list_repo_rulesets", Scope::RepoRead),
988 ("get_repo_ruleset", Scope::RepoRead),
989 ("get_branch_rules", Scope::RepoRead),
990 ("list_rule_evaluations", Scope::RepoRead),
991 ("create_repo_ruleset", Scope::RepoAdmin),
992 ("update_repo_ruleset", Scope::RepoAdmin),
993 ("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 step994 ("rename_branch", Scope::RepoWrite),
995 ("rename_repo", Scope::RepoAdmin),
996 ("transfer_repo", Scope::RepoAdmin),
997 ("archive_repo", Scope::RepoAdmin),
998 ("unarchive_repo", Scope::RepoAdmin),
999 ("set_repo_visibility", Scope::RepoAdmin),
1000 ("delete_repo", Scope::RepoAdmin),
1001 ("restore_repo", Scope::RepoAdmin),
1002 ("purge_repo", Scope::RepoAdmin),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1003 // A dismissed secret is let through push protection.
1004 ("dismiss_security_alert", Scope::RepoAdmin),
1005 ("reopen_security_alert", Scope::RepoAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1006 // The security suite: alerts, push protection, patterns, code
1007 // scanning, the supply chain and settings.
1008 ("list_secret_scanning_alerts", Scope::SecurityRead),
1009 ("get_secret_scanning_alert", Scope::SecurityRead),
1010 ("list_secret_scanning_locations", Scope::SecurityRead),
1011 ("list_bypass_requests", Scope::SecurityRead),
1012 ("list_custom_patterns", Scope::SecurityRead),
1013 ("list_code_scanning_alerts", Scope::SecurityRead),
1014 ("get_code_scanning_alert", Scope::SecurityRead),
1015 ("list_code_scanning_analyses", Scope::SecurityRead),
1016 ("get_sarif_upload", Scope::SecurityRead),
1017 ("list_vulnerability_alerts", Scope::SecurityRead),
1018 ("get_vulnerability_alert", Scope::SecurityRead),
1019 ("get_dependency_graph", Scope::SecurityRead),
1020 ("get_sbom", Scope::SecurityRead),
1021 ("compare_dependencies", Scope::SecurityRead),
1022 ("get_security_settings", Scope::SecurityRead),
1023 ("get_workspace_security_settings", Scope::SecurityRead),
1024 ("get_security_overview", Scope::SecurityRead),
1025 ("update_secret_scanning_alert", Scope::SecurityWrite),
1026 ("bypass_push_protection", Scope::SecurityWrite),
1027 ("check_secret_validity", Scope::SecurityWrite),
1028 ("review_bypass_request", Scope::SecurityWrite),
1029 ("create_custom_pattern", Scope::SecurityWrite),
1030 ("update_custom_pattern", Scope::SecurityWrite),
1031 ("delete_custom_pattern", Scope::SecurityWrite),
1032 ("dry_run_custom_pattern", Scope::SecurityWrite),
1033 ("update_code_scanning_alert", Scope::SecurityWrite),
1034 ("upload_sarif", Scope::SecurityWrite),
1035 ("update_vulnerability_alert", Scope::SecurityWrite),
1036 ("fix_security_alert", Scope::SecurityWrite),
1037 ("update_security_settings", Scope::SecurityWrite),
1038 ("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 step1039 // Issues and plans.
1040 ("list_issues", Scope::IssuesRead),
1041 ("get_issue", Scope::IssuesRead),
1042 ("get_plan", Scope::IssuesRead),
1043 ("create_issue", Scope::IssuesWrite),
1044 ("update_issue", Scope::IssuesWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1045 ("list_issue_labels", Scope::IssuesRead),
1046 ("add_issue_labels", Scope::IssuesWrite),
1047 ("set_issue_labels", Scope::IssuesWrite),
1048 ("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 step1049 ("close_issue", Scope::IssuesWrite),
1050 ("reopen_issue", Scope::IssuesWrite),
1051 ("add_comment", Scope::IssuesWrite),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1052 ("edit_comment", Scope::IssuesWrite),
1053 ("delete_comment", Scope::IssuesWrite),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1054 ("import_issue", Scope::IssuesWrite),
1055 ("apply_plan", Scope::IssuesWrite),
1056 // Pull requests.
1057 ("list_pull_requests", Scope::PullRequestsRead),
1058 ("get_pull_request", Scope::PullRequestsRead),
1059 ("get_pull_request_changes", Scope::PullRequestsRead),
1060 ("read_session", Scope::PullRequestsRead),
1061 ("get_merge_queue", Scope::PullRequestsRead),
1062 ("create_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1063 ("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 step1064 ("record_session", Scope::PullRequestsWrite),
1065 ("mark_pull_request_ready", Scope::PullRequestsWrite),
1066 ("close_pull_request", Scope::PullRequestsWrite),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1067 ("reopen_pull_request", Scope::PullRequestsWrite),
1068 ("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 step1069 ("review_pull_request", Scope::PullRequestsWrite),
1070 ("merge_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1071 ("request_reviewers", Scope::PullRequestsWrite),
1072 ("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 step1073 // g1t's agents.
1074 ("assign_issue", Scope::AgentsRun),
1075 ("delegate", Scope::AgentsRun),
1076 ("plan_work", Scope::AgentsRun),
1077 ("message_agent", Scope::AgentsRun),
1078 ("answer_message", Scope::AgentsRun),
1079 ("take_messages", Scope::AgentsRun),
1080 // Workflows.
1081 ("list_workflows", Scope::WorkflowsRead),
1082 ("list_workflow_runs", Scope::WorkflowsRead),
1083 ("get_workflow_run", Scope::WorkflowsRead),
1084 ("get_job_logs", Scope::WorkflowsRead),
1085 ("dispatch_workflow", Scope::WorkflowsWrite),
1086 ("cancel_workflow_run", Scope::WorkflowsWrite),
1087 ("rerun_workflow_run", Scope::WorkflowsWrite),
1088 ("update_workflow", Scope::WorkflowsWrite),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R21089 ("list_artifacts", Scope::WorkflowsRead),
1090 ("list_workflow_run_artifacts", Scope::WorkflowsRead),
1091 ("get_artifact", Scope::WorkflowsRead),
1092 ("download_artifact", Scope::WorkflowsRead),
1093 ("get_artifact_retention", Scope::WorkflowsRead),
1094 ("delete_artifact", Scope::WorkflowsWrite),
1095 ("set_artifact_retention", Scope::WorkflowsWrite),
Merge checks: statuses and check runs on every commit1096 // Checks: statuses, check runs and check suites on commits.
1097 ("list_commit_statuses", Scope::ChecksRead),
1098 ("get_combined_status", Scope::ChecksRead),
1099 ("list_check_runs_for_ref", Scope::ChecksRead),
1100 ("get_check_run", Scope::ChecksRead),
1101 ("list_check_run_annotations", Scope::ChecksRead),
1102 ("list_check_suites_for_ref", Scope::ChecksRead),
1103 ("get_check_suite", Scope::ChecksRead),
1104 ("create_commit_status", Scope::ChecksWrite),
1105 ("create_check_run", Scope::ChecksWrite),
1106 ("update_check_run", Scope::ChecksWrite),
1107 ("rerequest_check_run", Scope::ChecksWrite),
1108 ("rerequest_check_suite", Scope::ChecksWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971109 // Deployments, wherever they run: reading them, and reporting them.
1110 ("list_deployments", Scope::DeploymentsRead),
1111 ("get_deployment", Scope::DeploymentsRead),
1112 ("list_deployment_statuses", Scope::DeploymentsRead),
1113 ("list_environments", Scope::DeploymentsRead),
1114 ("get_environment", Scope::DeploymentsRead),
1115 ("create_deployment", Scope::DeploymentsWrite),
1116 ("create_deployment_status", Scope::DeploymentsWrite),
Merge branch 'worktree-agent-a3abfcce648e87dca'1117 // What keeps runs safe: the runs environments hold and reviewing them,
1118 // approving a pull request's run, and a repository's own rules for
1119 // its environments and tokens, which are an admin's.
1120 ("get_pending_deployments", Scope::WorkflowsRead),
1121 ("review_pending_deployments", Scope::WorkflowsWrite),
1122 ("approve_workflow_run", Scope::WorkflowsWrite),
1123 ("get_workflow_permissions", Scope::RepoRead),
1124 ("get_fork_pr_approval", Scope::RepoRead),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1125 ("get_actions_access", Scope::RepoRead),
Merge branch 'worktree-agent-a3abfcce648e87dca'1126 ("update_environment", Scope::RepoAdmin),
1127 ("delete_environment", Scope::RepoAdmin),
1128 ("set_workflow_permissions", Scope::RepoAdmin),
1129 ("set_fork_pr_approval", Scope::RepoAdmin),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1130 ("set_actions_access", Scope::RepoAdmin),
Merge branch 'worktree-agent-a3abfcce648e87dca'1131 // Starting workflows from outside, as a push would.
1132 ("create_repository_dispatch", Scope::CodeWrite),
1133 // A workspace's policy for its repositories' tokens.
1134 ("get_workspace_workflow_permissions", Scope::WorkspaceRead),
1135 ("set_workspace_workflow_permissions", Scope::WorkspaceAdmin),
API and MCP for a workspace's personal access token rules, members' tokens and approvals1136 // A workspace's rules for personal access tokens, and the members'
1137 // tokens that reach it: who has access.
1138 ("get_token_policy", Scope::WorkspaceRead),
1139 ("set_token_policy", Scope::WorkspaceAdmin),
1140 ("list_member_tokens", Scope::AccessRead),
1141 ("list_token_requests", Scope::AccessRead),
1142 ("review_token_request", Scope::AccessAdmin),
1143 ("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 step1144 // Memory and the context hub.
1145 ("recall", Scope::MemoryRead),
1146 ("search_context", Scope::MemoryRead),
1147 ("get_entity", Scope::MemoryRead),
1148 ("get_context", Scope::MemoryRead),
1149 ("remember", Scope::MemoryWrite),
1150 // Who has access.
1151 ("list_collaborators", Scope::AccessRead),
1152 ("get_collaborator_permission", Scope::AccessRead),
1153 ("list_repo_invitations", Scope::AccessRead),
1154 ("list_outside_collaborators", Scope::AccessRead),
1155 ("add_collaborator", Scope::AccessAdmin),
1156 ("update_collaborator", Scope::AccessAdmin),
1157 ("remove_collaborator", Scope::AccessAdmin),
1158 ("revoke_repo_invitation", Scope::AccessAdmin),
1159 ("set_base_permission", Scope::AccessAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1160 ("set_team_repo", Scope::AccessAdmin),
1161 ("remove_team_repo", Scope::AccessAdmin),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1162 // Deploy keys: each lets a machine reach one repository, so they
1163 // are part of who has access.
1164 ("list_deploy_keys", Scope::AccessRead),
1165 ("get_deploy_key", Scope::AccessRead),
1166 ("create_deploy_key", Scope::AccessAdmin),
1167 ("delete_deploy_key", Scope::AccessAdmin),
Merge branch 'mirroring' into artifacts-mode1168 // Mirroring: a repository's links to other hosts. Reading them is
1169 // reading the repository; syncing writes code; the rest is an admin's.
1170 ("get_mirror", Scope::RepoRead),
1171 ("sync_mirror", Scope::CodeWrite),
1172 ("get_hand_back_plan", Scope::RepoAdmin),
1173 ("take_over_mirror", Scope::RepoAdmin),
1174 ("set_ci_failover", Scope::RepoAdmin),
1175 ("hand_back_mirror", Scope::RepoAdmin),
1176 ("move_mirror_to_g1t", Scope::RepoAdmin),
1177 ("add_mirror_remote", Scope::RepoAdmin),
1178 ("update_mirror_remote", Scope::RepoAdmin),
1179 ("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 step1180 // Webhooks.
1181 ("list_webhooks", Scope::WebhooksRead),
1182 ("list_webhook_deliveries", Scope::WebhooksRead),
1183 ("create_webhook", Scope::WebhooksAdmin),
1184 ("update_webhook", Scope::WebhooksAdmin),
1185 ("delete_webhook", Scope::WebhooksAdmin),
1186 ("ping_webhook", Scope::WebhooksAdmin),
1187 ("redeliver_webhook", Scope::WebhooksAdmin),
1188 // Secrets and variables.
1189 ("list_actions_secrets", Scope::SecretsRead),
1190 ("list_actions_variables", Scope::SecretsRead),
1191 ("set_actions_secret", Scope::SecretsAdmin),
1192 ("delete_actions_secret", Scope::SecretsAdmin),
1193 ("set_actions_variable", Scope::SecretsAdmin),
1194 ("delete_actions_variable", Scope::SecretsAdmin),
Fast pages, required checks on the branch, self-hosted runners, honest incidents1195 // Self-hosted runners.
1196 ("list_runners", Scope::RunnersRead),
1197 ("list_runner_groups", Scope::RunnersRead),
1198 ("get_runner_settings", Scope::RunnersRead),
1199 ("create_runner_registration_token", Scope::RunnersAdmin),
1200 ("remove_runner", Scope::RunnersAdmin),
1201 ("create_runner_group", Scope::RunnersAdmin),
1202 ("update_runner_group", Scope::RunnersAdmin),
1203 ("delete_runner_group", Scope::RunnersAdmin),
1204 ("update_runner_settings", Scope::RunnersAdmin),
Merge packages: roles, Actions access, source label, soft delete, API1205 // Packages: reading them, their versions and who may use them needs
1206 // `packages:read`; changing their settings, access and Manage Actions
1207 // access `packages:write` (and the Admin role on the package, which the
1208 // packages service checks); deleting and restoring packages and
1209 // versions `packages:delete`, as the registries' own deletes do.
1210 ("list_packages", Scope::PackagesRead),
1211 ("get_package", Scope::PackagesRead),
1212 ("list_package_versions", Scope::PackagesRead),
1213 ("get_package_version", Scope::PackagesRead),
1214 ("list_package_access", Scope::PackagesRead),
1215 ("list_package_actions_access", Scope::PackagesRead),
1216 ("update_package", Scope::PackagesWrite),
1217 ("link_package", Scope::PackagesWrite),
1218 ("unlink_package", Scope::PackagesWrite),
1219 ("set_package_access", Scope::PackagesWrite),
1220 ("remove_package_access", Scope::PackagesWrite),
1221 ("set_package_actions_access", Scope::PackagesWrite),
1222 ("remove_package_actions_access", Scope::PackagesWrite),
1223 ("delete_package", Scope::PackagesDelete),
1224 ("restore_package", Scope::PackagesDelete),
1225 ("delete_package_version", Scope::PackagesDelete),
1226 ("restore_package_version", Scope::PackagesDelete),
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1227 // The AI Gateway. Sending a request to a model needs `models:write`,
1228 // checked by the model proxy at models.g1t.sh, not here.
1229 ("list_gateway_requests", Scope::ModelsRead),
Merge main into Artifacts Phase 21230 // Artifacts mode's docs, slides, designs and dashboards (the `artifact`
1231 // MCP tool). Reading takes artifacts:read, making and changing them
1232 // artifacts:write, and sharing them or deleting them for good
1233 // artifacts:admin. The docs service then checks the person's own role
1234 // on each one.
1235 ("list_workspace_artifacts", Scope::ArtifactsRead),
1236 ("search_workspace_artifacts", Scope::ArtifactsRead),
1237 ("get_workspace_artifact", Scope::ArtifactsRead),
1238 ("get_workspace_artifact_content", Scope::ArtifactsRead),
1239 ("list_workspace_artifact_versions", Scope::ArtifactsRead),
1240 ("get_workspace_artifact_access", Scope::ArtifactsRead),
1241 ("list_workspace_artifact_templates", Scope::ArtifactsRead),
1242 ("list_workspace_artifact_spaces", Scope::ArtifactsRead),
1243 ("query_workspace_dataset", Scope::ArtifactsRead),
1244 ("create_workspace_artifact", Scope::ArtifactsWrite),
1245 ("update_workspace_artifact", Scope::ArtifactsWrite),
1246 ("edit_workspace_artifact", Scope::ArtifactsWrite),
1247 ("trash_workspace_artifact", Scope::ArtifactsWrite),
1248 ("restore_workspace_artifact", Scope::ArtifactsWrite),
1249 ("restore_workspace_artifact_version", Scope::ArtifactsWrite),
1250 ("set_workspace_artifact_access", Scope::ArtifactsAdmin),
1251 ("purge_workspace_artifact", Scope::ArtifactsAdmin),
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1252];
1253
1254/// Operations any token may use: saying who it is.
1255pub const NO_SCOPE: &[&str] = &["whoami"];
1256
1257/// The scope `operation` needs. `None` for one in [`NO_SCOPE`]; an
1258/// operation in neither list needs full access.
1259pub fn scope_for(operation: &str) -> Option<Scope> {
1260 OPERATIONS
1261 .iter()
1262 .find(|(name, _)| *name == operation)
1263 .map(|(_, scope)| *scope)
1264}
1265
1266/// What a token needs for `operation` with this input beyond its own
1267/// scope: starting agents from an operation that can, and making a
1268/// repository public or private.
1269pub fn extra_scopes(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
1270 let mut extra = Vec::new();
1271 let assigns = input["assign"].as_bool() == Some(true)
1272 || input["agent"].as_bool() == Some(true)
1273 || input["assign_agent"].as_bool() == Some(true);
1274 if assigns && matches!(operation, "apply_plan" | "import_issue" | "create_issue") {
1275 extra.push(Scope::AgentsRun);
1276 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1277 // Fixing an alert opens an issue and puts g1t on it.
1278 if operation == "fix_security_alert" {
1279 extra.extend([Scope::IssuesWrite, Scope::AgentsRun]);
1280 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1281 // Opening the issue an agent is put on.
1282 if operation == "delegate" {
1283 extra.push(Scope::IssuesWrite);
1284 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1285 // A workspace's base permission is who has access.
1286 if operation == "update_workspace" && input.get("base_permission").is_some_and(|v| !v.is_null()) {
1287 extra.push(Scope::AccessAdmin);
1288 }
Merge checks: statuses and check runs on every commit1289 // Asking a g1t Actions job or run to run again reruns its workflow.
1290 if matches!(operation, "rerequest_check_run" | "rerequest_check_suite")
1291 && input["id"].as_str().is_some_and(|id| id.starts_with("job_") || id.starts_with("run_"))
1292 {
1293 extra.push(Scope::WorkflowsWrite);
1294 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1295 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())) {
1296 extra.push(Scope::RepoAdmin);
1297 }
1298 extra
1299}
1300
1301/// The scopes a call needs, its own first.
1302pub fn needed(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
1303 scope_for(operation)
1304 .into_iter()
1305 .chain(extra_scopes(operation, input))
1306 .collect()
1307}
1308
1309/// Whether `access` may use `operation` with `input`. The person's (or
1310/// workspace's) role is checked after this, by the service that owns what
1311/// was asked about.
1312pub fn decide(access: &TokenAccess, operation: &str, input: &serde_json::Value) -> Decision {
1313 let rule = if access.legacy { "token:legacy" } else { "token:scope" };
Merge branch 'worktree-agent-a3abfcce648e87dca'1314 // A workflow job may open or approve pull requests only where its
1315 // repository and workspace let it, as on GitHub.
1316 if let Some(job) = &access.job
1317 && !job.pull_requests
1318 && (operation == "create_pull_request" || (operation == "review_pull_request" && input["verdict"].as_str() == Some("approve")))
1319 {
1320 return Decision::deny(
1321 "token:pull-requests",
1322 "A workflow job cannot open or approve pull requests here: an admin can allow it under Settings, Actions.",
1323 );
1324 }
1325 if let Some(only) = access.repo.as_deref()
1326 && !NO_SCOPE.contains(&operation)
1327 {
1328 match input["repo"].as_str() {
1329 Some(repo) if access.reaches(repo) => {}
1330 Some(repo) => {
1331 return Decision::deny("token:repository", format!("This token is a workflow job's in {only}: it cannot reach {repo}."));
1332 }
1333 None => {
1334 return Decision::deny("token:repository", format!("This token is a workflow job's: it reaches only {only}, and {operation} is not about one repository."));
1335 }
1336 }
1337 }
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1338 // A token made for one workspace (or none) only reads outside it:
1339 // public repositories, as anyone may. Inside it, its repository
1340 // selection is checked with its owner's role (`access::granted`).
1341 if let Some(reach) = &access.reach
Token reach: workflow_files scope, fine-grained reach, workspace token cap1342 && let Some(repo) = input["repo"].as_str()
1343 && !NO_SCOPE.contains(&operation)
1344 {
1345 let namespace = repo.split('/').next().unwrap_or_default();
1346 let changes = needed(operation, input).iter().any(|scope| scope.level() != Level::Read);
1347 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 managers1348 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 cap1349 return Decision::deny(
1350 "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 managers1351 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 cap1352 );
1353 }
1354 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1355 if access.scopes.is_some() {
1356 let known = NO_SCOPE.contains(&operation) || scope_for(operation).is_some();
1357 if !known {
1358 return Decision::deny("token:scope", format!("This access token cannot use {operation}: it needs full access."));
1359 }
1360 if let Some(missing) = needed(operation, input).into_iter().find(|scope| !access.allows(*scope)) {
1361 return Decision::deny(
1362 "token:scope",
1363 format!("This access token needs the {} scope to use {operation}.", missing.as_str()),
1364 );
1365 }
1366 }
1367 Decision::allow(rule)
1368}
1369
Merge branch 'worktree-agent-a3abfcce648e87dca'1370/// Whether a token may use the repository `owner/name` at all: a refusal
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1371/// for a workflow job's token or a deploy key in another repository,
1372/// else `None`. Git and
Merge branch 'worktree-agent-a3abfcce648e87dca'1373/// the package registries ask this before [`decide_git`] and
1374/// [`decide_packages`].
1375pub fn decide_repo(access: &TokenAccess, repo: &str) -> Option<Decision> {
1376 let only = access.repo.as_deref()?;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1377 let why = if access.deploy_key.is_some() {
1378 format!("This deploy key is for {only}: it cannot reach {repo}.")
1379 } else {
1380 format!("This token is a workflow job's in {only}: it cannot reach {repo}.")
1381 };
1382 (!access.reaches(repo)).then(|| Decision::deny("token:repository", why))
Merge branch 'worktree-agent-a3abfcce648e87dca'1383}
1384
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1385/// Whether a token may clone or fetch (`write` false), or push to (`write`
1386/// true), a repository with git. `public` is whether anyone may read it,
1387/// which needs no scope.
1388pub fn decide_git(access: &TokenAccess, write: bool, public: bool) -> Decision {
1389 let needed = if write { Scope::CodeWrite } else { Scope::CodeRead };
1390 if !access.allows(needed) && (write || !public) {
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1391 if access.deploy_key.is_some() {
1392 return Decision::deny(
1393 "token:scope",
1394 "This deploy key is read-only. An admin of the repository can add it again with write access to push with it.",
1395 );
1396 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1397 return Decision::deny(
1398 "token:scope",
1399 format!("This access token needs the {} scope to {} with git.", needed.as_str(), if write { "push" } else { "clone or fetch a private repository" }),
1400 );
1401 }
1402 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
1403}
1404
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1405/// Whether a token may pull (`Level::Read`), push or publish
1406/// (`Level::Write`), or delete (`Level::Delete`) packages. `public` is
1407/// whether anyone may pull the package, which needs no scope.
1408pub fn decide_packages(access: &TokenAccess, level: Level, public: bool) -> Decision {
1409 let (needed, doing) = match level {
1410 Level::Read => (Scope::PackagesRead, "pull a private package"),
1411 Level::Delete | Level::Admin => (Scope::PackagesDelete, "delete packages"),
1412 Level::Write | Level::Run => (Scope::PackagesWrite, "push or publish packages"),
1413 };
1414 if !access.allows(needed) && !(level == Level::Read && public) {
1415 return Decision::deny(
1416 "token:scope",
1417 format!("This access token needs the {} scope to {doing}.", needed.as_str()),
1418 );
1419 }
1420 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
1421}
1422
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1423#[cfg(test)]
1424mod tests {
1425 use super::*;
1426 use serde_json::json;
1427
1428 fn token(scopes: &[Scope]) -> TokenAccess {
1429 TokenAccess {
1430 token_id: "tok_1".to_owned(),
1431 scopes: Some(scopes.iter().map(|scope| scope.as_str().to_owned()).collect()),
1432 legacy: false,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1433 name: None,
Merge branch 'worktree-agent-a3abfcce648e87dca'1434 ..TokenAccess::default()
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1435 }
1436 }
1437
1438 #[test]
1439 fn every_scope_reads_back_and_belongs_to_a_resource() {
1440 for scope in Scope::ALL {
1441 assert_eq!(Scope::parse(scope.as_str()), Some(scope));
1442 assert!(scope.as_str().starts_with(scope.resource().as_str()));
1443 assert!(scope.includes(scope));
1444 }
1445 assert_eq!(Scope::parse(" Issues:Write "), Some(Scope::IssuesWrite));
1446 assert_eq!(Scope::parse("issues"), None);
1447 }
1448
1449 #[test]
1450 fn a_higher_level_includes_the_lower_ones_of_its_resource_only() {
1451 assert!(Scope::RepoAdmin.includes(Scope::RepoRead));
1452 assert!(Scope::RepoAdmin.includes(Scope::RepoWrite));
1453 assert!(Scope::IssuesWrite.includes(Scope::IssuesRead));
1454 assert!(!Scope::IssuesRead.includes(Scope::IssuesWrite));
1455 assert!(!Scope::RepoAdmin.includes(Scope::CodeWrite));
1456 assert!(!Scope::PullRequestsWrite.includes(Scope::IssuesWrite));
1457 }
1458
1459 #[test]
1460 fn operations_are_listed_once_and_never_also_free() {
1461 let mut seen = std::collections::HashSet::new();
1462 for (name, _) in OPERATIONS {
1463 assert!(seen.insert(*name), "{name} twice");
1464 assert!(!NO_SCOPE.contains(name), "{name}");
1465 }
1466 }
1467
1468 #[test]
1469 fn scopes_are_parsed_from_oauth_text_leaving_out_unknown_ones() {
1470 assert_eq!(
1471 parse_scopes("issues:write repo:read,bogus:thing issues:write"),
1472 vec![Scope::RepoRead, Scope::IssuesWrite]
1473 );
1474 assert_eq!(scopes_text(&[Scope::RepoRead, Scope::IssuesWrite]), "repo:read issues:write");
1475 }
1476
1477 #[test]
1478 fn the_oauth_default_is_the_agent_preset_and_never_admin() {
1479 let scopes = oauth_default();
1480 assert!(scopes.contains(&Scope::IssuesWrite));
1481 assert!(scopes.contains(&Scope::PullRequestsWrite));
1482 assert!(scopes.contains(&Scope::AgentsRun));
1483 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 yet1484 for read in Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read && scope.offered()) {
1485 // Every read offered but the machines work runs on.
Fast pages, required checks on the branch, self-hosted runners, honest incidents1486 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 step1487 }
1488 assert!(Preset::ReadOnly.scopes().unwrap().iter().all(|scope| scope.level() == Level::Read));
1489 assert_eq!(Preset::Full.scopes(), None);
1490 }
1491
1492 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1493 fn billing_is_read_by_presets_and_changed_by_none_but_full_access() {
1494 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::BillingRead));
1495 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
1496 assert!(!preset.scopes().unwrap().contains(&Scope::BillingWrite), "{}", preset.as_str());
1497 }
1498 assert_eq!(scope_for("set_budget"), Some(Scope::BillingWrite));
1499 assert_eq!(scope_for("buy_ai_credit"), Some(Scope::BillingWrite));
1500 assert_eq!(scope_for("get_usage"), Some(Scope::BillingRead));
1501 let reader = token(&[Scope::BillingRead]);
1502 assert!(decide(&reader, "list_invoices", &json!({})).allowed);
1503 assert!(decide(&reader, "set_budget", &json!({})).reason.unwrap().contains("billing:write"));
1504 }
1505
1506 #[test]
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1507 fn the_ai_gateway_spends_only_with_models_write_which_no_preset_gives() {
1508 // Reading the log is a read like any other.
1509 assert_eq!(scope_for("list_gateway_requests"), Some(Scope::ModelsRead));
1510 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::ModelsRead));
1511 // Sending requests spends the workspace's AI credit: chosen on purpose.
1512 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
1513 assert!(!preset.scopes().unwrap().contains(&Scope::ModelsWrite), "{}", preset.as_str());
1514 }
1515 assert!(Scope::ModelsWrite.includes(Scope::ModelsRead));
1516 assert!(!Scope::ModelsWrite.dangerous());
1517 assert!(token(&[Scope::ModelsWrite]).allows(Scope::ModelsWrite));
1518 assert!(!token(&[Scope::BillingWrite]).allows(Scope::ModelsWrite));
1519 assert!(TokenAccess::full().allows(Scope::ModelsWrite));
1520 }
1521
1522 #[test]
Merge main into Artifacts Phase 21523 fn artifacts_are_offered_read_by_presets_and_shared_only_with_admin() {
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 yet1524 for scope in [Scope::ArtifactsRead, Scope::ArtifactsWrite, Scope::ArtifactsAdmin] {
1525 assert_eq!(scope.resource(), Resource::Artifacts);
Merge main into Artifacts Phase 21526 assert!(scope.offered());
1527 assert!(offered_scopes().contains(&scope));
1528 assert_eq!(parse_scopes(scope.as_str()), vec![scope]);
1529 assert!(OPERATIONS.iter().any(|(_, needed)| *needed == scope), "{scope:?} gates nothing");
1530 }
1531 // Reading them is a read like any other; changing them is chosen.
1532 for preset in [Preset::ReadOnly, Preset::Agent] {
1533 let scopes = preset.scopes().unwrap();
1534 assert!(scopes.contains(&Scope::ArtifactsRead), "{}", preset.as_str());
1535 assert!(!scopes.contains(&Scope::ArtifactsWrite) && !scopes.contains(&Scope::ArtifactsAdmin), "{}", preset.as_str());
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 yet1536 }
Merge main into Artifacts Phase 21537 assert!(!Preset::Ci.scopes().unwrap().contains(&Scope::ArtifactsRead));
1538 assert!(everything().contains(&Scope::ArtifactsAdmin));
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 yet1539 assert!(Scope::ArtifactsAdmin.includes(Scope::ArtifactsWrite));
1540 assert!(Scope::ArtifactsAdmin.dangerous());
Merge main into Artifacts Phase 21541 assert!(!Scope::ArtifactsWrite.dangerous());
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 yet1542 assert_eq!(Resource::Artifacts.group(), ResourceGroup::Workspace);
Merge main into Artifacts Phase 21543 let asked = std::collections::BTreeMap::from([("artifacts".to_owned(), "write".to_owned())]);
1544 assert_eq!(resolve_permissions(&asked, true), Ok(vec![Scope::ArtifactsWrite]));
1545 assert_eq!(offered_scopes().len(), Scope::ALL.len());
1546 // Sharing and deleting for good need admin; editing needs write.
1547 assert_eq!(scope_for("set_workspace_artifact_access"), Some(Scope::ArtifactsAdmin));
1548 assert_eq!(scope_for("purge_workspace_artifact"), Some(Scope::ArtifactsAdmin));
1549 assert_eq!(scope_for("edit_workspace_artifact"), Some(Scope::ArtifactsWrite));
1550 assert_eq!(scope_for("get_workspace_artifact_content"), Some(Scope::ArtifactsRead));
1551 let writer = token(&[Scope::ArtifactsWrite]);
1552 assert!(decide(&writer, "edit_workspace_artifact", &json!({})).allowed);
1553 assert!(decide(&writer, "list_workspace_artifacts", &json!({})).allowed, "write includes read");
1554 assert!(decide(&writer, "set_workspace_artifact_access", &json!({})).reason.unwrap().contains("artifacts:admin"));
1555 // Workflow runs' artifacts are another thing, with their own scope.
1556 let reader = token(&[Scope::ArtifactsRead]);
1557 assert!(!decide(&reader, "list_artifacts", &json!({ "repo": "acme/web" })).allowed);
1558 assert!(!decide(&token(&[Scope::WorkflowsRead]), "list_workspace_artifacts", &json!({})).allowed);
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 yet1559 }
1560
1561 #[test]
Merge checks: statuses and check runs on every commit1562 fn checks_are_reported_with_checks_write_which_ci_gets() {
1563 assert_eq!(scope_for("create_check_run"), Some(Scope::ChecksWrite));
1564 assert_eq!(scope_for("create_commit_status"), Some(Scope::ChecksWrite));
1565 assert_eq!(scope_for("list_check_runs_for_ref"), Some(Scope::ChecksRead));
1566 let ci = Preset::Ci.scopes().unwrap();
1567 assert!(ci.contains(&Scope::ChecksWrite));
1568 assert!(!Preset::Agent.scopes().unwrap().contains(&Scope::ChecksWrite));
1569 let reporter = token(&[Scope::ChecksWrite]);
1570 assert!(decide(&reporter, "update_check_run", &json!({ "id": "cr_1" })).allowed);
1571 assert!(decide(&reporter, "rerequest_check_run", &json!({ "id": "cr_1" })).allowed);
1572 // A g1t Actions job runs again as its workflow does.
1573 let refused = decide(&reporter, "rerequest_check_run", &json!({ "id": "job_1" }));
1574 assert!(refused.reason.unwrap().contains("workflows:write"));
1575 }
1576
1577 #[test]
Merge branch 'worktree-agent-a3abfcce648e87dca'1578 fn a_job_token_reaches_its_repository_only() {
1579 let job = TokenAccess {
1580 repo: Some("acme/web".into()),
1581 job: Some(JobToken { run_id: "run_1".into(), job_id: "job_1".into(), pull_requests: false }),
1582 ..token(&[Scope::RepoRead, Scope::IssuesWrite, Scope::IssuesRead, Scope::PullRequestsWrite])
1583 };
1584 assert!(decide(&job, "create_issue", &json!({ "repo": "acme/web" })).allowed);
1585 assert!(decide(&job, "create_issue", &json!({ "repo": "Acme/Web" })).allowed, "names compare without case");
1586 let elsewhere = decide(&job, "create_issue", &json!({ "repo": "acme/api" }));
1587 assert!(!elsewhere.allowed);
1588 assert_eq!(elsewhere.rule, "token:repository");
1589 // Nothing beyond the one repository, a workspace's listing included.
1590 assert!(!decide(&job, "list_repos", &json!({})).allowed);
1591 assert!(decide(&job, "whoami", &json!({})).allowed);
1592 // Its scopes still hold inside it.
1593 assert!(!decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).allowed);
1594 assert!(decide_repo(&job, "acme/web").is_none());
1595 assert!(!decide_repo(&job, "acme/api").unwrap().allowed);
1596 assert!(decide_repo(&token(&[Scope::CodeRead]), "acme/api").is_none(), "other tokens reach what their owner can");
1597 // Opening and approving pull requests is off unless allowed.
1598 assert_eq!(decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).rule, "token:pull-requests");
1599 assert!(!decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "approve" })).allowed);
1600 assert!(decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "request_changes" })).allowed);
1601 let allowed = TokenAccess { job: Some(JobToken { pull_requests: true, ..job.job.clone().unwrap() }), ..job.clone() };
1602 assert!(decide(&allowed, "create_pull_request", &json!({ "repo": "acme/web" })).allowed);
1603 }
1604
1605 #[test]
Workflow files need workflow_files:write from a token; fine-grained permission table1606 fn workflow_files_need_their_own_scope() {
1607 for path in [".g1t/workflows/ci.yml", ".github/workflows/deploy.yaml", "/.github/workflows/x.yml", ".GitHub/Workflows/ci.yml", ".github/workflows"] {
1608 assert!(is_workflow_file(path), "{path}");
1609 }
1610 for path in ["README.md", ".github/CODEOWNERS", ".github/workflowsx/ci.yml", "docs/.github/workflows/ci.yml", ".g1t/actions/ci.yml"] {
1611 assert!(!is_workflow_file(path), "{path}");
1612 }
1613 let code = token(&[Scope::CodeWrite]);
1614 let refused = decide_workflow_files(Some(&code), ["README.md", ".github/workflows/ci.yml"]).unwrap();
1615 assert_eq!(refused.rule, "token:workflows");
1616 assert!(refused.reason.as_deref().unwrap().contains(".github/workflows/ci.yml"));
1617 assert!(refused.reason.as_deref().unwrap().contains("workflow_files:write"));
1618 assert!(decide_workflow_files(Some(&code), ["README.md"]).is_none());
1619 assert!(decide_workflow_files(Some(&token(&[Scope::CodeWrite, Scope::WorkflowFilesWrite])), [".g1t/workflows/ci.yml"]).is_none());
1620 assert!(decide_workflow_files(Some(&TokenAccess::full()), [".g1t/workflows/ci.yml"]).is_none(), "full access");
1621 assert!(decide_workflow_files(None, [".g1t/workflows/ci.yml"]).is_none(), "a signed-in person");
1622 // A job's token never may, as GITHUB_TOKEN never may.
1623 let job = TokenAccess { job: Some(JobToken::default()), ..TokenAccess::full() };
1624 assert!(decide_workflow_files(Some(&job), [".g1t/workflows/ci.yml"]).unwrap().reason.unwrap().contains("job"));
1625 // Nothing in a preset changes workflow files but full access.
1626 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
1627 assert!(!preset.scopes().unwrap().contains(&Scope::WorkflowFilesWrite), "{}", preset.as_str());
1628 }
1629 assert!(!Scope::WorkflowFilesWrite.includes(Scope::WorkflowsWrite) && !Scope::WorkflowsWrite.includes(Scope::WorkflowFilesWrite));
1630 }
1631
1632 #[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 managers1633 fn a_narrowed_token_only_reads_outside_its_workspace() {
1634 let reach = TokenReach { workspace: Some("acme".into()), repositories: RepositorySelection::All, repo_ids: Vec::new() };
1635 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 table1636 assert!(decide(&fine, "create_issue", &json!({ "repo": "acme/web" })).allowed);
1637 assert!(decide(&fine, "create_issue", &json!({ "repo": "Acme/web" })).allowed);
1638 let elsewhere = decide(&fine, "create_issue", &json!({ "repo": "globex/site" }));
1639 assert_eq!(elsewhere.rule, "token:resource-owner");
1640 assert!(elsewhere.reason.unwrap().contains("acme"));
1641 assert!(decide(&fine, "get_issue", &json!({ "repo": "globex/site" })).allowed, "public repositories elsewhere read");
1642 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 managers1643 let mine = TokenAccess { reach: Some(TokenReach::default()), ..token(&[Scope::IssuesWrite]) };
Workflow files need workflow_files:write from a token; fine-grained permission table1644 assert!(decide(&mine, "create_issue", &json!({ "repo": "acme/web" })).reason.unwrap().contains("your account"));
1645 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 managers1646 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 table1647 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 managers1648 let public = TokenReach { repositories: RepositorySelection::Public, ..selected.clone() };
Workflow files need workflow_files:write from a token; fine-grained permission table1649 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 managers1650 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 table1651 assert_eq!(RepositorySelection::parse("public_only"), Some(RepositorySelection::Public));
1652 }
1653
1654 #[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 managers1655 fn permissions_are_scopes_read_per_resource() {
1656 let asked: std::collections::BTreeMap<String, String> =
1657 [("issues", "write"), ("repo", "read"), ("code", "none"), ("packages", "delete")].iter().map(|(a, b)| ((*a).to_owned(), (*b).to_owned())).collect();
1658 let scopes = resolve_permissions(&asked, true).unwrap();
1659 assert_eq!(scopes, vec![Scope::RepoRead, Scope::PackagesDelete, Scope::IssuesWrite]);
1660 let back = permissions_of(&scopes);
1661 assert_eq!(back.get("issues").map(String::as_str), Some("write"));
1662 assert_eq!(back.get("packages").map(String::as_str), Some("delete"));
1663 assert!(!back.contains_key("code"));
1664 // Lower levels held beside a higher one say nothing more.
1665 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 yet1666 // 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 managers1667 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 yet1668 assert_eq!(all.len(), Resource::ALL.into_iter().filter(|resource| resource.offered()).count());
1669 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 managers1670 assert!(all.iter().any(|held| held.includes(scope)), "{scope:?}");
1671 }
1672 }
1673
1674 #[test]
1675 fn permissions_are_checked_by_name_level_and_owner() {
1676 let one = |name: &str, level: &str| -> std::collections::BTreeMap<String, String> { [(name.to_owned(), level.to_owned())].into() };
1677 assert!(resolve_permissions(&one("wiki", "read"), true).unwrap_err().contains("wiki"));
1678 assert!(resolve_permissions(&one("issues", "admin"), true).unwrap_err().contains("read, write"));
1679 assert!(resolve_permissions(&one("workflow_files", "read"), true).is_err(), "workflow files are written only");
1680 assert!(resolve_permissions(&one("notifications", "read"), false).unwrap_err().contains("account"));
1681 assert_eq!(resolve_permissions(&one("notifications", "read"), true).unwrap(), vec![Scope::NotificationsRead]);
1682 assert_eq!(resolve_permissions(&one("agents", "run"), false).unwrap(), vec![Scope::AgentsRun]);
1683 for resource in Resource::ALL {
1684 assert_eq!(Resource::parse(resource.as_str()), Some(resource));
1685 assert!(!resource.scopes().is_empty());
1686 }
1687 }
1688
1689 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1690 fn a_legacy_token_can_do_everything() {
1691 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
1692 for (operation, _) in OPERATIONS {
1693 assert!(decide(&legacy, operation, &json!({})).allowed, "{operation}");
1694 }
1695 assert_eq!(decide(&legacy, "delete_repo", &json!({})).rule, "token:legacy");
1696 }
1697
1698 #[test]
1699 fn a_missing_scope_is_named() {
1700 let read = token(&[Scope::IssuesRead]);
1701 assert!(decide(&read, "get_issue", &json!({})).allowed);
1702 assert!(decide(&read, "whoami", &json!({})).allowed);
1703 let refused = decide(&read, "create_issue", &json!({}));
1704 assert!(!refused.allowed);
1705 assert_eq!(refused.reason.as_deref(), Some("This access token needs the issues:write scope to use create_issue."));
1706 // An operation the table does not know needs full access.
1707 assert!(!decide(&read, "something_new", &json!({})).allowed);
1708 }
1709
1710 #[test]
1711 fn starting_agents_from_another_operation_needs_agents_run() {
1712 let writer = token(&[Scope::IssuesWrite]);
1713 assert!(decide(&writer, "apply_plan", &json!({})).allowed);
1714 let refused = decide(&writer, "apply_plan", &json!({ "assign": true }));
1715 assert!(refused.reason.unwrap().contains("agents:run"));
1716 let maintainer = token(&[Scope::RepoWrite]);
1717 assert!(decide(&maintainer, "update_repo", &json!({ "description": "x" })).allowed);
1718 assert!(!decide(&maintainer, "update_repo", &json!({ "private": true })).allowed);
1719 }
1720
1721 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1722 fn a_workspaces_base_permission_needs_access_admin_too() {
1723 let admin = token(&[Scope::WorkspaceAdmin]);
1724 assert!(decide(&admin, "update_workspace", &json!({ "name": "Acme" })).allowed);
1725 let refused = decide(&admin, "update_workspace", &json!({ "name": "Acme", "base_permission": "read" }));
1726 assert!(refused.reason.unwrap().contains("access:admin"));
1727 let both = token(&[Scope::WorkspaceAdmin, Scope::AccessAdmin]);
1728 assert!(decide(&both, "update_workspace", &json!({ "base_permission": "read" })).allowed);
1729 assert!(!decide(&token(&[Scope::WorkspaceRead]), "update_workspace", &json!({ "name": "Acme" })).allowed);
1730 }
1731
1732 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1733 fn delegating_needs_both_agents_and_issues() {
1734 let agents = token(&[Scope::AgentsRun]);
1735 assert!(decide(&agents, "delegate", &json!({})).reason.unwrap().contains("issues:write"));
1736 let both = token(&[Scope::AgentsRun, Scope::IssuesWrite]);
1737 assert!(decide(&both, "delegate", &json!({})).allowed);
1738 }
1739
1740 #[test]
1741 fn git_push_needs_code_write_and_private_reads_need_code_read() {
1742 let reader = token(&[Scope::CodeRead]);
1743 assert!(decide_git(&reader, false, false).allowed);
1744 let refused = decide_git(&reader, true, false);
1745 assert!(!refused.allowed);
1746 assert!(refused.reason.unwrap().contains("code:write"));
1747 let issues = token(&[Scope::IssuesWrite]);
1748 assert!(!decide_git(&issues, false, false).allowed);
1749 assert!(decide_git(&issues, false, true).allowed, "public code needs no scope");
1750 assert!(!decide_git(&issues, true, true).allowed, "pushing to public code still needs code:write");
1751 let writer = token(&[Scope::CodeWrite]);
1752 assert!(decide_git(&writer, true, false).allowed);
1753 assert!(decide_git(&writer, false, false).allowed, "code:write includes code:read");
1754 assert!(decide_git(&TokenAccess::full(), true, false).allowed);
1755 }
1756
1757 #[test]
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member1758 fn packages_need_their_own_scopes_and_public_pulls_none() {
1759 let reader = token(&[Scope::PackagesRead]);
1760 assert!(decide_packages(&reader, Level::Read, false).allowed);
1761 assert!(!decide_packages(&reader, Level::Write, false).allowed);
1762 let code = token(&[Scope::CodeWrite]);
1763 assert!(!decide_packages(&code, Level::Read, false).allowed, "code scopes are not package scopes");
1764 assert!(decide_packages(&code, Level::Read, true).allowed, "public packages pull with any token");
1765 let writer = token(&[Scope::PackagesWrite]);
1766 assert!(decide_packages(&writer, Level::Write, false).allowed);
1767 assert!(decide_packages(&writer, Level::Read, false).allowed, "packages:write includes packages:read");
1768 let refused = decide_packages(&writer, Level::Delete, false);
1769 assert!(refused.reason.unwrap().contains("packages:delete"));
1770 assert!(decide_packages(&token(&[Scope::PackagesDelete]), Level::Write, false).allowed);
1771 assert!(Scope::PackagesDelete.dangerous());
1772 // Tokens made before these scopes, and full-access ones, keep working.
1773 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
1774 assert!(decide_packages(&legacy, Level::Delete, false).allowed);
1775 assert!(decide_packages(&TokenAccess::full(), Level::Write, false).allowed);
1776 }
1777
1778 #[test]
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1779 fn token_access_travels_as_json() {
1780 let access = token(&[Scope::IssuesRead]);
1781 let wire = serde_json::to_value(&access).unwrap();
1782 assert_eq!(wire["scopes"], json!(["issues:read"]));
1783 assert!(wire.get("resources").is_none());
1784 let back: TokenAccess = serde_json::from_value(wire).unwrap();
1785 assert_eq!(back, access);
1786 let full: TokenAccess = serde_json::from_value(json!({})).unwrap();
1787 assert!(full.is_full());
1788 // A reach written by an older version is ignored: a token reaches
1789 // whatever its owner can.
1790 let older: TokenAccess = serde_json::from_value(json!({
1791 "token_id": "tok_1",
1792 "scopes": ["issues:read"],
1793 "resources": { "kind": "repositories", "repositories": ["acme/rocket"] },
1794 }))
1795 .unwrap();
1796 assert_eq!(older, access);
Merge main into Artifacts Phase 21797 // Using the website is off unless set, and said only when on.
1798 assert!(!access.website);
1799 assert!(wire_of(&access).get("website").is_none());
1800 let website = TokenAccess { website: true, ..access };
1801 assert_eq!(wire_of(&website)["website"], json!(true));
1802 // Never part of full access.
1803 assert!(!TokenAccess::full().website);
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1804 }
1805
Merge main into Artifacts Phase 21806 fn wire_of(access: &TokenAccess) -> serde_json::Value {
1807 serde_json::to_value(access).unwrap()
1808 }
1809
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1810 /// The site's copy of the table, `packages/contracts/src/scopes.ts`,
1811 /// lists the same scopes in the same order, the same operations with
1812 /// the same scopes, and the same presets.
1813 #[test]
1814 fn the_typescript_mirror_has_the_same_table() {
1815 let ts = include_str!("../../../packages/contracts/src/scopes.ts");
1816 let section = |start: &str| {
1817 ts.split_once(start)
1818 .and_then(|(_, rest)| rest.split_once("] as const"))
1819 .map(|(table, _)| table)
1820 .unwrap_or_else(|| panic!("{start} in scopes.ts"))
1821 };
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 yet1822 let names = |table: &str| -> Vec<String> {
1823 section(table)
1824 .lines()
1825 .filter_map(|line| line.split_once("scope: \"").and_then(|(_, rest)| rest.split_once('"')).map(|(scope, _)| scope.to_owned()))
1826 .collect()
1827 };
1828 // Offered scopes in `SCOPES`, the rest in `UPCOMING_SCOPES`.
1829 let offered: Vec<String> = Scope::ALL.iter().filter(|scope| scope.offered()).map(|scope| scope.as_str().to_owned()).collect();
1830 let upcoming: Vec<String> = Scope::ALL.iter().filter(|scope| !scope.offered()).map(|scope| scope.as_str().to_owned()).collect();
1831 assert_eq!(names("export const SCOPES = ["), offered);
Merge main into Artifacts Phase 21832 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 step1833 let operations: Vec<(String, String)> = section("export const OPERATION_SCOPES = [")
1834 .lines()
1835 .filter_map(|line| {
1836 let mut quoted = line.split('"').skip(1).step_by(2);
1837 Some((quoted.next()?.to_owned(), quoted.next()?.to_owned()))
1838 })
1839 .collect();
1840 let expected: Vec<(String, String)> = OPERATIONS
1841 .iter()
1842 .map(|(name, scope)| ((*name).to_owned(), scope.as_str().to_owned()))
1843 .collect();
1844 assert_eq!(operations, expected);
1845 for preset in Preset::ALL {
1846 let list = section(&format!("{}: [", preset.as_str()));
1847 let mirrored: Vec<&str> = list
1848 .split(',')
1849 .map(|item| item.trim().trim_matches('"'))
1850 .filter(|item| !item.is_empty())
1851 .collect();
1852 let expected: Vec<&str> = preset
1853 .scopes()
1854 .map(|scopes| scopes.iter().map(|scope| scope.as_str()).collect())
1855 .unwrap_or_else(|| vec!["*"]);
1856 assert_eq!(mirrored, expected, "{}", preset.as_str());
1857 }
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 yet1858 // Each resource with its group, in the same order: offered ones in
1859 // `SCOPE_RESOURCES`, the rest in `UPCOMING_RESOURCES`.
1860 for (table, offered) in [("export const SCOPE_RESOURCES", true), ("export const UPCOMING_RESOURCES", false)] {
1861 let resources = ts
1862 .split_once(table)
1863 .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 managers1864];"))
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 yet1865 .map(|(table, _)| table)
1866 .unwrap_or_else(|| panic!("{table} in scopes.ts"));
1867 let rows: Vec<&str> = resources.lines().filter(|line| line.trim_start().starts_with("{ resource:")).collect();
1868 let expected: Vec<Resource> = Resource::ALL.into_iter().filter(|resource| resource.offered() == offered).collect();
1869 assert_eq!(rows.len(), expected.len(), "{table}");
1870 for (row, resource) in rows.iter().zip(expected) {
1871 assert!(row.contains(&format!("resource: \"{}\"", resource.as_str())), "{row}");
1872 assert!(row.contains(&format!("group: \"{}\"", resource.group().as_str())), "{row}");
1873 }
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1874 }
Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step1875 }
1876}

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