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 step | 1 | //! 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 | ||
| 18 | use serde::{Deserialize, Serialize}; | |
| 19 | ||
| 20 | use crate::credentials::Decision; | |
| 21 | ||
| 22 | /// Something a token can be given access to. | |
| 23 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | |
| 24 | pub enum Resource { | |
| 25 | Account, | |
| API: notifications over REST and MCP, with notifications scopes | 26 | Notifications, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 27 | Workspace, |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 28 | Billing, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 29 | Repo, |
| 30 | Code, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 31 | Security, |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 32 | Packages, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 33 | Issues, |
| 34 | PullRequests, | |
| 35 | Agents, | |
| 36 | Workflows, | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 37 | WorkflowFiles, |
| Merge checks: statuses and check runs on every commit | 38 | Checks, |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 39 | Deployments, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 40 | Memory, |
| 41 | Access, | |
| 42 | Webhooks, | |
| 43 | Secrets, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 44 | Runners, |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 45 | 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 yet | 46 | /// Artifacts mode's docs, slides, designs and dashboards (folios in |
| Merge main into Artifacts Phase 2 | 47 | /// 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 yet | 49 | Artifacts, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 50 | } |
| 51 | ||
| 52 | impl 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 yet | 53 | 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 step | 54 | Resource::Repo, |
| 55 | Resource::Code, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 56 | Resource::Security, |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 57 | Resource::Packages, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 58 | Resource::Issues, |
| 59 | Resource::PullRequests, | |
| 60 | Resource::Agents, | |
| 61 | Resource::Workflows, | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 62 | Resource::WorkflowFiles, |
| Merge checks: statuses and check runs on every commit | 63 | Resource::Checks, |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 64 | Resource::Deployments, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 65 | Resource::Memory, |
| 66 | Resource::Account, | |
| API: notifications over REST and MCP, with notifications scopes | 67 | Resource::Notifications, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 68 | Resource::Workspace, |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 69 | Resource::Billing, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 70 | Resource::Access, |
| 71 | Resource::Webhooks, | |
| 72 | Resource::Secrets, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 73 | Resource::Runners, |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 74 | 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 yet | 75 | Resource::Artifacts, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 76 | ]; |
| 77 | ||
| 78 | pub fn as_str(self) -> &'static str { | |
| 79 | match self { | |
| 80 | Resource::Account => "account", | |
| API: notifications over REST and MCP, with notifications scopes | 81 | Resource::Notifications => "notifications", |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 82 | Resource::Workspace => "workspace", |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 83 | Resource::Billing => "billing", |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 84 | Resource::Repo => "repo", |
| 85 | Resource::Code => "code", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 86 | Resource::Security => "security", |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 87 | Resource::Packages => "packages", |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 88 | 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 cap | 92 | Resource::WorkflowFiles => "workflow_files", |
| Merge checks: statuses and check runs on every commit | 93 | Resource::Checks => "checks", |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 94 | Resource::Deployments => "deployments", |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 95 | 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 incidents | 99 | Resource::Runners => "runners", |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 100 | 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 yet | 101 | Resource::Artifacts => "artifacts", |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 102 | } |
| 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 scopes | 109 | Resource::Notifications => "Notifications", |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 110 | Resource::Workspace => "Workspaces", |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 111 | Resource::Billing => "Billing", |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 112 | Resource::Repo => "Repositories", |
| 113 | Resource::Code => "Code", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 114 | Resource::Security => "Security", |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 115 | Resource::Packages => "Packages", |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 116 | 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 cap | 120 | Resource::WorkflowFiles => "Workflow files", |
| Merge checks: statuses and check runs on every commit | 121 | Resource::Checks => "Checks and statuses", |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 122 | Resource::Deployments => "Deployments", |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 123 | 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 incidents | 127 | Resource::Runners => "Self-hosted runners", |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 128 | 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 yet | 129 | Resource::Artifacts => "Artifacts", |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 130 | } |
| 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 yet | 132 | |
| Merge main into Artifacts Phase 2 | 133 | /// 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; | |
| The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were. | 138 | /// Artifacts was the last. |
| 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 yet | 139 | pub fn offered(self) -> bool { |
| Merge main into Artifacts Phase 2 | 140 | 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 yet | 142 | } |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 143 | } |
| 144 | ||
| 145 | /// How much of a resource. | |
| 146 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] | |
| 147 | pub 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 member | 152 | /// 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 step | 154 | Admin, |
| 155 | } | |
| 156 | ||
| 157 | impl 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 member | 163 | Level::Delete => "delete", |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 164 | 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)] | |
| 172 | pub 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 bar | 178 | SecurityRead, |
| 179 | SecurityWrite, | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 180 | 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 step | 183 | IssuesRead, |
| 184 | IssuesWrite, | |
| 185 | PullRequestsRead, | |
| 186 | PullRequestsWrite, | |
| 187 | AgentsRun, | |
| 188 | WorkflowsRead, | |
| 189 | WorkflowsWrite, | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 190 | WorkflowFilesWrite, |
| Merge checks: statuses and check runs on every commit | 191 | ChecksRead, |
| 192 | ChecksWrite, | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 193 | DeploymentsRead, |
| 194 | DeploymentsWrite, | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 195 | MemoryRead, |
| 196 | MemoryWrite, | |
| 197 | AccountRead, | |
| 198 | AccountWrite, | |
| API: notifications over REST and MCP, with notifications scopes | 199 | NotificationsRead, |
| 200 | NotificationsWrite, | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 201 | WorkspaceRead, |
| 202 | WorkspaceAdmin, | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 203 | BillingRead, |
| 204 | BillingWrite, | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 205 | AccessRead, |
| 206 | AccessAdmin, | |
| 207 | WebhooksRead, | |
| 208 | WebhooksAdmin, | |
| 209 | SecretsRead, | |
| 210 | SecretsAdmin, | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 211 | RunnersRead, |
| 212 | RunnersAdmin, | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 213 | 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 yet | 215 | 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 step | 218 | } |
| 219 | ||
| 220 | impl 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 yet | 222 | 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 step | 223 | 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 bar | 228 | Scope::SecurityRead, |
| 229 | Scope::SecurityWrite, | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 230 | 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 step | 233 | 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 cap | 240 | Scope::WorkflowFilesWrite, |
| Merge checks: statuses and check runs on every commit | 241 | Scope::ChecksRead, |
| 242 | Scope::ChecksWrite, | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 243 | 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 step | 245 | Scope::MemoryRead, |
| 246 | Scope::MemoryWrite, | |
| 247 | Scope::AccountRead, | |
| 248 | Scope::AccountWrite, | |
| API: notifications over REST and MCP, with notifications scopes | 249 | 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 step | 251 | Scope::WorkspaceRead, |
| 252 | Scope::WorkspaceAdmin, | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 253 | 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 step | 255 | 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 incidents | 261 | Scope::RunnersRead, |
| 262 | Scope::RunnersAdmin, | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 263 | 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 yet | 265 | 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 step | 268 | ]; |
| 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 bar | 277 | 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 member | 279 | 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 step | 282 | 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 cap | 289 | Scope::WorkflowFilesWrite => "workflow_files:write", |
| Merge checks: statuses and check runs on every commit | 290 | Scope::ChecksRead => "checks:read", |
| 291 | Scope::ChecksWrite => "checks:write", | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 292 | 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 step | 294 | 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 scopes | 298 | 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 step | 300 | Scope::WorkspaceRead => "workspace:read", |
| 301 | Scope::WorkspaceAdmin => "workspace:admin", | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 302 | 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 step | 304 | 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 incidents | 310 | Scope::RunnersRead => "runners:read", |
| 311 | Scope::RunnersAdmin => "runners:admin", | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 312 | 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 yet | 314 | 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 step | 317 | } |
| 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 member | 337 | "delete" => Level::Delete, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 338 | "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 member | 352 | 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 step | 353 | } |
| 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-a69aeabc4b0deeb97 | 358 | 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 merge | 360 | 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 step | 361 | 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 bar | 363 | 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 member | 365 | 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, API | 367 | 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 step | 368 | 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 cap | 375 | 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 commit | 376 | 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-a69aeabc4b0deeb97 | 378 | 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 step | 380 | 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-a69aeabc4b0deeb97 | 382 | 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 scopes | 384 | 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 merge | 386 | 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 audit | 388 | 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 step | 390 | Scope::AccessRead => "See who has access to repositories", |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 391 | 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 step | 392 | 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 incidents | 396 | 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 tokens | 398 | 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 yet | 400 | 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 step | 403 | } |
| 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 yet | 405 | |
| 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. | |
| 413 | pub 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 step | 415 | } |
| 416 | ||
| 417 | impl 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 | ||
| 423 | impl<'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. | |
| 433 | pub 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 yet | 437 | .filter(|scope| scope.offered()) |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 438 | .collect(); |
| 439 | normalize(&mut scopes); | |
| 440 | scopes | |
| 441 | } | |
| 442 | ||
| 443 | /// In table order, without repeats. | |
| 444 | pub 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. | |
| 450 | pub 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 managers | 454 | /// 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")] | |
| 457 | pub 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 | ||
| 466 | impl 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 | ||
| 478 | impl 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 yet | 482 | 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 managers | 483 | _ => 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. | |
| 508 | pub 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. | |
| 520 | pub 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 yet | 521 | 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 managers | 522 | } |
| 523 | ||
| 524 | /// Scopes as permissions: each resource held, by name, at its highest | |
| 525 | /// level held. | |
| 526 | pub 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. | |
| 536 | pub 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 yet | 539 | 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 managers | 540 | 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 step | 558 | /// What a token stores for full access, which is not a scope a client can |
| 559 | /// ask for by name. | |
| 560 | pub const FULL_ACCESS: &str = "*"; | |
| 561 | ||
| 562 | /// Starting points for choosing scopes. | |
| 563 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 564 | pub enum Preset { | |
| 565 | ReadOnly, | |
| 566 | Agent, | |
| 567 | Ci, | |
| 568 | Full, | |
| 569 | } | |
| 570 | ||
| 571 | impl 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 yet | 594 | 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 step | 595 | match self { |
| 596 | Preset::ReadOnly => Some(reads().collect()), | |
| 597 | Preset::Agent => { | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 598 | // 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 scopes | 601 | // 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 step | 603 | 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 scopes | 609 | Scope::NotificationsWrite, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 610 | ]); |
| 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 member | 618 | 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 step | 620 | Scope::WorkflowsRead, |
| 621 | Scope::WorkflowsWrite, | |
| Merge checks: statuses and check runs on every commit | 622 | Scope::ChecksRead, |
| 623 | Scope::ChecksWrite, | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 624 | 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 step | 626 | ]), |
| 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. | |
| 634 | pub 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)] | |
| 642 | pub 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 tokens | 664 | /// 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 managers | 668 | /// 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 cap | 677 | /// 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 2 | 686 | /// 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 cap | 692 | } |
| 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 managers | 694 | /// Which repositories a token reaches in the workspace it is made for. |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 695 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
| 696 | #[serde(rename_all = "snake_case")] | |
| 697 | pub 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 managers | 705 | /// With no workspace: the owner's account and public repositories only. |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 706 | Public, |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 707 | } |
| 708 | ||
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 709 | impl 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 managers | 728 | /// What a narrowed token reaches, as identity resolves it on each use. |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 729 | #[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 managers | 730 | pub 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 cap | 734 | #[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 managers | 743 | impl TokenReach { |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 744 | /// 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 managers | 760 | /// Whether it is made for the workspace `slug`. |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 761 | 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. | |
| 769 | pub const WORKFLOW_DIRS: [&str; 2] = [".g1t/workflows/", ".github/workflows/"]; | |
| 770 | ||
| 771 | /// Whether `path` is a workflow file, or a file in one's directory. | |
| 772 | pub 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. | |
| 782 | pub 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)] | |
| 801 | pub 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 step | 813 | impl 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 step | 825 | 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 cap | 842 | |
| 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 managers | 844 | /// 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 cap | 846 | /// 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 managers | 848 | self.reach.as_ref().is_none_or(|reach| reach.covers(repo_id, namespace)) |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 849 | } |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 850 | } |
| 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. | |
| 855 | pub 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 step | 860 | ("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 step | 868 | ("list_my_repo_invitations", Scope::AccountRead), |
| 869 | ("accept_repo_invitation", Scope::AccountWrite), | |
| 870 | ("decline_repo_invitation", Scope::AccountWrite), | |
| API: pinned projects over REST and MCP | 871 | // 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-a69aeabc4b0deeb97 | 874 | // 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 MCP | 879 | ("unpin_project", Scope::AccountWrite), |
| 880 | ("reorder_pinned_projects", Scope::AccountWrite), | |
| API: notifications over REST and MCP, with notifications scopes | 881 | // 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 step | 896 | // 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 daily | 900 | ("update_workspace", Scope::WorkspaceAdmin), |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 901 | // 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 step | 907 | ("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 providers | 912 | ("update_integration", Scope::WorkspaceAdmin), |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 913 | ("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 bar | 917 | // 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 merge | 926 | ("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 bar | 932 | ("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 audit | 937 | // 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 step | 945 | // Repositories. |
| 946 | ("list_repos", Scope::RepoRead), | |
| 947 | ("get_repo", Scope::RepoRead), | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 948 | // 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 step | 951 | ("search", Scope::RepoRead), |
| 952 | ("list_events", Scope::RepoRead), | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 953 | // 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 step | 966 | ("list_labels", Scope::RepoRead), |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 967 | ("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 step | 976 | ("get_repo_settings", Scope::RepoRead), |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 977 | ("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 step | 978 | ("list_deleted_repos", Scope::RepoRead), |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 979 | ("list_security_alerts", Scope::RepoRead), |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 980 | ("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 step | 981 | ("create_repo", Scope::RepoWrite), |
| 982 | ("update_repo", Scope::RepoWrite), | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 983 | ("update_project", Scope::RepoWrite), |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 984 | ("update_repo_settings", Scope::RepoWrite), |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 985 | // 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 step | 994 | ("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 daily | 1003 | // 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 bar | 1006 | // 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 step | 1039 | // 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 bar | 1045 | ("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 step | 1049 | ("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 timeouts | 1052 | ("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 step | 1054 | ("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 bar | 1063 | ("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 step | 1064 | ("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 timeouts | 1067 | ("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 step | 1069 | ("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 bar | 1071 | ("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 step | 1073 | // 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), | |
| Every agent can have its own computer. A session that needs one wakes it: a home of its own on g1t cloud, one per agent and never shared, where it runs commands, reads and writes files and keeps what it made, with each session working in its own folder under a shared home; after ten idle minutes it sleeps, its home kept as a snapshot and restored when it wakes, and Reset wipes the home while memory and artifacts stay. Its shell and files are abilities with the usual choices, Alone, Alone when asked, Ask first or Never, offered only inside sessions and never to a chat reply; every command shows on the session with its output, and the agent's new Computer tab shows the state, the disk used of the five gigabytes included, the recent commands, and Wake, Put to sleep and Reset. Machine time counts only while it is awake, on the sandbox lines of the ledger that name the agent and who asked, held to the same spend caps as the session; the disk itself costs nothing in this version. The runner gained a long-lived supervisor that answers the computer's requests inside the container, and the runner service a computer per agent that keeps its snapshot in the agent homes bucket when one is attached, and says so when none is. The REST API and the agent tool can read a computer, wake it, put it to sleep and reset it. The agents, abilities, sessions, runners, billing and deploy guides say how it works and what an operator sets up; pinning a computer to your own runner, its browser and take-over come next. | 1079 | // A workspace agent's own computer: waking it spends the workspace's |
| 1080 | // money, so the same scope covers reading and managing it. | |
| 1081 | ("get_agent_computer", Scope::AgentsRun), | |
| 1082 | ("wake_agent_computer", Scope::AgentsRun), | |
| 1083 | ("sleep_agent_computer", Scope::AgentsRun), | |
| 1084 | ("reset_agent_computer", Scope::AgentsRun), | |
| 1085 | ("list_agent_computer_commands", Scope::AgentsRun), | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1086 | ("take_messages", Scope::AgentsRun), |
| 1087 | // Workflows. | |
| 1088 | ("list_workflows", Scope::WorkflowsRead), | |
| 1089 | ("list_workflow_runs", Scope::WorkflowsRead), | |
| 1090 | ("get_workflow_run", Scope::WorkflowsRead), | |
| 1091 | ("get_job_logs", Scope::WorkflowsRead), | |
| 1092 | ("dispatch_workflow", Scope::WorkflowsWrite), | |
| 1093 | ("cancel_workflow_run", Scope::WorkflowsWrite), | |
| 1094 | ("rerun_workflow_run", Scope::WorkflowsWrite), | |
| 1095 | ("update_workflow", Scope::WorkflowsWrite), | |
| Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2 | 1096 | ("list_artifacts", Scope::WorkflowsRead), |
| 1097 | ("list_workflow_run_artifacts", Scope::WorkflowsRead), | |
| 1098 | ("get_artifact", Scope::WorkflowsRead), | |
| 1099 | ("download_artifact", Scope::WorkflowsRead), | |
| 1100 | ("get_artifact_retention", Scope::WorkflowsRead), | |
| 1101 | ("delete_artifact", Scope::WorkflowsWrite), | |
| 1102 | ("set_artifact_retention", Scope::WorkflowsWrite), | |
| Merge checks: statuses and check runs on every commit | 1103 | // Checks: statuses, check runs and check suites on commits. |
| 1104 | ("list_commit_statuses", Scope::ChecksRead), | |
| 1105 | ("get_combined_status", Scope::ChecksRead), | |
| 1106 | ("list_check_runs_for_ref", Scope::ChecksRead), | |
| 1107 | ("get_check_run", Scope::ChecksRead), | |
| 1108 | ("list_check_run_annotations", Scope::ChecksRead), | |
| 1109 | ("list_check_suites_for_ref", Scope::ChecksRead), | |
| 1110 | ("get_check_suite", Scope::ChecksRead), | |
| 1111 | ("create_commit_status", Scope::ChecksWrite), | |
| 1112 | ("create_check_run", Scope::ChecksWrite), | |
| 1113 | ("update_check_run", Scope::ChecksWrite), | |
| 1114 | ("rerequest_check_run", Scope::ChecksWrite), | |
| 1115 | ("rerequest_check_suite", Scope::ChecksWrite), | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 1116 | // Deployments, wherever they run: reading them, and reporting them. |
| 1117 | ("list_deployments", Scope::DeploymentsRead), | |
| 1118 | ("get_deployment", Scope::DeploymentsRead), | |
| 1119 | ("list_deployment_statuses", Scope::DeploymentsRead), | |
| 1120 | ("list_environments", Scope::DeploymentsRead), | |
| 1121 | ("get_environment", Scope::DeploymentsRead), | |
| 1122 | ("create_deployment", Scope::DeploymentsWrite), | |
| 1123 | ("create_deployment_status", Scope::DeploymentsWrite), | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1124 | // What keeps runs safe: the runs environments hold and reviewing them, |
| 1125 | // approving a pull request's run, and a repository's own rules for | |
| 1126 | // its environments and tokens, which are an admin's. | |
| 1127 | ("get_pending_deployments", Scope::WorkflowsRead), | |
| 1128 | ("review_pending_deployments", Scope::WorkflowsWrite), | |
| 1129 | ("approve_workflow_run", Scope::WorkflowsWrite), | |
| 1130 | ("get_workflow_permissions", Scope::RepoRead), | |
| 1131 | ("get_fork_pr_approval", Scope::RepoRead), | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 1132 | ("get_actions_access", Scope::RepoRead), |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1133 | ("update_environment", Scope::RepoAdmin), |
| 1134 | ("delete_environment", Scope::RepoAdmin), | |
| 1135 | ("set_workflow_permissions", Scope::RepoAdmin), | |
| 1136 | ("set_fork_pr_approval", Scope::RepoAdmin), | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 1137 | ("set_actions_access", Scope::RepoAdmin), |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1138 | // Starting workflows from outside, as a push would. |
| 1139 | ("create_repository_dispatch", Scope::CodeWrite), | |
| 1140 | // A workspace's policy for its repositories' tokens. | |
| 1141 | ("get_workspace_workflow_permissions", Scope::WorkspaceRead), | |
| 1142 | ("set_workspace_workflow_permissions", Scope::WorkspaceAdmin), | |
| API and MCP for a workspace's personal access token rules, members' tokens and approvals | 1143 | // A workspace's rules for personal access tokens, and the members' |
| 1144 | // tokens that reach it: who has access. | |
| 1145 | ("get_token_policy", Scope::WorkspaceRead), | |
| 1146 | ("set_token_policy", Scope::WorkspaceAdmin), | |
| 1147 | ("list_member_tokens", Scope::AccessRead), | |
| 1148 | ("list_token_requests", Scope::AccessRead), | |
| 1149 | ("review_token_request", Scope::AccessAdmin), | |
| 1150 | ("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 step | 1151 | // Memory and the context hub. |
| 1152 | ("recall", Scope::MemoryRead), | |
| 1153 | ("search_context", Scope::MemoryRead), | |
| 1154 | ("get_entity", Scope::MemoryRead), | |
| 1155 | ("get_context", Scope::MemoryRead), | |
| 1156 | ("remember", Scope::MemoryWrite), | |
| 1157 | // Who has access. | |
| 1158 | ("list_collaborators", Scope::AccessRead), | |
| 1159 | ("get_collaborator_permission", Scope::AccessRead), | |
| 1160 | ("list_repo_invitations", Scope::AccessRead), | |
| 1161 | ("list_outside_collaborators", Scope::AccessRead), | |
| 1162 | ("add_collaborator", Scope::AccessAdmin), | |
| 1163 | ("update_collaborator", Scope::AccessAdmin), | |
| 1164 | ("remove_collaborator", Scope::AccessAdmin), | |
| 1165 | ("revoke_repo_invitation", Scope::AccessAdmin), | |
| 1166 | ("set_base_permission", Scope::AccessAdmin), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1167 | ("set_team_repo", Scope::AccessAdmin), |
| 1168 | ("remove_team_repo", Scope::AccessAdmin), | |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 1169 | // Deploy keys: each lets a machine reach one repository, so they |
| 1170 | // are part of who has access. | |
| 1171 | ("list_deploy_keys", Scope::AccessRead), | |
| 1172 | ("get_deploy_key", Scope::AccessRead), | |
| 1173 | ("create_deploy_key", Scope::AccessAdmin), | |
| 1174 | ("delete_deploy_key", Scope::AccessAdmin), | |
| Merge branch 'mirroring' into artifacts-mode | 1175 | // Mirroring: a repository's links to other hosts. Reading them is |
| 1176 | // reading the repository; syncing writes code; the rest is an admin's. | |
| 1177 | ("get_mirror", Scope::RepoRead), | |
| 1178 | ("sync_mirror", Scope::CodeWrite), | |
| 1179 | ("get_hand_back_plan", Scope::RepoAdmin), | |
| 1180 | ("take_over_mirror", Scope::RepoAdmin), | |
| 1181 | ("set_ci_failover", Scope::RepoAdmin), | |
| 1182 | ("hand_back_mirror", Scope::RepoAdmin), | |
| 1183 | ("move_mirror_to_g1t", Scope::RepoAdmin), | |
| 1184 | ("add_mirror_remote", Scope::RepoAdmin), | |
| 1185 | ("update_mirror_remote", Scope::RepoAdmin), | |
| 1186 | ("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 step | 1187 | // Webhooks. |
| 1188 | ("list_webhooks", Scope::WebhooksRead), | |
| 1189 | ("list_webhook_deliveries", Scope::WebhooksRead), | |
| 1190 | ("create_webhook", Scope::WebhooksAdmin), | |
| 1191 | ("update_webhook", Scope::WebhooksAdmin), | |
| 1192 | ("delete_webhook", Scope::WebhooksAdmin), | |
| 1193 | ("ping_webhook", Scope::WebhooksAdmin), | |
| 1194 | ("redeliver_webhook", Scope::WebhooksAdmin), | |
| 1195 | // Secrets and variables. | |
| 1196 | ("list_actions_secrets", Scope::SecretsRead), | |
| 1197 | ("list_actions_variables", Scope::SecretsRead), | |
| 1198 | ("set_actions_secret", Scope::SecretsAdmin), | |
| 1199 | ("delete_actions_secret", Scope::SecretsAdmin), | |
| 1200 | ("set_actions_variable", Scope::SecretsAdmin), | |
| 1201 | ("delete_actions_variable", Scope::SecretsAdmin), | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 1202 | // Self-hosted runners. |
| 1203 | ("list_runners", Scope::RunnersRead), | |
| 1204 | ("list_runner_groups", Scope::RunnersRead), | |
| 1205 | ("get_runner_settings", Scope::RunnersRead), | |
| 1206 | ("create_runner_registration_token", Scope::RunnersAdmin), | |
| 1207 | ("remove_runner", Scope::RunnersAdmin), | |
| 1208 | ("create_runner_group", Scope::RunnersAdmin), | |
| 1209 | ("update_runner_group", Scope::RunnersAdmin), | |
| 1210 | ("delete_runner_group", Scope::RunnersAdmin), | |
| 1211 | ("update_runner_settings", Scope::RunnersAdmin), | |
| Merge packages: roles, Actions access, source label, soft delete, API | 1212 | // Packages: reading them, their versions and who may use them needs |
| 1213 | // `packages:read`; changing their settings, access and Manage Actions | |
| 1214 | // access `packages:write` (and the Admin role on the package, which the | |
| 1215 | // packages service checks); deleting and restoring packages and | |
| 1216 | // versions `packages:delete`, as the registries' own deletes do. | |
| 1217 | ("list_packages", Scope::PackagesRead), | |
| 1218 | ("get_package", Scope::PackagesRead), | |
| 1219 | ("list_package_versions", Scope::PackagesRead), | |
| 1220 | ("get_package_version", Scope::PackagesRead), | |
| 1221 | ("list_package_access", Scope::PackagesRead), | |
| 1222 | ("list_package_actions_access", Scope::PackagesRead), | |
| 1223 | ("update_package", Scope::PackagesWrite), | |
| 1224 | ("link_package", Scope::PackagesWrite), | |
| 1225 | ("unlink_package", Scope::PackagesWrite), | |
| 1226 | ("set_package_access", Scope::PackagesWrite), | |
| 1227 | ("remove_package_access", Scope::PackagesWrite), | |
| 1228 | ("set_package_actions_access", Scope::PackagesWrite), | |
| 1229 | ("remove_package_actions_access", Scope::PackagesWrite), | |
| 1230 | ("delete_package", Scope::PackagesDelete), | |
| 1231 | ("restore_package", Scope::PackagesDelete), | |
| 1232 | ("delete_package_version", Scope::PackagesDelete), | |
| 1233 | ("restore_package_version", Scope::PackagesDelete), | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 1234 | // The AI Gateway. Sending a request to a model needs `models:write`, |
| 1235 | // checked by the model proxy at models.g1t.sh, not here. | |
| 1236 | ("list_gateway_requests", Scope::ModelsRead), | |
| Merge main into Artifacts Phase 2 | 1237 | // Artifacts mode's docs, slides, designs and dashboards (the `artifact` |
| 1238 | // MCP tool). Reading takes artifacts:read, making and changing them | |
| 1239 | // artifacts:write, and sharing them or deleting them for good | |
| The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook. | 1240 | // artifacts:admin. The artifacts service then checks the person's own role |
| Merge main into Artifacts Phase 2 | 1241 | // on each one. |
| 1242 | ("list_workspace_artifacts", Scope::ArtifactsRead), | |
| 1243 | ("search_workspace_artifacts", Scope::ArtifactsRead), | |
| 1244 | ("get_workspace_artifact", Scope::ArtifactsRead), | |
| 1245 | ("get_workspace_artifact_content", Scope::ArtifactsRead), | |
| 1246 | ("list_workspace_artifact_versions", Scope::ArtifactsRead), | |
| 1247 | ("get_workspace_artifact_access", Scope::ArtifactsRead), | |
| 1248 | ("list_workspace_artifact_templates", Scope::ArtifactsRead), | |
| 1249 | ("list_workspace_artifact_spaces", Scope::ArtifactsRead), | |
| 1250 | ("query_workspace_dataset", Scope::ArtifactsRead), | |
| 1251 | ("create_workspace_artifact", Scope::ArtifactsWrite), | |
| 1252 | ("update_workspace_artifact", Scope::ArtifactsWrite), | |
| 1253 | ("edit_workspace_artifact", Scope::ArtifactsWrite), | |
| 1254 | ("trash_workspace_artifact", Scope::ArtifactsWrite), | |
| 1255 | ("restore_workspace_artifact", Scope::ArtifactsWrite), | |
| 1256 | ("restore_workspace_artifact_version", Scope::ArtifactsWrite), | |
| 1257 | ("set_workspace_artifact_access", Scope::ArtifactsAdmin), | |
| 1258 | ("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 step | 1259 | ]; |
| 1260 | ||
| 1261 | /// Operations any token may use: saying who it is. | |
| 1262 | pub const NO_SCOPE: &[&str] = &["whoami"]; | |
| 1263 | ||
| 1264 | /// The scope `operation` needs. `None` for one in [`NO_SCOPE`]; an | |
| 1265 | /// operation in neither list needs full access. | |
| 1266 | pub fn scope_for(operation: &str) -> Option<Scope> { | |
| 1267 | OPERATIONS | |
| 1268 | .iter() | |
| 1269 | .find(|(name, _)| *name == operation) | |
| 1270 | .map(|(_, scope)| *scope) | |
| 1271 | } | |
| 1272 | ||
| 1273 | /// What a token needs for `operation` with this input beyond its own | |
| 1274 | /// scope: starting agents from an operation that can, and making a | |
| 1275 | /// repository public or private. | |
| 1276 | pub fn extra_scopes(operation: &str, input: &serde_json::Value) -> Vec<Scope> { | |
| 1277 | let mut extra = Vec::new(); | |
| 1278 | let assigns = input["assign"].as_bool() == Some(true) | |
| 1279 | || input["agent"].as_bool() == Some(true) | |
| 1280 | || input["assign_agent"].as_bool() == Some(true); | |
| 1281 | if assigns && matches!(operation, "apply_plan" | "import_issue" | "create_issue") { | |
| 1282 | extra.push(Scope::AgentsRun); | |
| 1283 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1284 | // Fixing an alert opens an issue and puts g1t on it. |
| 1285 | if operation == "fix_security_alert" { | |
| 1286 | extra.extend([Scope::IssuesWrite, Scope::AgentsRun]); | |
| 1287 | } | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1288 | // Opening the issue an agent is put on. |
| 1289 | if operation == "delegate" { | |
| 1290 | extra.push(Scope::IssuesWrite); | |
| 1291 | } | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 1292 | // A workspace's base permission is who has access. |
| 1293 | if operation == "update_workspace" && input.get("base_permission").is_some_and(|v| !v.is_null()) { | |
| 1294 | extra.push(Scope::AccessAdmin); | |
| 1295 | } | |
| Merge checks: statuses and check runs on every commit | 1296 | // Asking a g1t Actions job or run to run again reruns its workflow. |
| 1297 | if matches!(operation, "rerequest_check_run" | "rerequest_check_suite") | |
| 1298 | && input["id"].as_str().is_some_and(|id| id.starts_with("job_") || id.starts_with("run_")) | |
| 1299 | { | |
| 1300 | extra.push(Scope::WorkflowsWrite); | |
| 1301 | } | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1302 | 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())) { |
| 1303 | extra.push(Scope::RepoAdmin); | |
| 1304 | } | |
| 1305 | extra | |
| 1306 | } | |
| 1307 | ||
| 1308 | /// The scopes a call needs, its own first. | |
| 1309 | pub fn needed(operation: &str, input: &serde_json::Value) -> Vec<Scope> { | |
| 1310 | scope_for(operation) | |
| 1311 | .into_iter() | |
| 1312 | .chain(extra_scopes(operation, input)) | |
| 1313 | .collect() | |
| 1314 | } | |
| 1315 | ||
| 1316 | /// Whether `access` may use `operation` with `input`. The person's (or | |
| 1317 | /// workspace's) role is checked after this, by the service that owns what | |
| 1318 | /// was asked about. | |
| 1319 | pub fn decide(access: &TokenAccess, operation: &str, input: &serde_json::Value) -> Decision { | |
| 1320 | let rule = if access.legacy { "token:legacy" } else { "token:scope" }; | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1321 | // A workflow job may open or approve pull requests only where its |
| 1322 | // repository and workspace let it, as on GitHub. | |
| 1323 | if let Some(job) = &access.job | |
| 1324 | && !job.pull_requests | |
| 1325 | && (operation == "create_pull_request" || (operation == "review_pull_request" && input["verdict"].as_str() == Some("approve"))) | |
| 1326 | { | |
| 1327 | return Decision::deny( | |
| 1328 | "token:pull-requests", | |
| 1329 | "A workflow job cannot open or approve pull requests here: an admin can allow it under Settings, Actions.", | |
| 1330 | ); | |
| 1331 | } | |
| 1332 | if let Some(only) = access.repo.as_deref() | |
| 1333 | && !NO_SCOPE.contains(&operation) | |
| 1334 | { | |
| 1335 | match input["repo"].as_str() { | |
| 1336 | Some(repo) if access.reaches(repo) => {} | |
| 1337 | Some(repo) => { | |
| 1338 | return Decision::deny("token:repository", format!("This token is a workflow job's in {only}: it cannot reach {repo}.")); | |
| 1339 | } | |
| 1340 | None => { | |
| 1341 | return Decision::deny("token:repository", format!("This token is a workflow job's: it reaches only {only}, and {operation} is not about one repository.")); | |
| 1342 | } | |
| 1343 | } | |
| 1344 | } | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 1345 | // A token made for one workspace (or none) only reads outside it: |
| 1346 | // public repositories, as anyone may. Inside it, its repository | |
| 1347 | // selection is checked with its owner's role (`access::granted`). | |
| 1348 | if let Some(reach) = &access.reach | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 1349 | && let Some(repo) = input["repo"].as_str() |
| 1350 | && !NO_SCOPE.contains(&operation) | |
| 1351 | { | |
| 1352 | let namespace = repo.split('/').next().unwrap_or_default(); | |
| 1353 | let changes = needed(operation, input).iter().any(|scope| scope.level() != Level::Read); | |
| 1354 | 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 managers | 1355 | 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 cap | 1356 | return Decision::deny( |
| 1357 | "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 managers | 1358 | 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 cap | 1359 | ); |
| 1360 | } | |
| 1361 | } | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1362 | if access.scopes.is_some() { |
| 1363 | let known = NO_SCOPE.contains(&operation) || scope_for(operation).is_some(); | |
| 1364 | if !known { | |
| 1365 | return Decision::deny("token:scope", format!("This access token cannot use {operation}: it needs full access.")); | |
| 1366 | } | |
| 1367 | if let Some(missing) = needed(operation, input).into_iter().find(|scope| !access.allows(*scope)) { | |
| 1368 | return Decision::deny( | |
| 1369 | "token:scope", | |
| 1370 | format!("This access token needs the {} scope to use {operation}.", missing.as_str()), | |
| 1371 | ); | |
| 1372 | } | |
| 1373 | } | |
| 1374 | Decision::allow(rule) | |
| 1375 | } | |
| 1376 | ||
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1377 | /// Whether a token may use the repository `owner/name` at all: a refusal |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 1378 | /// for a workflow job's token or a deploy key in another repository, |
| 1379 | /// else `None`. Git and | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1380 | /// the package registries ask this before [`decide_git`] and |
| 1381 | /// [`decide_packages`]. | |
| 1382 | pub fn decide_repo(access: &TokenAccess, repo: &str) -> Option<Decision> { | |
| 1383 | let only = access.repo.as_deref()?; | |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 1384 | let why = if access.deploy_key.is_some() { |
| 1385 | format!("This deploy key is for {only}: it cannot reach {repo}.") | |
| 1386 | } else { | |
| 1387 | format!("This token is a workflow job's in {only}: it cannot reach {repo}.") | |
| 1388 | }; | |
| 1389 | (!access.reaches(repo)).then(|| Decision::deny("token:repository", why)) | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1390 | } |
| 1391 | ||
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1392 | /// Whether a token may clone or fetch (`write` false), or push to (`write` |
| 1393 | /// true), a repository with git. `public` is whether anyone may read it, | |
| 1394 | /// which needs no scope. | |
| 1395 | pub fn decide_git(access: &TokenAccess, write: bool, public: bool) -> Decision { | |
| 1396 | let needed = if write { Scope::CodeWrite } else { Scope::CodeRead }; | |
| 1397 | if !access.allows(needed) && (write || !public) { | |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 1398 | if access.deploy_key.is_some() { |
| 1399 | return Decision::deny( | |
| 1400 | "token:scope", | |
| 1401 | "This deploy key is read-only. An admin of the repository can add it again with write access to push with it.", | |
| 1402 | ); | |
| 1403 | } | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1404 | return Decision::deny( |
| 1405 | "token:scope", | |
| 1406 | format!("This access token needs the {} scope to {} with git.", needed.as_str(), if write { "push" } else { "clone or fetch a private repository" }), | |
| 1407 | ); | |
| 1408 | } | |
| 1409 | Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" }) | |
| 1410 | } | |
| 1411 | ||
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 1412 | /// Whether a token may pull (`Level::Read`), push or publish |
| 1413 | /// (`Level::Write`), or delete (`Level::Delete`) packages. `public` is | |
| 1414 | /// whether anyone may pull the package, which needs no scope. | |
| 1415 | pub fn decide_packages(access: &TokenAccess, level: Level, public: bool) -> Decision { | |
| 1416 | let (needed, doing) = match level { | |
| 1417 | Level::Read => (Scope::PackagesRead, "pull a private package"), | |
| 1418 | Level::Delete | Level::Admin => (Scope::PackagesDelete, "delete packages"), | |
| 1419 | Level::Write | Level::Run => (Scope::PackagesWrite, "push or publish packages"), | |
| 1420 | }; | |
| 1421 | if !access.allows(needed) && !(level == Level::Read && public) { | |
| 1422 | return Decision::deny( | |
| 1423 | "token:scope", | |
| 1424 | format!("This access token needs the {} scope to {doing}.", needed.as_str()), | |
| 1425 | ); | |
| 1426 | } | |
| 1427 | Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" }) | |
| 1428 | } | |
| 1429 | ||
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1430 | #[cfg(test)] |
| 1431 | mod tests { | |
| 1432 | use super::*; | |
| 1433 | use serde_json::json; | |
| 1434 | ||
| 1435 | fn token(scopes: &[Scope]) -> TokenAccess { | |
| 1436 | TokenAccess { | |
| 1437 | token_id: "tok_1".to_owned(), | |
| 1438 | scopes: Some(scopes.iter().map(|scope| scope.as_str().to_owned()).collect()), | |
| 1439 | legacy: false, | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 1440 | name: None, |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1441 | ..TokenAccess::default() |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1442 | } |
| 1443 | } | |
| 1444 | ||
| 1445 | #[test] | |
| 1446 | fn every_scope_reads_back_and_belongs_to_a_resource() { | |
| 1447 | for scope in Scope::ALL { | |
| 1448 | assert_eq!(Scope::parse(scope.as_str()), Some(scope)); | |
| 1449 | assert!(scope.as_str().starts_with(scope.resource().as_str())); | |
| 1450 | assert!(scope.includes(scope)); | |
| 1451 | } | |
| 1452 | assert_eq!(Scope::parse(" Issues:Write "), Some(Scope::IssuesWrite)); | |
| 1453 | assert_eq!(Scope::parse("issues"), None); | |
| 1454 | } | |
| 1455 | ||
| 1456 | #[test] | |
| 1457 | fn a_higher_level_includes_the_lower_ones_of_its_resource_only() { | |
| 1458 | assert!(Scope::RepoAdmin.includes(Scope::RepoRead)); | |
| 1459 | assert!(Scope::RepoAdmin.includes(Scope::RepoWrite)); | |
| 1460 | assert!(Scope::IssuesWrite.includes(Scope::IssuesRead)); | |
| 1461 | assert!(!Scope::IssuesRead.includes(Scope::IssuesWrite)); | |
| 1462 | assert!(!Scope::RepoAdmin.includes(Scope::CodeWrite)); | |
| 1463 | assert!(!Scope::PullRequestsWrite.includes(Scope::IssuesWrite)); | |
| 1464 | } | |
| 1465 | ||
| 1466 | #[test] | |
| 1467 | fn operations_are_listed_once_and_never_also_free() { | |
| 1468 | let mut seen = std::collections::HashSet::new(); | |
| 1469 | for (name, _) in OPERATIONS { | |
| 1470 | assert!(seen.insert(*name), "{name} twice"); | |
| 1471 | assert!(!NO_SCOPE.contains(name), "{name}"); | |
| 1472 | } | |
| 1473 | } | |
| 1474 | ||
| 1475 | #[test] | |
| 1476 | fn scopes_are_parsed_from_oauth_text_leaving_out_unknown_ones() { | |
| 1477 | assert_eq!( | |
| 1478 | parse_scopes("issues:write repo:read,bogus:thing issues:write"), | |
| 1479 | vec![Scope::RepoRead, Scope::IssuesWrite] | |
| 1480 | ); | |
| 1481 | assert_eq!(scopes_text(&[Scope::RepoRead, Scope::IssuesWrite]), "repo:read issues:write"); | |
| 1482 | } | |
| 1483 | ||
| 1484 | #[test] | |
| 1485 | fn the_oauth_default_is_the_agent_preset_and_never_admin() { | |
| 1486 | let scopes = oauth_default(); | |
| 1487 | assert!(scopes.contains(&Scope::IssuesWrite)); | |
| 1488 | assert!(scopes.contains(&Scope::PullRequestsWrite)); | |
| 1489 | assert!(scopes.contains(&Scope::AgentsRun)); | |
| 1490 | 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 yet | 1491 | for read in Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read && scope.offered()) { |
| 1492 | // Every read offered but the machines work runs on. | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 1493 | 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 step | 1494 | } |
| 1495 | assert!(Preset::ReadOnly.scopes().unwrap().iter().all(|scope| scope.level() == Level::Read)); | |
| 1496 | assert_eq!(Preset::Full.scopes(), None); | |
| 1497 | } | |
| 1498 | ||
| 1499 | #[test] | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 1500 | fn billing_is_read_by_presets_and_changed_by_none_but_full_access() { |
| 1501 | assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::BillingRead)); | |
| 1502 | for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] { | |
| 1503 | assert!(!preset.scopes().unwrap().contains(&Scope::BillingWrite), "{}", preset.as_str()); | |
| 1504 | } | |
| 1505 | assert_eq!(scope_for("set_budget"), Some(Scope::BillingWrite)); | |
| 1506 | assert_eq!(scope_for("buy_ai_credit"), Some(Scope::BillingWrite)); | |
| 1507 | assert_eq!(scope_for("get_usage"), Some(Scope::BillingRead)); | |
| 1508 | let reader = token(&[Scope::BillingRead]); | |
| 1509 | assert!(decide(&reader, "list_invoices", &json!({})).allowed); | |
| 1510 | assert!(decide(&reader, "set_budget", &json!({})).reason.unwrap().contains("billing:write")); | |
| 1511 | } | |
| 1512 | ||
| 1513 | #[test] | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 1514 | fn the_ai_gateway_spends_only_with_models_write_which_no_preset_gives() { |
| 1515 | // Reading the log is a read like any other. | |
| 1516 | assert_eq!(scope_for("list_gateway_requests"), Some(Scope::ModelsRead)); | |
| 1517 | assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::ModelsRead)); | |
| 1518 | // Sending requests spends the workspace's AI credit: chosen on purpose. | |
| 1519 | for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] { | |
| 1520 | assert!(!preset.scopes().unwrap().contains(&Scope::ModelsWrite), "{}", preset.as_str()); | |
| 1521 | } | |
| 1522 | assert!(Scope::ModelsWrite.includes(Scope::ModelsRead)); | |
| 1523 | assert!(!Scope::ModelsWrite.dangerous()); | |
| 1524 | assert!(token(&[Scope::ModelsWrite]).allows(Scope::ModelsWrite)); | |
| 1525 | assert!(!token(&[Scope::BillingWrite]).allows(Scope::ModelsWrite)); | |
| 1526 | assert!(TokenAccess::full().allows(Scope::ModelsWrite)); | |
| 1527 | } | |
| 1528 | ||
| 1529 | #[test] | |
| Merge main into Artifacts Phase 2 | 1530 | 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 yet | 1531 | for scope in [Scope::ArtifactsRead, Scope::ArtifactsWrite, Scope::ArtifactsAdmin] { |
| 1532 | assert_eq!(scope.resource(), Resource::Artifacts); | |
| Merge main into Artifacts Phase 2 | 1533 | assert!(scope.offered()); |
| 1534 | assert!(offered_scopes().contains(&scope)); | |
| 1535 | assert_eq!(parse_scopes(scope.as_str()), vec![scope]); | |
| 1536 | assert!(OPERATIONS.iter().any(|(_, needed)| *needed == scope), "{scope:?} gates nothing"); | |
| 1537 | } | |
| 1538 | // Reading them is a read like any other; changing them is chosen. | |
| 1539 | for preset in [Preset::ReadOnly, Preset::Agent] { | |
| 1540 | let scopes = preset.scopes().unwrap(); | |
| 1541 | assert!(scopes.contains(&Scope::ArtifactsRead), "{}", preset.as_str()); | |
| 1542 | 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 yet | 1543 | } |
| Merge main into Artifacts Phase 2 | 1544 | assert!(!Preset::Ci.scopes().unwrap().contains(&Scope::ArtifactsRead)); |
| 1545 | 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 yet | 1546 | assert!(Scope::ArtifactsAdmin.includes(Scope::ArtifactsWrite)); |
| 1547 | assert!(Scope::ArtifactsAdmin.dangerous()); | |
| Merge main into Artifacts Phase 2 | 1548 | 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 yet | 1549 | assert_eq!(Resource::Artifacts.group(), ResourceGroup::Workspace); |
| Merge main into Artifacts Phase 2 | 1550 | let asked = std::collections::BTreeMap::from([("artifacts".to_owned(), "write".to_owned())]); |
| 1551 | assert_eq!(resolve_permissions(&asked, true), Ok(vec![Scope::ArtifactsWrite])); | |
| 1552 | assert_eq!(offered_scopes().len(), Scope::ALL.len()); | |
| 1553 | // Sharing and deleting for good need admin; editing needs write. | |
| 1554 | assert_eq!(scope_for("set_workspace_artifact_access"), Some(Scope::ArtifactsAdmin)); | |
| 1555 | assert_eq!(scope_for("purge_workspace_artifact"), Some(Scope::ArtifactsAdmin)); | |
| 1556 | assert_eq!(scope_for("edit_workspace_artifact"), Some(Scope::ArtifactsWrite)); | |
| 1557 | assert_eq!(scope_for("get_workspace_artifact_content"), Some(Scope::ArtifactsRead)); | |
| 1558 | let writer = token(&[Scope::ArtifactsWrite]); | |
| 1559 | assert!(decide(&writer, "edit_workspace_artifact", &json!({})).allowed); | |
| 1560 | assert!(decide(&writer, "list_workspace_artifacts", &json!({})).allowed, "write includes read"); | |
| 1561 | assert!(decide(&writer, "set_workspace_artifact_access", &json!({})).reason.unwrap().contains("artifacts:admin")); | |
| 1562 | // Workflow runs' artifacts are another thing, with their own scope. | |
| 1563 | let reader = token(&[Scope::ArtifactsRead]); | |
| 1564 | assert!(!decide(&reader, "list_artifacts", &json!({ "repo": "acme/web" })).allowed); | |
| 1565 | 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 yet | 1566 | } |
| 1567 | ||
| 1568 | #[test] | |
| Merge checks: statuses and check runs on every commit | 1569 | fn checks_are_reported_with_checks_write_which_ci_gets() { |
| 1570 | assert_eq!(scope_for("create_check_run"), Some(Scope::ChecksWrite)); | |
| 1571 | assert_eq!(scope_for("create_commit_status"), Some(Scope::ChecksWrite)); | |
| 1572 | assert_eq!(scope_for("list_check_runs_for_ref"), Some(Scope::ChecksRead)); | |
| 1573 | let ci = Preset::Ci.scopes().unwrap(); | |
| 1574 | assert!(ci.contains(&Scope::ChecksWrite)); | |
| 1575 | assert!(!Preset::Agent.scopes().unwrap().contains(&Scope::ChecksWrite)); | |
| 1576 | let reporter = token(&[Scope::ChecksWrite]); | |
| 1577 | assert!(decide(&reporter, "update_check_run", &json!({ "id": "cr_1" })).allowed); | |
| 1578 | assert!(decide(&reporter, "rerequest_check_run", &json!({ "id": "cr_1" })).allowed); | |
| 1579 | // A g1t Actions job runs again as its workflow does. | |
| 1580 | let refused = decide(&reporter, "rerequest_check_run", &json!({ "id": "job_1" })); | |
| 1581 | assert!(refused.reason.unwrap().contains("workflows:write")); | |
| 1582 | } | |
| 1583 | ||
| 1584 | #[test] | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1585 | fn a_job_token_reaches_its_repository_only() { |
| 1586 | let job = TokenAccess { | |
| 1587 | repo: Some("acme/web".into()), | |
| 1588 | job: Some(JobToken { run_id: "run_1".into(), job_id: "job_1".into(), pull_requests: false }), | |
| 1589 | ..token(&[Scope::RepoRead, Scope::IssuesWrite, Scope::IssuesRead, Scope::PullRequestsWrite]) | |
| 1590 | }; | |
| 1591 | assert!(decide(&job, "create_issue", &json!({ "repo": "acme/web" })).allowed); | |
| 1592 | assert!(decide(&job, "create_issue", &json!({ "repo": "Acme/Web" })).allowed, "names compare without case"); | |
| 1593 | let elsewhere = decide(&job, "create_issue", &json!({ "repo": "acme/api" })); | |
| 1594 | assert!(!elsewhere.allowed); | |
| 1595 | assert_eq!(elsewhere.rule, "token:repository"); | |
| 1596 | // Nothing beyond the one repository, a workspace's listing included. | |
| 1597 | assert!(!decide(&job, "list_repos", &json!({})).allowed); | |
| 1598 | assert!(decide(&job, "whoami", &json!({})).allowed); | |
| 1599 | // Its scopes still hold inside it. | |
| 1600 | assert!(!decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).allowed); | |
| 1601 | assert!(decide_repo(&job, "acme/web").is_none()); | |
| 1602 | assert!(!decide_repo(&job, "acme/api").unwrap().allowed); | |
| 1603 | assert!(decide_repo(&token(&[Scope::CodeRead]), "acme/api").is_none(), "other tokens reach what their owner can"); | |
| 1604 | // Opening and approving pull requests is off unless allowed. | |
| 1605 | assert_eq!(decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).rule, "token:pull-requests"); | |
| 1606 | assert!(!decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "approve" })).allowed); | |
| 1607 | assert!(decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "request_changes" })).allowed); | |
| 1608 | let allowed = TokenAccess { job: Some(JobToken { pull_requests: true, ..job.job.clone().unwrap() }), ..job.clone() }; | |
| 1609 | assert!(decide(&allowed, "create_pull_request", &json!({ "repo": "acme/web" })).allowed); | |
| 1610 | } | |
| 1611 | ||
| 1612 | #[test] | |
| Workflow files need workflow_files:write from a token; fine-grained permission table | 1613 | fn workflow_files_need_their_own_scope() { |
| 1614 | for path in [".g1t/workflows/ci.yml", ".github/workflows/deploy.yaml", "/.github/workflows/x.yml", ".GitHub/Workflows/ci.yml", ".github/workflows"] { | |
| 1615 | assert!(is_workflow_file(path), "{path}"); | |
| 1616 | } | |
| 1617 | for path in ["README.md", ".github/CODEOWNERS", ".github/workflowsx/ci.yml", "docs/.github/workflows/ci.yml", ".g1t/actions/ci.yml"] { | |
| 1618 | assert!(!is_workflow_file(path), "{path}"); | |
| 1619 | } | |
| 1620 | let code = token(&[Scope::CodeWrite]); | |
| 1621 | let refused = decide_workflow_files(Some(&code), ["README.md", ".github/workflows/ci.yml"]).unwrap(); | |
| 1622 | assert_eq!(refused.rule, "token:workflows"); | |
| 1623 | assert!(refused.reason.as_deref().unwrap().contains(".github/workflows/ci.yml")); | |
| 1624 | assert!(refused.reason.as_deref().unwrap().contains("workflow_files:write")); | |
| 1625 | assert!(decide_workflow_files(Some(&code), ["README.md"]).is_none()); | |
| 1626 | assert!(decide_workflow_files(Some(&token(&[Scope::CodeWrite, Scope::WorkflowFilesWrite])), [".g1t/workflows/ci.yml"]).is_none()); | |
| 1627 | assert!(decide_workflow_files(Some(&TokenAccess::full()), [".g1t/workflows/ci.yml"]).is_none(), "full access"); | |
| 1628 | assert!(decide_workflow_files(None, [".g1t/workflows/ci.yml"]).is_none(), "a signed-in person"); | |
| 1629 | // A job's token never may, as GITHUB_TOKEN never may. | |
| 1630 | let job = TokenAccess { job: Some(JobToken::default()), ..TokenAccess::full() }; | |
| 1631 | assert!(decide_workflow_files(Some(&job), [".g1t/workflows/ci.yml"]).unwrap().reason.unwrap().contains("job")); | |
| 1632 | // Nothing in a preset changes workflow files but full access. | |
| 1633 | for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] { | |
| 1634 | assert!(!preset.scopes().unwrap().contains(&Scope::WorkflowFilesWrite), "{}", preset.as_str()); | |
| 1635 | } | |
| 1636 | assert!(!Scope::WorkflowFilesWrite.includes(Scope::WorkflowsWrite) && !Scope::WorkflowsWrite.includes(Scope::WorkflowFilesWrite)); | |
| 1637 | } | |
| 1638 | ||
| 1639 | #[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 managers | 1640 | fn a_narrowed_token_only_reads_outside_its_workspace() { |
| 1641 | let reach = TokenReach { workspace: Some("acme".into()), repositories: RepositorySelection::All, repo_ids: Vec::new() }; | |
| 1642 | 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 table | 1643 | assert!(decide(&fine, "create_issue", &json!({ "repo": "acme/web" })).allowed); |
| 1644 | assert!(decide(&fine, "create_issue", &json!({ "repo": "Acme/web" })).allowed); | |
| 1645 | let elsewhere = decide(&fine, "create_issue", &json!({ "repo": "globex/site" })); | |
| 1646 | assert_eq!(elsewhere.rule, "token:resource-owner"); | |
| 1647 | assert!(elsewhere.reason.unwrap().contains("acme")); | |
| 1648 | assert!(decide(&fine, "get_issue", &json!({ "repo": "globex/site" })).allowed, "public repositories elsewhere read"); | |
| 1649 | 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 managers | 1650 | let mine = TokenAccess { reach: Some(TokenReach::default()), ..token(&[Scope::IssuesWrite]) }; |
| Workflow files need workflow_files:write from a token; fine-grained permission table | 1651 | assert!(decide(&mine, "create_issue", &json!({ "repo": "acme/web" })).reason.unwrap().contains("your account")); |
| 1652 | 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 managers | 1653 | 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 table | 1654 | 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 managers | 1655 | let public = TokenReach { repositories: RepositorySelection::Public, ..selected.clone() }; |
| Workflow files need workflow_files:write from a token; fine-grained permission table | 1656 | 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 managers | 1657 | 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 table | 1658 | assert_eq!(RepositorySelection::parse("public_only"), Some(RepositorySelection::Public)); |
| 1659 | } | |
| 1660 | ||
| 1661 | #[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 managers | 1662 | fn permissions_are_scopes_read_per_resource() { |
| 1663 | let asked: std::collections::BTreeMap<String, String> = | |
| 1664 | [("issues", "write"), ("repo", "read"), ("code", "none"), ("packages", "delete")].iter().map(|(a, b)| ((*a).to_owned(), (*b).to_owned())).collect(); | |
| 1665 | let scopes = resolve_permissions(&asked, true).unwrap(); | |
| 1666 | assert_eq!(scopes, vec![Scope::RepoRead, Scope::PackagesDelete, Scope::IssuesWrite]); | |
| 1667 | let back = permissions_of(&scopes); | |
| 1668 | assert_eq!(back.get("issues").map(String::as_str), Some("write")); | |
| 1669 | assert_eq!(back.get("packages").map(String::as_str), Some("delete")); | |
| 1670 | assert!(!back.contains_key("code")); | |
| 1671 | // Lower levels held beside a higher one say nothing more. | |
| 1672 | 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 yet | 1673 | // 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 managers | 1674 | 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 yet | 1675 | assert_eq!(all.len(), Resource::ALL.into_iter().filter(|resource| resource.offered()).count()); |
| 1676 | 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 managers | 1677 | assert!(all.iter().any(|held| held.includes(scope)), "{scope:?}"); |
| 1678 | } | |
| 1679 | } | |
| 1680 | ||
| 1681 | #[test] | |
| 1682 | fn permissions_are_checked_by_name_level_and_owner() { | |
| 1683 | let one = |name: &str, level: &str| -> std::collections::BTreeMap<String, String> { [(name.to_owned(), level.to_owned())].into() }; | |
| 1684 | assert!(resolve_permissions(&one("wiki", "read"), true).unwrap_err().contains("wiki")); | |
| 1685 | assert!(resolve_permissions(&one("issues", "admin"), true).unwrap_err().contains("read, write")); | |
| 1686 | assert!(resolve_permissions(&one("workflow_files", "read"), true).is_err(), "workflow files are written only"); | |
| 1687 | assert!(resolve_permissions(&one("notifications", "read"), false).unwrap_err().contains("account")); | |
| 1688 | assert_eq!(resolve_permissions(&one("notifications", "read"), true).unwrap(), vec![Scope::NotificationsRead]); | |
| 1689 | assert_eq!(resolve_permissions(&one("agents", "run"), false).unwrap(), vec![Scope::AgentsRun]); | |
| 1690 | for resource in Resource::ALL { | |
| 1691 | assert_eq!(Resource::parse(resource.as_str()), Some(resource)); | |
| 1692 | assert!(!resource.scopes().is_empty()); | |
| 1693 | } | |
| 1694 | } | |
| 1695 | ||
| 1696 | #[test] | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1697 | fn a_legacy_token_can_do_everything() { |
| 1698 | let legacy = TokenAccess { legacy: true, ..TokenAccess::full() }; | |
| 1699 | for (operation, _) in OPERATIONS { | |
| 1700 | assert!(decide(&legacy, operation, &json!({})).allowed, "{operation}"); | |
| 1701 | } | |
| 1702 | assert_eq!(decide(&legacy, "delete_repo", &json!({})).rule, "token:legacy"); | |
| 1703 | } | |
| 1704 | ||
| 1705 | #[test] | |
| 1706 | fn a_missing_scope_is_named() { | |
| 1707 | let read = token(&[Scope::IssuesRead]); | |
| 1708 | assert!(decide(&read, "get_issue", &json!({})).allowed); | |
| 1709 | assert!(decide(&read, "whoami", &json!({})).allowed); | |
| 1710 | let refused = decide(&read, "create_issue", &json!({})); | |
| 1711 | assert!(!refused.allowed); | |
| 1712 | assert_eq!(refused.reason.as_deref(), Some("This access token needs the issues:write scope to use create_issue.")); | |
| 1713 | // An operation the table does not know needs full access. | |
| 1714 | assert!(!decide(&read, "something_new", &json!({})).allowed); | |
| 1715 | } | |
| 1716 | ||
| 1717 | #[test] | |
| 1718 | fn starting_agents_from_another_operation_needs_agents_run() { | |
| 1719 | let writer = token(&[Scope::IssuesWrite]); | |
| 1720 | assert!(decide(&writer, "apply_plan", &json!({})).allowed); | |
| 1721 | let refused = decide(&writer, "apply_plan", &json!({ "assign": true })); | |
| 1722 | assert!(refused.reason.unwrap().contains("agents:run")); | |
| 1723 | let maintainer = token(&[Scope::RepoWrite]); | |
| 1724 | assert!(decide(&maintainer, "update_repo", &json!({ "description": "x" })).allowed); | |
| 1725 | assert!(!decide(&maintainer, "update_repo", &json!({ "private": true })).allowed); | |
| 1726 | } | |
| 1727 | ||
| 1728 | #[test] | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 1729 | fn a_workspaces_base_permission_needs_access_admin_too() { |
| 1730 | let admin = token(&[Scope::WorkspaceAdmin]); | |
| 1731 | assert!(decide(&admin, "update_workspace", &json!({ "name": "Acme" })).allowed); | |
| 1732 | let refused = decide(&admin, "update_workspace", &json!({ "name": "Acme", "base_permission": "read" })); | |
| 1733 | assert!(refused.reason.unwrap().contains("access:admin")); | |
| 1734 | let both = token(&[Scope::WorkspaceAdmin, Scope::AccessAdmin]); | |
| 1735 | assert!(decide(&both, "update_workspace", &json!({ "base_permission": "read" })).allowed); | |
| 1736 | assert!(!decide(&token(&[Scope::WorkspaceRead]), "update_workspace", &json!({ "name": "Acme" })).allowed); | |
| 1737 | } | |
| 1738 | ||
| 1739 | #[test] | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1740 | fn delegating_needs_both_agents_and_issues() { |
| 1741 | let agents = token(&[Scope::AgentsRun]); | |
| 1742 | assert!(decide(&agents, "delegate", &json!({})).reason.unwrap().contains("issues:write")); | |
| 1743 | let both = token(&[Scope::AgentsRun, Scope::IssuesWrite]); | |
| 1744 | assert!(decide(&both, "delegate", &json!({})).allowed); | |
| 1745 | } | |
| 1746 | ||
| 1747 | #[test] | |
| 1748 | fn git_push_needs_code_write_and_private_reads_need_code_read() { | |
| 1749 | let reader = token(&[Scope::CodeRead]); | |
| 1750 | assert!(decide_git(&reader, false, false).allowed); | |
| 1751 | let refused = decide_git(&reader, true, false); | |
| 1752 | assert!(!refused.allowed); | |
| 1753 | assert!(refused.reason.unwrap().contains("code:write")); | |
| 1754 | let issues = token(&[Scope::IssuesWrite]); | |
| 1755 | assert!(!decide_git(&issues, false, false).allowed); | |
| 1756 | assert!(decide_git(&issues, false, true).allowed, "public code needs no scope"); | |
| 1757 | assert!(!decide_git(&issues, true, true).allowed, "pushing to public code still needs code:write"); | |
| 1758 | let writer = token(&[Scope::CodeWrite]); | |
| 1759 | assert!(decide_git(&writer, true, false).allowed); | |
| 1760 | assert!(decide_git(&writer, false, false).allowed, "code:write includes code:read"); | |
| 1761 | assert!(decide_git(&TokenAccess::full(), true, false).allowed); | |
| 1762 | } | |
| 1763 | ||
| 1764 | #[test] | |
| Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member | 1765 | fn packages_need_their_own_scopes_and_public_pulls_none() { |
| 1766 | let reader = token(&[Scope::PackagesRead]); | |
| 1767 | assert!(decide_packages(&reader, Level::Read, false).allowed); | |
| 1768 | assert!(!decide_packages(&reader, Level::Write, false).allowed); | |
| 1769 | let code = token(&[Scope::CodeWrite]); | |
| 1770 | assert!(!decide_packages(&code, Level::Read, false).allowed, "code scopes are not package scopes"); | |
| 1771 | assert!(decide_packages(&code, Level::Read, true).allowed, "public packages pull with any token"); | |
| 1772 | let writer = token(&[Scope::PackagesWrite]); | |
| 1773 | assert!(decide_packages(&writer, Level::Write, false).allowed); | |
| 1774 | assert!(decide_packages(&writer, Level::Read, false).allowed, "packages:write includes packages:read"); | |
| 1775 | let refused = decide_packages(&writer, Level::Delete, false); | |
| 1776 | assert!(refused.reason.unwrap().contains("packages:delete")); | |
| 1777 | assert!(decide_packages(&token(&[Scope::PackagesDelete]), Level::Write, false).allowed); | |
| 1778 | assert!(Scope::PackagesDelete.dangerous()); | |
| 1779 | // Tokens made before these scopes, and full-access ones, keep working. | |
| 1780 | let legacy = TokenAccess { legacy: true, ..TokenAccess::full() }; | |
| 1781 | assert!(decide_packages(&legacy, Level::Delete, false).allowed); | |
| 1782 | assert!(decide_packages(&TokenAccess::full(), Level::Write, false).allowed); | |
| 1783 | } | |
| 1784 | ||
| 1785 | #[test] | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1786 | fn token_access_travels_as_json() { |
| 1787 | let access = token(&[Scope::IssuesRead]); | |
| 1788 | let wire = serde_json::to_value(&access).unwrap(); | |
| 1789 | assert_eq!(wire["scopes"], json!(["issues:read"])); | |
| 1790 | assert!(wire.get("resources").is_none()); | |
| 1791 | let back: TokenAccess = serde_json::from_value(wire).unwrap(); | |
| 1792 | assert_eq!(back, access); | |
| 1793 | let full: TokenAccess = serde_json::from_value(json!({})).unwrap(); | |
| 1794 | assert!(full.is_full()); | |
| 1795 | // A reach written by an older version is ignored: a token reaches | |
| 1796 | // whatever its owner can. | |
| 1797 | let older: TokenAccess = serde_json::from_value(json!({ | |
| 1798 | "token_id": "tok_1", | |
| 1799 | "scopes": ["issues:read"], | |
| 1800 | "resources": { "kind": "repositories", "repositories": ["acme/rocket"] }, | |
| 1801 | })) | |
| 1802 | .unwrap(); | |
| 1803 | assert_eq!(older, access); | |
| Merge main into Artifacts Phase 2 | 1804 | // Using the website is off unless set, and said only when on. |
| 1805 | assert!(!access.website); | |
| 1806 | assert!(wire_of(&access).get("website").is_none()); | |
| 1807 | let website = TokenAccess { website: true, ..access }; | |
| 1808 | assert_eq!(wire_of(&website)["website"], json!(true)); | |
| 1809 | // Never part of full access. | |
| 1810 | assert!(!TokenAccess::full().website); | |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1811 | } |
| 1812 | ||
| Merge main into Artifacts Phase 2 | 1813 | fn wire_of(access: &TokenAccess) -> serde_json::Value { |
| 1814 | serde_json::to_value(access).unwrap() | |
| 1815 | } | |
| 1816 | ||
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1817 | /// The site's copy of the table, `packages/contracts/src/scopes.ts`, |
| 1818 | /// lists the same scopes in the same order, the same operations with | |
| 1819 | /// the same scopes, and the same presets. | |
| 1820 | #[test] | |
| 1821 | fn the_typescript_mirror_has_the_same_table() { | |
| 1822 | let ts = include_str!("../../../packages/contracts/src/scopes.ts"); | |
| 1823 | let section = |start: &str| { | |
| 1824 | ts.split_once(start) | |
| 1825 | .and_then(|(_, rest)| rest.split_once("] as const")) | |
| 1826 | .map(|(table, _)| table) | |
| 1827 | .unwrap_or_else(|| panic!("{start} in scopes.ts")) | |
| 1828 | }; | |
| 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 yet | 1829 | let names = |table: &str| -> Vec<String> { |
| 1830 | section(table) | |
| 1831 | .lines() | |
| 1832 | .filter_map(|line| line.split_once("scope: \"").and_then(|(_, rest)| rest.split_once('"')).map(|(scope, _)| scope.to_owned())) | |
| 1833 | .collect() | |
| 1834 | }; | |
| 1835 | // Offered scopes in `SCOPES`, the rest in `UPCOMING_SCOPES`. | |
| 1836 | let offered: Vec<String> = Scope::ALL.iter().filter(|scope| scope.offered()).map(|scope| scope.as_str().to_owned()).collect(); | |
| 1837 | let upcoming: Vec<String> = Scope::ALL.iter().filter(|scope| !scope.offered()).map(|scope| scope.as_str().to_owned()).collect(); | |
| 1838 | assert_eq!(names("export const SCOPES = ["), offered); | |
| Merge main into Artifacts Phase 2 | 1839 | 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 step | 1840 | let operations: Vec<(String, String)> = section("export const OPERATION_SCOPES = [") |
| 1841 | .lines() | |
| 1842 | .filter_map(|line| { | |
| 1843 | let mut quoted = line.split('"').skip(1).step_by(2); | |
| 1844 | Some((quoted.next()?.to_owned(), quoted.next()?.to_owned())) | |
| 1845 | }) | |
| 1846 | .collect(); | |
| 1847 | let expected: Vec<(String, String)> = OPERATIONS | |
| 1848 | .iter() | |
| 1849 | .map(|(name, scope)| ((*name).to_owned(), scope.as_str().to_owned())) | |
| 1850 | .collect(); | |
| 1851 | assert_eq!(operations, expected); | |
| 1852 | for preset in Preset::ALL { | |
| 1853 | let list = section(&format!("{}: [", preset.as_str())); | |
| 1854 | let mirrored: Vec<&str> = list | |
| 1855 | .split(',') | |
| 1856 | .map(|item| item.trim().trim_matches('"')) | |
| 1857 | .filter(|item| !item.is_empty()) | |
| 1858 | .collect(); | |
| 1859 | let expected: Vec<&str> = preset | |
| 1860 | .scopes() | |
| 1861 | .map(|scopes| scopes.iter().map(|scope| scope.as_str()).collect()) | |
| 1862 | .unwrap_or_else(|| vec!["*"]); | |
| 1863 | assert_eq!(mirrored, expected, "{}", preset.as_str()); | |
| 1864 | } | |
| 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 yet | 1865 | // Each resource with its group, in the same order: offered ones in |
| 1866 | // `SCOPE_RESOURCES`, the rest in `UPCOMING_RESOURCES`. | |
| 1867 | for (table, offered) in [("export const SCOPE_RESOURCES", true), ("export const UPCOMING_RESOURCES", false)] { | |
| 1868 | let resources = ts | |
| 1869 | .split_once(table) | |
| 1870 | .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 managers | 1871 | ];")) |
| 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 yet | 1872 | .map(|(table, _)| table) |
| 1873 | .unwrap_or_else(|| panic!("{table} in scopes.ts")); | |
| 1874 | let rows: Vec<&str> = resources.lines().filter(|line| line.trim_start().starts_with("{ resource:")).collect(); | |
| 1875 | let expected: Vec<Resource> = Resource::ALL.into_iter().filter(|resource| resource.offered() == offered).collect(); | |
| 1876 | assert_eq!(rows.len(), expected.len(), "{table}"); | |
| 1877 | for (row, resource) in rows.iter().zip(expected) { | |
| 1878 | assert!(row.contains(&format!("resource: \"{}\"", resource.as_str())), "{row}"); | |
| 1879 | assert!(row.contains(&format!("group: \"{}\"", resource.group().as_str())), "{row}"); | |
| 1880 | } | |
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 1881 | } |
| Thirteen MCP tools and classic token scopes; agents rate their confidence and can be put on an issue in one step | 1882 | } |
| 1883 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.