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.
| Fixes from the page sweep | 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, |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 27 | /// Chat: the conversations a person is in (channels, direct messages, |
| 28 | /// threads), read and answered as them on the site's chat routes and | |
| 29 | /// live sockets. A person's own; no preset gives it. | |
| 30 | Chat, | |
| Fixes from the page sweep | 31 | Workspace, |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 32 | Billing, |
| Fixes from the page sweep | 33 | Repo, |
| 34 | Code, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 35 | Security, |
| Fixes from the page sweep | 36 | Packages, |
| 37 | Issues, | |
| 38 | PullRequests, | |
| 39 | Agents, | |
| 40 | Workflows, | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 41 | WorkflowFiles, |
| Merge checks: statuses and check runs on every commit | 42 | Checks, |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 43 | Deployments, |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 44 | /// Observability: what a project's running software did, as requests, |
| 45 | /// logs, traces, errors and metrics, and how they are collected. | |
| 46 | Observability, | |
| Fixes from the page sweep | 47 | Memory, |
| 48 | Access, | |
| 49 | Webhooks, | |
| 50 | Secrets, | |
| 51 | Runners, | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 52 | 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 | 53 | /// Artifacts mode's docs, slides, designs and dashboards (folios in |
| Merge main into Artifacts Phase 2 | 54 | /// code): the `artifact` MCP tool and `/workspaces/{ws}/artifacts`. |
| 55 | /// 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 | 56 | Artifacts, |
| Fixes from the page sweep | 57 | } |
| 58 | ||
| 59 | impl Resource { | |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 60 | pub const ALL: [Resource; 24] = [ |
| Fixes from the page sweep | 61 | Resource::Repo, |
| 62 | Resource::Code, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 63 | Resource::Security, |
| Fixes from the page sweep | 64 | Resource::Packages, |
| 65 | Resource::Issues, | |
| 66 | Resource::PullRequests, | |
| 67 | Resource::Agents, | |
| 68 | Resource::Workflows, | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 69 | Resource::WorkflowFiles, |
| Merge checks: statuses and check runs on every commit | 70 | Resource::Checks, |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 71 | Resource::Deployments, |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 72 | Resource::Observability, |
| Fixes from the page sweep | 73 | Resource::Memory, |
| 74 | Resource::Account, | |
| API: notifications over REST and MCP, with notifications scopes | 75 | Resource::Notifications, |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 76 | Resource::Chat, |
| Fixes from the page sweep | 77 | Resource::Workspace, |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 78 | Resource::Billing, |
| Fixes from the page sweep | 79 | Resource::Access, |
| 80 | Resource::Webhooks, | |
| 81 | Resource::Secrets, | |
| 82 | Resource::Runners, | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 83 | 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 | 84 | Resource::Artifacts, |
| Fixes from the page sweep | 85 | ]; |
| 86 | ||
| 87 | pub fn as_str(self) -> &'static str { | |
| 88 | match self { | |
| 89 | Resource::Account => "account", | |
| API: notifications over REST and MCP, with notifications scopes | 90 | Resource::Notifications => "notifications", |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 91 | Resource::Chat => "chat", |
| Fixes from the page sweep | 92 | Resource::Workspace => "workspace", |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 93 | Resource::Billing => "billing", |
| Fixes from the page sweep | 94 | Resource::Repo => "repo", |
| 95 | Resource::Code => "code", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 96 | Resource::Security => "security", |
| Fixes from the page sweep | 97 | Resource::Packages => "packages", |
| 98 | Resource::Issues => "issues", | |
| 99 | Resource::PullRequests => "pull_requests", | |
| 100 | Resource::Agents => "agents", | |
| 101 | Resource::Workflows => "workflows", | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 102 | Resource::WorkflowFiles => "workflow_files", |
| Merge checks: statuses and check runs on every commit | 103 | Resource::Checks => "checks", |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 104 | Resource::Deployments => "deployments", |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 105 | Resource::Observability => "observability", |
| Fixes from the page sweep | 106 | Resource::Memory => "memory", |
| 107 | Resource::Access => "access", | |
| 108 | Resource::Webhooks => "webhooks", | |
| 109 | Resource::Secrets => "secrets", | |
| 110 | Resource::Runners => "runners", | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 111 | 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 | 112 | Resource::Artifacts => "artifacts", |
| Fixes from the page sweep | 113 | } |
| 114 | } | |
| 115 | ||
| 116 | /// Its name, for people. | |
| 117 | pub fn label(self) -> &'static str { | |
| 118 | match self { | |
| 119 | Resource::Account => "Your account", | |
| API: notifications over REST and MCP, with notifications scopes | 120 | Resource::Notifications => "Notifications", |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 121 | Resource::Chat => "Chat", |
| Fixes from the page sweep | 122 | Resource::Workspace => "Workspaces", |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 123 | Resource::Billing => "Billing", |
| Fixes from the page sweep | 124 | Resource::Repo => "Repositories", |
| 125 | Resource::Code => "Code", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 126 | Resource::Security => "Security", |
| Fixes from the page sweep | 127 | Resource::Packages => "Packages", |
| 128 | Resource::Issues => "Issues", | |
| 129 | Resource::PullRequests => "Pull requests", | |
| 130 | Resource::Agents => "g1t agents", | |
| 131 | Resource::Workflows => "Workflows", | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 132 | Resource::WorkflowFiles => "Workflow files", |
| Merge checks: statuses and check runs on every commit | 133 | Resource::Checks => "Checks and statuses", |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 134 | Resource::Deployments => "Deployments", |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 135 | Resource::Observability => "Observability", |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 136 | Resource::Memory => "Memory", |
| Fixes from the page sweep | 137 | Resource::Access => "Who has access", |
| 138 | Resource::Webhooks => "Webhooks", | |
| 139 | Resource::Secrets => "Secrets and variables", | |
| 140 | Resource::Runners => "Self-hosted runners", | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 141 | 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 | 142 | Resource::Artifacts => "Artifacts", |
| Fixes from the page sweep | 143 | } |
| 144 | } | |
| 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 | 145 | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 146 | /// Whether tokens are offered it. A resource being built can be in |
| Merge main into Artifacts Phase 2 | 147 | /// the table before its API ships (so its scopes parse, and the |
| 148 | /// TypeScript mirror lists it under `UPCOMING_RESOURCES`) while nothing | |
| 149 | /// hands it out: presets, full access, OAuth and the token form leave | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 150 | /// it out, and no operation needs it. A [`Resource::retired`] one is |
| 151 | /// never offered again. | |
| 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 | 152 | pub fn offered(self) -> bool { |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 153 | !self.retired() |
| 154 | } | |
| 155 | ||
| 156 | /// Whether it belonged to something g1t no longer has: `agents` (g1t's | |
| 157 | /// own agents) and `models` (its model proxy and AI Gateway). Their | |
| 158 | /// scopes still parse, so tokens and OAuth grants that stored them read | |
| 159 | /// as before, but [`parse_scopes`] leaves them out, nothing hands them | |
| 160 | /// out and no operation needs them: they grant nothing. The TypeScript | |
| 161 | /// mirror lists them under `RETIRED_RESOURCES` and `RETIRED_SCOPES`. | |
| 162 | pub fn retired(self) -> bool { | |
| 163 | matches!(self, Resource::Agents | 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 | 164 | } |
| Fixes from the page sweep | 165 | } |
| 166 | ||
| 167 | /// How much of a resource. | |
| 168 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] | |
| 169 | pub enum Level { | |
| 170 | Read, | |
| 171 | Write, | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 172 | /// Starting g1t's own agents: only `agents:run`, which is retired. |
| Fixes from the page sweep | 173 | Run, |
| 174 | /// Deleting what cannot be brought back, such as a package's versions. | |
| 175 | Delete, | |
| 176 | Admin, | |
| 177 | } | |
| 178 | ||
| 179 | impl Level { | |
| 180 | pub fn as_str(self) -> &'static str { | |
| 181 | match self { | |
| 182 | Level::Read => "read", | |
| 183 | Level::Write => "write", | |
| 184 | Level::Run => "run", | |
| 185 | Level::Delete => "delete", | |
| 186 | Level::Admin => "admin", | |
| 187 | } | |
| 188 | } | |
| 189 | } | |
| 190 | ||
| 191 | /// One scope. Its text form, `resource:level`, is what tokens store, OAuth | |
| 192 | /// clients ask for, and errors name. | |
| 193 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | |
| 194 | pub enum Scope { | |
| 195 | RepoRead, | |
| 196 | RepoWrite, | |
| 197 | RepoAdmin, | |
| 198 | CodeRead, | |
| 199 | CodeWrite, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 200 | SecurityRead, |
| 201 | SecurityWrite, | |
| Fixes from the page sweep | 202 | PackagesRead, |
| 203 | PackagesWrite, | |
| 204 | PackagesDelete, | |
| 205 | IssuesRead, | |
| 206 | IssuesWrite, | |
| 207 | PullRequestsRead, | |
| 208 | PullRequestsWrite, | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 209 | /// Retired with g1t's own agents ([`Resource::retired`]): parses, so |
| 210 | /// stored tokens still read, but is never granted and gates nothing. | |
| Fixes from the page sweep | 211 | AgentsRun, |
| 212 | WorkflowsRead, | |
| 213 | WorkflowsWrite, | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 214 | WorkflowFilesWrite, |
| Merge checks: statuses and check runs on every commit | 215 | ChecksRead, |
| 216 | ChecksWrite, | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 217 | DeploymentsRead, |
| 218 | DeploymentsWrite, | |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 219 | ObservabilityRead, |
| 220 | ObservabilityWrite, | |
| Fixes from the page sweep | 221 | MemoryRead, |
| 222 | MemoryWrite, | |
| 223 | AccountRead, | |
| 224 | AccountWrite, | |
| API: notifications over REST and MCP, with notifications scopes | 225 | NotificationsRead, |
| 226 | NotificationsWrite, | |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 227 | ChatRead, |
| 228 | ChatWrite, | |
| Fixes from the page sweep | 229 | WorkspaceRead, |
| 230 | WorkspaceAdmin, | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 231 | BillingRead, |
| 232 | BillingWrite, | |
| Fixes from the page sweep | 233 | AccessRead, |
| 234 | AccessAdmin, | |
| 235 | WebhooksRead, | |
| 236 | WebhooksAdmin, | |
| 237 | SecretsRead, | |
| 238 | SecretsAdmin, | |
| 239 | RunnersRead, | |
| 240 | RunnersAdmin, | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 241 | /// Retired with the AI Gateway ([`Resource::retired`]): parses, so |
| 242 | /// stored tokens still read, but is never granted and gates nothing. | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 243 | ModelsRead, |
| 244 | 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 | 245 | ArtifactsRead, |
| 246 | ArtifactsWrite, | |
| 247 | ArtifactsAdmin, | |
| Fixes from the page sweep | 248 | } |
| 249 | ||
| 250 | impl Scope { | |
| 251 | /// Every scope, grouped by resource, least first. | |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 252 | pub const ALL: [Scope; 49] = [ |
| Fixes from the page sweep | 253 | Scope::RepoRead, |
| 254 | Scope::RepoWrite, | |
| 255 | Scope::RepoAdmin, | |
| 256 | Scope::CodeRead, | |
| 257 | Scope::CodeWrite, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 258 | Scope::SecurityRead, |
| 259 | Scope::SecurityWrite, | |
| Fixes from the page sweep | 260 | Scope::PackagesRead, |
| 261 | Scope::PackagesWrite, | |
| 262 | Scope::PackagesDelete, | |
| 263 | Scope::IssuesRead, | |
| 264 | Scope::IssuesWrite, | |
| 265 | Scope::PullRequestsRead, | |
| 266 | Scope::PullRequestsWrite, | |
| 267 | Scope::AgentsRun, | |
| 268 | Scope::WorkflowsRead, | |
| 269 | Scope::WorkflowsWrite, | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 270 | Scope::WorkflowFilesWrite, |
| Merge checks: statuses and check runs on every commit | 271 | Scope::ChecksRead, |
| 272 | Scope::ChecksWrite, | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 273 | Scope::DeploymentsRead, |
| 274 | Scope::DeploymentsWrite, | |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 275 | Scope::ObservabilityRead, |
| 276 | Scope::ObservabilityWrite, | |
| Fixes from the page sweep | 277 | Scope::MemoryRead, |
| 278 | Scope::MemoryWrite, | |
| 279 | Scope::AccountRead, | |
| 280 | Scope::AccountWrite, | |
| API: notifications over REST and MCP, with notifications scopes | 281 | Scope::NotificationsRead, |
| 282 | Scope::NotificationsWrite, | |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 283 | Scope::ChatRead, |
| 284 | Scope::ChatWrite, | |
| Fixes from the page sweep | 285 | Scope::WorkspaceRead, |
| 286 | Scope::WorkspaceAdmin, | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 287 | Scope::BillingRead, |
| 288 | Scope::BillingWrite, | |
| Fixes from the page sweep | 289 | Scope::AccessRead, |
| 290 | Scope::AccessAdmin, | |
| 291 | Scope::WebhooksRead, | |
| 292 | Scope::WebhooksAdmin, | |
| 293 | Scope::SecretsRead, | |
| 294 | Scope::SecretsAdmin, | |
| 295 | Scope::RunnersRead, | |
| 296 | Scope::RunnersAdmin, | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 297 | Scope::ModelsRead, |
| 298 | 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 | 299 | Scope::ArtifactsRead, |
| 300 | Scope::ArtifactsWrite, | |
| 301 | Scope::ArtifactsAdmin, | |
| Fixes from the page sweep | 302 | ]; |
| 303 | ||
| 304 | pub fn as_str(self) -> &'static str { | |
| 305 | match self { | |
| 306 | Scope::RepoRead => "repo:read", | |
| 307 | Scope::RepoWrite => "repo:write", | |
| 308 | Scope::RepoAdmin => "repo:admin", | |
| 309 | Scope::CodeRead => "code:read", | |
| 310 | Scope::CodeWrite => "code:write", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 311 | Scope::SecurityRead => "security:read", |
| 312 | Scope::SecurityWrite => "security:write", | |
| Fixes from the page sweep | 313 | Scope::PackagesRead => "packages:read", |
| 314 | Scope::PackagesWrite => "packages:write", | |
| 315 | Scope::PackagesDelete => "packages:delete", | |
| 316 | Scope::IssuesRead => "issues:read", | |
| 317 | Scope::IssuesWrite => "issues:write", | |
| 318 | Scope::PullRequestsRead => "pull_requests:read", | |
| 319 | Scope::PullRequestsWrite => "pull_requests:write", | |
| 320 | Scope::AgentsRun => "agents:run", | |
| 321 | Scope::WorkflowsRead => "workflows:read", | |
| 322 | Scope::WorkflowsWrite => "workflows:write", | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 323 | Scope::WorkflowFilesWrite => "workflow_files:write", |
| Merge checks: statuses and check runs on every commit | 324 | Scope::ChecksRead => "checks:read", |
| 325 | Scope::ChecksWrite => "checks:write", | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 326 | Scope::DeploymentsRead => "deployments:read", |
| 327 | Scope::DeploymentsWrite => "deployments:write", | |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 328 | Scope::ObservabilityRead => "observability:read", |
| 329 | Scope::ObservabilityWrite => "observability:write", | |
| Fixes from the page sweep | 330 | Scope::MemoryRead => "memory:read", |
| 331 | Scope::MemoryWrite => "memory:write", | |
| 332 | Scope::AccountRead => "account:read", | |
| 333 | Scope::AccountWrite => "account:write", | |
| API: notifications over REST and MCP, with notifications scopes | 334 | Scope::NotificationsRead => "notifications:read", |
| 335 | Scope::NotificationsWrite => "notifications:write", | |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 336 | Scope::ChatRead => "chat:read", |
| 337 | Scope::ChatWrite => "chat:write", | |
| Fixes from the page sweep | 338 | Scope::WorkspaceRead => "workspace:read", |
| 339 | Scope::WorkspaceAdmin => "workspace:admin", | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 340 | Scope::BillingRead => "billing:read", |
| 341 | Scope::BillingWrite => "billing:write", | |
| Fixes from the page sweep | 342 | Scope::AccessRead => "access:read", |
| 343 | Scope::AccessAdmin => "access:admin", | |
| 344 | Scope::WebhooksRead => "webhooks:read", | |
| 345 | Scope::WebhooksAdmin => "webhooks:admin", | |
| 346 | Scope::SecretsRead => "secrets:read", | |
| 347 | Scope::SecretsAdmin => "secrets:admin", | |
| 348 | Scope::RunnersRead => "runners:read", | |
| 349 | Scope::RunnersAdmin => "runners:admin", | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 350 | Scope::ModelsRead => "models:read", |
| 351 | 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 | 352 | Scope::ArtifactsRead => "artifacts:read", |
| 353 | Scope::ArtifactsWrite => "artifacts:write", | |
| 354 | Scope::ArtifactsAdmin => "artifacts:admin", | |
| Fixes from the page sweep | 355 | } |
| 356 | } | |
| 357 | ||
| 358 | pub fn parse(text: &str) -> Option<Scope> { | |
| 359 | let text = text.trim().to_ascii_lowercase(); | |
| 360 | Scope::ALL.into_iter().find(|scope| scope.as_str() == text) | |
| 361 | } | |
| 362 | ||
| 363 | pub fn resource(self) -> Resource { | |
| 364 | let name = self.as_str().split_once(':').map_or("", |(resource, _)| resource); | |
| 365 | Resource::ALL | |
| 366 | .into_iter() | |
| 367 | .find(|resource| resource.as_str() == name) | |
| 368 | .unwrap_or(Resource::Account) | |
| 369 | } | |
| 370 | ||
| 371 | pub fn level(self) -> Level { | |
| 372 | match self.as_str().rsplit_once(':').map_or("", |(_, level)| level) { | |
| 373 | "write" => Level::Write, | |
| 374 | "run" => Level::Run, | |
| 375 | "delete" => Level::Delete, | |
| 376 | "admin" => Level::Admin, | |
| 377 | _ => Level::Read, | |
| 378 | } | |
| 379 | } | |
| 380 | ||
| 381 | /// Whether holding `self` gives `other`: the same resource, at the same | |
| 382 | /// level or a lower one. | |
| 383 | pub fn includes(self, other: Scope) -> bool { | |
| 384 | self.resource() == other.resource() && self.level() >= other.level() | |
| 385 | } | |
| 386 | ||
| 387 | /// Changes that are hard or impossible to undo, or that decide who can | |
| 388 | /// reach what. Shown behind a warning wherever scopes are chosen. | |
| 389 | pub fn dangerous(self) -> bool { | |
| 390 | matches!(self.level(), Level::Admin | Level::Delete) | |
| 391 | } | |
| 392 | ||
| 393 | /// What it lets a token do, in plain words. | |
| 394 | pub fn describe(self) -> &'static str { | |
| 395 | match self { | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 396 | Scope::RepoRead => "See repositories, their settings, labels, timelines, releases, languages, contributors and security alerts, and search", |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 397 | Scope::RepoWrite => "Create repositories, create, rename and delete branches, change how pull requests merge and publish releases", |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 398 | Scope::RepoAdmin => "Rename, archive, transfer, delete or change who can see a repository, change its rulesets, and dismiss security alerts", |
| Fixes from the page sweep | 399 | Scope::CodeRead => "Clone and fetch private repositories with git", |
| 400 | Scope::CodeWrite => "Push commits with git", | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 401 | Scope::SecurityRead => "See secret scanning, code scanning and vulnerability alerts, custom patterns, the dependency graph and SBOM, and security settings", |
| 402 | Scope::SecurityWrite => "Dismiss and reopen alerts, bypass push protection, review bypass requests, manage custom patterns, upload SARIF and change security settings", | |
| Fixes from the page sweep | 403 | Scope::PackagesRead => "Pull container images and install private packages", |
| 404 | Scope::PackagesWrite => "Push container images and publish packages", | |
| Merge packages: roles, Actions access, source label, soft delete, API | 405 | Scope::PackagesDelete => "Delete and restore packages and their versions", |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 406 | Scope::IssuesRead => "Read issues and comments", |
| Fixes from the page sweep | 407 | Scope::IssuesWrite => "Open, edit, close and comment on issues", |
| 408 | Scope::PullRequestsRead => "Read pull requests, their changes, sessions and merge queues", | |
| 409 | Scope::PullRequestsWrite => "Open, review, close and merge pull requests", | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 410 | Scope::AgentsRun => "Retired: put g1t's own agents to work. Grants nothing now", |
| Fixes from the page sweep | 411 | Scope::WorkflowsRead => "Read workflows, runs and logs", |
| 412 | Scope::WorkflowsWrite => "Run, cancel, rerun and turn workflows on or off", | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 413 | 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 | 414 | Scope::ChecksRead => "Read commits' statuses, check runs, check suites and annotations", |
| 415 | Scope::ChecksWrite => "Report statuses and check runs on commits, and ask for checks to run again", | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 416 | Scope::DeploymentsRead => "See deployments, their statuses and environments", |
| 417 | Scope::DeploymentsWrite => "Report deployments and their statuses, from any CI", | |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 418 | Scope::ObservabilityRead => "See projects' requests, logs, traces, errors and metrics, and their observability settings, ingest tokens and exports", |
| 419 | Scope::ObservabilityWrite => "Resolve and ignore errors, and change projects' observability settings, ingest tokens and exports", | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 420 | Scope::MemoryRead => "Recall memory", |
| 421 | Scope::MemoryWrite => "Save memory for whoever works on the project next", | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 422 | Scope::AccountRead => "Read your email addresses, invites, invitations, pinned projects and stars", |
| 423 | Scope::AccountWrite => "Change your email addresses, make invites, answer invitations, pin projects and star repositories", | |
| API: notifications over REST and MCP, with notifications scopes | 424 | Scope::NotificationsRead => "See your inbox, its threads, and what you subscribe to and watch", |
| 425 | Scope::NotificationsWrite => "Mark notifications read, done, saved or snoozed, subscribe to threads and watch repositories", | |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 426 | Scope::ChatRead => "Read the conversations you are in: channels, direct messages, threads and reactions, live", |
| 427 | Scope::ChatWrite => "Send, edit and delete your messages, react, mark conversations read, and answer cards in chat", | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 428 | Scope::WorkspaceRead => "Read workspace settings, invites, integrations, teams and rulesets", |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 429 | Scope::WorkspaceAdmin => "Create and delete workspaces, invite members, connect integrations, create, change and delete teams, and change the workspace's rulesets", |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 430 | Scope::BillingRead => "See a workspace's usage, budget and invoices", |
| 431 | Scope::BillingWrite => "Change a workspace's budget", | |
| Fixes from the page sweep | 432 | Scope::AccessRead => "See who has access to repositories", |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 433 | Scope::AccessAdmin => "Give and take away access to repositories, a team's included", |
| Fixes from the page sweep | 434 | Scope::WebhooksRead => "See webhooks and their deliveries", |
| 435 | Scope::WebhooksAdmin => "Create, change and delete webhooks", | |
| 436 | Scope::SecretsRead => "List secrets (never their values) and read variables", | |
| 437 | Scope::SecretsAdmin => "Set and delete secrets and variables", | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 438 | Scope::RunnersRead => "See self-hosted runners and their groups", |
| Fixes from the page sweep | 439 | Scope::RunnersAdmin => "Register and remove self-hosted runners, change their groups and settings", |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 440 | Scope::ModelsRead => "Retired: see the AI Gateway's requests. Grants nothing now", |
| 441 | Scope::ModelsWrite => "Retired: send requests through the AI Gateway. Grants nothing now", | |
| 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 | 442 | Scope::ArtifactsRead => "List, read and search artifacts you can see, their versions, and the numbers their dashboards show", |
| 443 | Scope::ArtifactsWrite => "Create, rename, move, edit, trash and restore artifacts, and propose changes to them", | |
| 444 | Scope::ArtifactsAdmin => "Share artifacts, change who can open them, and delete them for good", | |
| Fixes from the page sweep | 445 | } |
| 446 | } | |
| 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 | 447 | |
| 448 | /// Whether tokens are offered it yet: its resource's [`Resource::offered`]. | |
| 449 | pub fn offered(self) -> bool { | |
| 450 | self.resource().offered() | |
| 451 | } | |
| 452 | } | |
| 453 | ||
| 454 | /// Every scope tokens are offered, in table order: what OAuth advertises. | |
| 455 | pub fn offered_scopes() -> Vec<Scope> { | |
| 456 | Scope::ALL.into_iter().filter(|scope| scope.offered()).collect() | |
| Fixes from the page sweep | 457 | } |
| 458 | ||
| 459 | impl Serialize for Scope { | |
| 460 | fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { | |
| 461 | serializer.serialize_str(self.as_str()) | |
| 462 | } | |
| 463 | } | |
| 464 | ||
| 465 | impl<'de> Deserialize<'de> for Scope { | |
| 466 | fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { | |
| 467 | let text = String::deserialize(deserializer)?; | |
| 468 | Scope::parse(&text).ok_or_else(|| serde::de::Error::custom(format!("unknown scope {text}"))) | |
| 469 | } | |
| 470 | } | |
| 471 | ||
| 472 | /// Scopes as written in a token's row or an OAuth request: separated by | |
| 473 | /// spaces or commas. Unknown names are left out, so a client asking for a | |
| 474 | /// scope from a newer version gets the rest. | |
| 475 | pub fn parse_scopes(text: &str) -> Vec<Scope> { | |
| 476 | let mut scopes: Vec<Scope> = text | |
| 477 | .split(|c: char| c.is_whitespace() || c == ',') | |
| 478 | .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 | 479 | .filter(|scope| scope.offered()) |
| Fixes from the page sweep | 480 | .collect(); |
| 481 | normalize(&mut scopes); | |
| 482 | scopes | |
| 483 | } | |
| 484 | ||
| 485 | /// In table order, without repeats. | |
| 486 | pub fn normalize(scopes: &mut Vec<Scope>) { | |
| 487 | let given = std::mem::take(scopes); | |
| 488 | scopes.extend(Scope::ALL.into_iter().filter(|scope| given.contains(scope))); | |
| 489 | } | |
| 490 | ||
| 491 | /// Space-separated, as stored and as OAuth writes them. | |
| 492 | pub fn scopes_text(scopes: &[Scope]) -> String { | |
| 493 | scopes.iter().map(|scope| scope.as_str()).collect::<Vec<_>>().join(" ") | |
| 494 | } | |
| 495 | ||
| 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 | 496 | /// Where a resource sits on the token form, and which tokens may hold it. |
| 497 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 498 | #[serde(rename_all = "snake_case")] | |
| 499 | pub enum ResourceGroup { | |
| 500 | /// About repositories: what they hold and how they are run. | |
| 501 | Repository, | |
| 502 | /// About a workspace itself. | |
| 503 | Workspace, | |
| 504 | /// About the person: only a personal token may hold these. | |
| 505 | Account, | |
| 506 | } | |
| 507 | ||
| 508 | impl ResourceGroup { | |
| 509 | pub const ALL: [ResourceGroup; 3] = [ResourceGroup::Repository, ResourceGroup::Workspace, ResourceGroup::Account]; | |
| 510 | ||
| 511 | pub fn as_str(self) -> &'static str { | |
| 512 | match self { | |
| 513 | ResourceGroup::Repository => "repository", | |
| 514 | ResourceGroup::Workspace => "workspace", | |
| 515 | ResourceGroup::Account => "account", | |
| 516 | } | |
| 517 | } | |
| 518 | } | |
| 519 | ||
| 520 | impl Resource { | |
| 521 | pub fn group(self) -> ResourceGroup { | |
| 522 | match self { | |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 523 | Resource::Account | Resource::Notifications | Resource::Chat => 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 | 524 | 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 | 525 | _ => ResourceGroup::Repository, |
| 526 | } | |
| 527 | } | |
| 528 | ||
| 529 | pub fn parse(text: &str) -> Option<Resource> { | |
| 530 | let text = text.trim().to_ascii_lowercase(); | |
| 531 | Resource::ALL.into_iter().find(|resource| resource.as_str() == text) | |
| 532 | } | |
| 533 | ||
| 534 | /// Its scopes, least first. | |
| 535 | pub fn scopes(self) -> Vec<Scope> { | |
| 536 | Scope::ALL.into_iter().filter(|scope| scope.resource() == self).collect() | |
| 537 | } | |
| 538 | } | |
| 539 | ||
| 540 | // --- Permissions -------------------------------------------------------------- | |
| 541 | // | |
| 542 | // A token's permissions are its scopes read per resource: each resource | |
| 543 | // at none or one level (`{"issues": "write", "repo": "read"}`). A level | |
| 544 | // includes the ones below it, so the highest scope held of each resource | |
| 545 | // says everything; that is what a token stores. Personal tokens and a | |
| 546 | // workspace's own tokens are made, shown and checked this way alike. | |
| 547 | ||
| 548 | /// The highest scope of each resource held, in table order: the fewest | |
| 549 | /// scopes that give the same access, as tokens store them. | |
| 550 | pub fn top_scopes(scopes: &[Scope]) -> Vec<Scope> { | |
| 551 | let mut top: Vec<Scope> = Vec::new(); | |
| 552 | for resource in Resource::ALL { | |
| 553 | if let Some(best) = scopes.iter().filter(|scope| scope.resource() == resource).max_by_key(|scope| scope.level()) { | |
| 554 | top.push(*best); | |
| 555 | } | |
| 556 | } | |
| 557 | normalize(&mut top); | |
| 558 | top | |
| 559 | } | |
| 560 | ||
| 561 | /// Every resource at its highest level: all a token can be given. | |
| 562 | 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 | 563 | 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 | 564 | } |
| 565 | ||
| 566 | /// Scopes as permissions: each resource held, by name, at its highest | |
| 567 | /// level held. | |
| 568 | pub fn permissions_of(scopes: &[Scope]) -> std::collections::BTreeMap<String, String> { | |
| 569 | top_scopes(scopes) | |
| 570 | .into_iter() | |
| 571 | .map(|scope| (scope.resource().as_str().to_owned(), scope.level().as_str().to_owned())) | |
| 572 | .collect() | |
| 573 | } | |
| 574 | ||
| 575 | /// Permissions as asked for (`{"issues": "write"}`, `none` or empty left | |
| 576 | /// out) into the scopes a token stores, or why they cannot be. `personal` | |
| 577 | /// is whether the token is a person's: only theirs may hold account ones. | |
| 578 | pub fn resolve_permissions(asked: &std::collections::BTreeMap<String, String>, personal: bool) -> Result<Vec<Scope>, String> { | |
| 579 | let mut scopes = Vec::new(); | |
| 580 | 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 | 581 | 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 | 582 | return Err(format!("There is no permission called {name}.")); |
| 583 | }; | |
| 584 | let level = level.trim().to_ascii_lowercase(); | |
| 585 | if level.is_empty() || level == "none" { | |
| 586 | continue; | |
| 587 | } | |
| 588 | let Some(scope) = Scope::parse(&format!("{}:{level}", resource.as_str())) else { | |
| 589 | let levels: Vec<&str> = resource.scopes().iter().map(|scope| scope.level().as_str()).collect(); | |
| 590 | return Err(format!("{} is none or {}, not {level}.", resource.as_str(), levels.join(", "))); | |
| 591 | }; | |
| 592 | if resource.group() == ResourceGroup::Account && !personal { | |
| 593 | return Err(format!("{} is about a person's account: a workspace's token cannot hold it.", resource.as_str())); | |
| 594 | } | |
| 595 | scopes.push(scope); | |
| 596 | } | |
| 597 | Ok(top_scopes(&scopes)) | |
| 598 | } | |
| 599 | ||
| Fixes from the page sweep | 600 | /// What a token stores for full access, which is not a scope a client can |
| 601 | /// ask for by name. | |
| 602 | pub const FULL_ACCESS: &str = "*"; | |
| 603 | ||
| 604 | /// Starting points for choosing scopes. | |
| 605 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | |
| 606 | pub enum Preset { | |
| 607 | ReadOnly, | |
| 608 | Agent, | |
| 609 | Ci, | |
| 610 | Full, | |
| 611 | } | |
| 612 | ||
| 613 | impl Preset { | |
| 614 | pub const ALL: [Preset; 4] = [Preset::ReadOnly, Preset::Agent, Preset::Ci, Preset::Full]; | |
| 615 | ||
| 616 | pub fn as_str(self) -> &'static str { | |
| 617 | match self { | |
| 618 | Preset::ReadOnly => "read_only", | |
| 619 | Preset::Agent => "agent", | |
| 620 | Preset::Ci => "ci", | |
| 621 | Preset::Full => "full", | |
| 622 | } | |
| 623 | } | |
| 624 | ||
| 625 | pub fn label(self) -> &'static str { | |
| 626 | match self { | |
| 627 | Preset::ReadOnly => "Read only", | |
| 628 | Preset::Agent => "Agent", | |
| 629 | Preset::Ci => "CI", | |
| 630 | Preset::Full => "Full access", | |
| 631 | } | |
| 632 | } | |
| 633 | ||
| 634 | /// Its scopes; `None` for full access. | |
| 635 | pub fn scopes(self) -> Option<Vec<Scope>> { | |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 636 | // Chat is not in any: a person's conversations go to an application |
| 637 | // only when it asks for them by name. | |
| 638 | let reads = || { | |
| 639 | Scope::ALL | |
| 640 | .into_iter() | |
| 641 | .filter(|scope| scope.level() == Level::Read && scope.offered() && scope.resource() != Resource::Chat) | |
| 642 | }; | |
| Fixes from the page sweep | 643 | match self { |
| 644 | Preset::ReadOnly => Some(reads().collect()), | |
| 645 | Preset::Agent => { | |
| 646 | // Not the machines work runs on: an agent has no business | |
| 647 | // knowing a workspace's own runners. | |
| 648 | let mut scopes: Vec<Scope> = reads().filter(|scope| scope.resource() != Resource::Runners).collect(); | |
| API: notifications over REST and MCP, with notifications scopes | 649 | // And answering what needs the person it works for: marking |
| 650 | // it done, subscribing, watching. | |
| Fixes from the page sweep | 651 | scopes.extend([ |
| 652 | Scope::CodeWrite, | |
| 653 | Scope::IssuesWrite, | |
| 654 | Scope::PullRequestsWrite, | |
| 655 | Scope::MemoryWrite, | |
| API: notifications over REST and MCP, with notifications scopes | 656 | Scope::NotificationsWrite, |
| Fixes from the page sweep | 657 | ]); |
| 658 | normalize(&mut scopes); | |
| 659 | Some(scopes) | |
| 660 | } | |
| 661 | Preset::Ci => Some(vec![ | |
| 662 | Scope::RepoRead, | |
| 663 | Scope::CodeRead, | |
| 664 | Scope::CodeWrite, | |
| 665 | Scope::PackagesRead, | |
| 666 | Scope::PackagesWrite, | |
| 667 | Scope::WorkflowsRead, | |
| 668 | Scope::WorkflowsWrite, | |
| Merge checks: statuses and check runs on every commit | 669 | Scope::ChecksRead, |
| 670 | Scope::ChecksWrite, | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 671 | Scope::DeploymentsRead, |
| 672 | Scope::DeploymentsWrite, | |
| Fixes from the page sweep | 673 | ]), |
| 674 | Preset::Full => None, | |
| 675 | } | |
| 676 | } | |
| 677 | } | |
| 678 | ||
| 679 | /// What an OAuth client gets when it asks for nothing in particular: the | |
| 680 | /// agent preset. Never an admin scope. | |
| 681 | pub fn oauth_default() -> Vec<Scope> { | |
| 682 | Preset::Agent.scopes().unwrap_or_default() | |
| 683 | } | |
| 684 | ||
| 685 | /// Set on a [`crate::User`] resolved from an access token: what the token | |
| 686 | /// may do. Absent on a signed-in session, which may do whatever its person | |
| 687 | /// can. | |
| 688 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 689 | pub struct TokenAccess { | |
| 690 | /// The token's id, as audit entries and errors name it. | |
| 691 | #[serde(default)] | |
| 692 | pub token_id: String, | |
| 693 | /// Its scopes, as `resource:level`. Absent: full access, everything the | |
| 694 | /// person (or workspace) can do. | |
| 695 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 696 | pub scopes: Option<Vec<String>>, | |
| 697 | /// Made before tokens had scopes: full access until someone narrows it. | |
| 698 | #[serde(default, skip_serializing_if = "std::ops::Not::not")] | |
| 699 | pub legacy: bool, | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 700 | /// Set on a workflow job's token (`G1T_TOKEN`): the one repository it |
| 701 | /// reaches, as `owner/name`. Every other is refused, whatever its owner | |
| 702 | /// could reach. | |
| 703 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 704 | pub repo: Option<String>, | |
| 705 | /// Set on a workflow job's token: the run and job it was made for. The | |
| 706 | /// audit log records its changes as that job's, and what it changes | |
| 707 | /// starts no workflows (only `workflow_dispatch` and | |
| 708 | /// `repository_dispatch` do), so a workflow cannot set itself off. | |
| 709 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 710 | pub job: Option<JobToken>, | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 711 | /// The token's name, as its owner gave it, so a log can say which |
| 712 | /// token made a request. Absent where whoever resolved it did not say. | |
| 713 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 714 | 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 | 715 | /// Set on a token narrowed to less than its owner can reach: one |
| 716 | /// workspace (all, selected or none of its private repositories), or | |
| 717 | /// none at all (its owner's account and public repositories). Absent | |
| 718 | /// on a token that reaches every workspace its owner can. | |
| 719 | /// | |
| 720 | /// The wire key is `fine_grained`, kept from before tokens were one | |
| 721 | /// kind, so services deployed at different moments agree on it. | |
| 722 | #[serde(rename = "fine_grained", default, skip_serializing_if = "Option::is_none")] | |
| 723 | pub reach: Option<TokenReach>, | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 724 | /// Set on a workspace's own token that an owner gave Admin when making |
| 725 | /// it. Without it a workspace's token has Write on the workspace's | |
| 726 | /// repositories, as a member would (see [`crate::access`]). | |
| 727 | #[serde(default, skip_serializing_if = "std::ops::Not::not")] | |
| 728 | pub admin: bool, | |
| 729 | /// Set on what a repository's deploy key resolves to: the key's id. Its | |
| 730 | /// `repo` is the one repository it reaches. | |
| 731 | #[serde(default, skip_serializing_if = "Option::is_none")] | |
| 732 | pub deploy_key: Option<String>, | |
| Merge main into Artifacts Phase 2 | 733 | /// Set on a person's token whose owner let it use the website (g1t.sh) |
| 734 | /// as them, sent as `Authorization: Bearer`. Not a scope: no preset, | |
| 735 | /// full access or OAuth grant includes it, and git, the API and MCP | |
| 736 | /// ignore it. | |
| 737 | #[serde(default, skip_serializing_if = "std::ops::Not::not")] | |
| 738 | pub website: bool, | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 739 | } |
| 740 | ||
| One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers | 741 | /// Which repositories a token reaches in the workspace it is made for. |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 742 | #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] |
| 743 | #[serde(rename_all = "snake_case")] | |
| 744 | pub enum RepositorySelection { | |
| 745 | /// Every repository of the workspace, ones made later included. | |
| 746 | #[default] | |
| 747 | All, | |
| 748 | /// The repositories chosen, by id. | |
| 749 | Selected, | |
| 750 | /// None of the workspace's private repositories: public repositories, | |
| 751 | /// 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 | 752 | /// With no workspace: the owner's account and public repositories only. |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 753 | Public, |
| Fixes from the page sweep | 754 | } |
| 755 | ||
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 756 | impl RepositorySelection { |
| 757 | pub fn as_str(self) -> &'static str { | |
| 758 | match self { | |
| 759 | RepositorySelection::All => "all", | |
| 760 | RepositorySelection::Selected => "selected", | |
| 761 | RepositorySelection::Public => "public", | |
| 762 | } | |
| 763 | } | |
| 764 | ||
| 765 | pub fn parse(text: &str) -> Option<RepositorySelection> { | |
| 766 | match text.trim().to_ascii_lowercase().as_str() { | |
| 767 | "all" => Some(RepositorySelection::All), | |
| 768 | "selected" => Some(RepositorySelection::Selected), | |
| 769 | "public" | "public_only" | "none" => Some(RepositorySelection::Public), | |
| 770 | _ => None, | |
| 771 | } | |
| 772 | } | |
| 773 | } | |
| 774 | ||
| 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 | 775 | /// What a narrowed token reaches, as identity resolves it on each use. |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 776 | #[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 | 777 | pub struct TokenReach { |
| 778 | /// The workspace whose repositories and settings it reaches, by slug as | |
| 779 | /// it is now. Absent: its owner's account only, with public | |
| 780 | /// repositories read-only. | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 781 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 782 | pub workspace: Option<String>, | |
| 783 | #[serde(default)] | |
| 784 | pub repositories: RepositorySelection, | |
| 785 | /// With [`RepositorySelection::Selected`]: the repositories' ids. | |
| 786 | #[serde(default, skip_serializing_if = "Vec::is_empty")] | |
| 787 | pub repo_ids: Vec<String>, | |
| 788 | } | |
| 789 | ||
| 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 | 790 | impl TokenReach { |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 791 | /// Whether it reaches the repository with this id in the workspace |
| 792 | /// `namespace` for more than what anyone may do with a public one. | |
| 793 | pub fn covers(&self, repo_id: &str, namespace: &str) -> bool { | |
| 794 | let Some(workspace) = self.workspace.as_deref() else { | |
| 795 | return false; | |
| 796 | }; | |
| 797 | if !workspace.eq_ignore_ascii_case(namespace) { | |
| 798 | return false; | |
| 799 | } | |
| 800 | match self.repositories { | |
| 801 | RepositorySelection::All => true, | |
| 802 | RepositorySelection::Selected => self.repo_ids.iter().any(|id| id == repo_id), | |
| 803 | RepositorySelection::Public => false, | |
| 804 | } | |
| 805 | } | |
| 806 | ||
| 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 | 807 | /// Whether it is made for the workspace `slug`. |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 808 | pub fn owned_by(&self, slug: &str) -> bool { |
| 809 | self.workspace.as_deref().is_some_and(|workspace| workspace.eq_ignore_ascii_case(slug)) | |
| 810 | } | |
| 811 | } | |
| 812 | ||
| 813 | /// Where workflow files live. Adding, changing or deleting a file under | |
| 814 | /// one, with git or through g1t, needs [`Scope::WorkflowFilesWrite`] from a | |
| 815 | /// token: what GitHub's `workflow` scope and `workflows` permission do. | |
| 816 | pub const WORKFLOW_DIRS: [&str; 2] = [".g1t/workflows/", ".github/workflows/"]; | |
| 817 | ||
| 818 | /// Whether `path` is a workflow file, or a file in one's directory. | |
| 819 | pub fn is_workflow_file(path: &str) -> bool { | |
| 820 | let path = path.trim_start_matches('/'); | |
| 821 | WORKFLOW_DIRS.iter().any(|dir| { | |
| 822 | path.len() >= dir.len() && path.is_char_boundary(dir.len()) && path[..dir.len()].eq_ignore_ascii_case(dir) | |
| 823 | }) || WORKFLOW_DIRS.iter().any(|dir| path.eq_ignore_ascii_case(dir.trim_end_matches('/'))) | |
| 824 | } | |
| 825 | ||
| 826 | /// Whether a token may add, change or delete the files at `paths`: a | |
| 827 | /// refusal naming the first workflow file it may not touch, else `None`. | |
| 828 | /// A signed-in person (no token) is never refused here; their role decides. | |
| 829 | pub fn decide_workflow_files<'a>(access: Option<&TokenAccess>, paths: impl IntoIterator<Item = &'a str>) -> Option<Decision> { | |
| 830 | let access = access?; | |
| 831 | if access.allows(Scope::WorkflowFilesWrite) && access.job.is_none() { | |
| 832 | return None; | |
| 833 | } | |
| 834 | let path = paths.into_iter().find(|path| is_workflow_file(path))?; | |
| 835 | let why = if access.job.is_some() { | |
| 836 | "a workflow job's token can never add or change workflow files".to_owned() | |
| 837 | } else { | |
| 838 | format!("it needs the {} scope", Scope::WorkflowFilesWrite.as_str()) | |
| 839 | }; | |
| 840 | Some(Decision::deny( | |
| 841 | "token:workflows", | |
| 842 | format!("This access token cannot change the workflow file {path}: {why}."), | |
| 843 | )) | |
| 844 | } | |
| 845 | ||
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 846 | /// The workflow job a token was made for. |
| 847 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] | |
| 848 | pub struct JobToken { | |
| 849 | /// The run, `run_…`. | |
| 850 | pub run_id: String, | |
| 851 | /// The job, `job_…`. | |
| 852 | pub job_id: String, | |
| 853 | /// Whether it may open pull requests and approve them, by its | |
| 854 | /// repository's and workspace's choice ("Allow g1t Actions to create and | |
| 855 | /// approve pull requests"). Off unless chosen. | |
| 856 | #[serde(default)] | |
| 857 | pub pull_requests: bool, | |
| 858 | } | |
| 859 | ||
| Fixes from the page sweep | 860 | impl TokenAccess { |
| 861 | /// Full access to everything: the access tokens made before scopes had. | |
| 862 | pub fn full() -> Self { | |
| 863 | TokenAccess::default() | |
| 864 | } | |
| 865 | ||
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 866 | /// Whether it may reach the repository `owner/name`: every token but a |
| 867 | /// workflow job's, which reaches its own repository only. | |
| 868 | pub fn reaches(&self, repo: &str) -> bool { | |
| 869 | self.repo.as_deref().is_none_or(|only| only.eq_ignore_ascii_case(repo)) | |
| 870 | } | |
| 871 | ||
| Fixes from the page sweep | 872 | pub fn is_full(&self) -> bool { |
| 873 | self.scopes.is_none() | |
| 874 | } | |
| 875 | ||
| 876 | /// The scopes it holds, or `None` for full access. | |
| 877 | pub fn granted(&self) -> Option<Vec<Scope>> { | |
| 878 | self.scopes | |
| 879 | .as_ref() | |
| 880 | .map(|scopes| scopes.iter().filter_map(|scope| Scope::parse(scope)).collect()) | |
| 881 | } | |
| 882 | ||
| 883 | pub fn allows(&self, needed: Scope) -> bool { | |
| 884 | match self.granted() { | |
| 885 | None => true, | |
| 886 | Some(granted) => granted.iter().any(|held| held.includes(needed)), | |
| 887 | } | |
| 888 | } | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 889 | |
| 890 | /// 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 | 891 | /// more than reading a public one: every token but a narrowed one |
| 892 | /// outside its workspace or repository selection. Its owner's role | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 893 | /// still decides; see [`crate::access`]. |
| 894 | 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 | 895 | self.reach.as_ref().is_none_or(|reach| reach.covers(repo_id, namespace)) |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 896 | } |
| Fixes from the page sweep | 897 | } |
| 898 | ||
| 899 | /// Every operation of the API and MCP server, with the scope it needs. An | |
| 900 | /// operation in [`NO_SCOPE`] needs none. The API checks that every one of | |
| 901 | /// its operations is in exactly one of the two. | |
| 902 | pub const OPERATIONS: &[(&str, Scope)] = &[ | |
| 903 | // Your account. | |
| 904 | ("list_emails", Scope::AccountRead), | |
| 905 | ("add_email", Scope::AccountWrite), | |
| Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036) | 906 | ("confirm_email", Scope::AccountWrite), |
| Fixes from the page sweep | 907 | ("remove_email", Scope::AccountWrite), |
| 908 | ("update_email_settings", 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) | 909 | ("list_invitations", Scope::AccountRead), |
| 910 | ("accept_invitation", Scope::AccountWrite), | |
| 911 | ("decline_invitation", Scope::AccountWrite), | |
| Fixes from the page sweep | 912 | ("list_my_repo_invitations", Scope::AccountRead), |
| 913 | ("accept_repo_invitation", Scope::AccountWrite), | |
| 914 | ("decline_repo_invitation", Scope::AccountWrite), | |
| API: pinned projects over REST and MCP | 915 | // Your pinned projects: a preference of your account. |
| 916 | ("list_pinned_projects", Scope::AccountRead), | |
| 917 | ("pin_project", Scope::AccountWrite), | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 918 | // Your stars: a preference of your account. |
| 919 | ("list_starred", Scope::AccountRead), | |
| 920 | ("check_starred", Scope::AccountRead), | |
| 921 | ("star_repo", Scope::AccountWrite), | |
| 922 | ("unstar_repo", Scope::AccountWrite), | |
| API: pinned projects over REST and MCP | 923 | ("unpin_project", Scope::AccountWrite), |
| 924 | ("reorder_pinned_projects", Scope::AccountWrite), | |
| API: notifications over REST and MCP, with notifications scopes | 925 | // Your inbox: notifications, subscriptions and watching. |
| 926 | ("list_notifications", Scope::NotificationsRead), | |
| 927 | ("get_notification_thread", Scope::NotificationsRead), | |
| 928 | ("get_thread_subscription", Scope::NotificationsRead), | |
| 929 | ("get_repo_subscription", Scope::NotificationsRead), | |
| 930 | ("list_watched_repos", Scope::NotificationsRead), | |
| 931 | ("mark_notifications_read", Scope::NotificationsWrite), | |
| 932 | ("mark_thread_read", Scope::NotificationsWrite), | |
| 933 | ("mark_thread_done", Scope::NotificationsWrite), | |
| 934 | ("save_thread", Scope::NotificationsWrite), | |
| 935 | ("snooze_thread", Scope::NotificationsWrite), | |
| 936 | ("set_thread_subscription", Scope::NotificationsWrite), | |
| 937 | ("delete_thread_subscription", Scope::NotificationsWrite), | |
| 938 | ("set_repo_subscription", Scope::NotificationsWrite), | |
| 939 | ("delete_repo_subscription", Scope::NotificationsWrite), | |
| Fixes from the page sweep | 940 | // Workspaces, their invites and integrations. |
| 941 | ("create_workspace", Scope::WorkspaceAdmin), | |
| 942 | ("delete_workspace", Scope::WorkspaceAdmin), | |
| Merge branch 'worktree-agent-ad7c6d88d93adc817' | 943 | ("get_workspace", Scope::WorkspaceRead), |
| Fixes from the page sweep | 944 | ("update_workspace", Scope::WorkspaceAdmin), |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 945 | // Its members, and who owns it. |
| 946 | ("list_members", Scope::WorkspaceRead), | |
| 947 | ("update_member", Scope::WorkspaceAdmin), | |
| 948 | ("remove_member", Scope::WorkspaceAdmin), | |
| 949 | ("transfer_ownership", Scope::WorkspaceAdmin), | |
| 950 | ("leave_workspace", Scope::AccountWrite), | |
| Fixes from the page sweep | 951 | ("list_workspace_invites", Scope::WorkspaceRead), |
| 952 | ("invite_member", Scope::WorkspaceAdmin), | |
| 953 | ("revoke_workspace_invite", Scope::WorkspaceAdmin), | |
| 954 | ("list_integrations", Scope::WorkspaceRead), | |
| 955 | ("connect_integration", Scope::WorkspaceAdmin), | |
| AI Gateway: OpenAI's format, open models, and your own providers | 956 | ("update_integration", Scope::WorkspaceAdmin), |
| Fixes from the page sweep | 957 | ("disconnect_integration", Scope::WorkspaceAdmin), |
| 958 | ("test_integration", Scope::WorkspaceAdmin), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 959 | // Teams: reading them, and managing them. A team's role on a |
| 960 | // repository is who has access. | |
| 961 | ("list_teams", Scope::WorkspaceRead), | |
| 962 | ("get_team", Scope::WorkspaceRead), | |
| 963 | ("list_team_members", Scope::WorkspaceRead), | |
| 964 | ("list_child_teams", Scope::WorkspaceRead), | |
| 965 | ("list_team_repos", Scope::WorkspaceRead), | |
| 966 | ("list_user_teams", Scope::WorkspaceRead), | |
| 967 | ("create_team", Scope::WorkspaceAdmin), | |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 968 | ("list_workspace_rulesets", Scope::WorkspaceRead), |
| 969 | ("get_workspace_ruleset", Scope::WorkspaceRead), | |
| 970 | ("list_workspace_rule_evaluations", Scope::WorkspaceRead), | |
| 971 | ("create_workspace_ruleset", Scope::WorkspaceAdmin), | |
| 972 | ("update_workspace_ruleset", Scope::WorkspaceAdmin), | |
| 973 | ("delete_workspace_ruleset", Scope::WorkspaceAdmin), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 974 | ("update_team", Scope::WorkspaceAdmin), |
| 975 | ("delete_team", Scope::WorkspaceAdmin), | |
| 976 | ("set_team_member", Scope::WorkspaceAdmin), | |
| 977 | ("remove_team_member", Scope::WorkspaceAdmin), | |
| 978 | ("set_team_review_assignment", Scope::WorkspaceAdmin), | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 979 | // A workspace's billing: usage, budget and invoices. |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 980 | ("get_usage", Scope::BillingRead), |
| 981 | ("get_budget", Scope::BillingRead), | |
| 982 | ("list_invoices", Scope::BillingRead), | |
| 983 | ("get_billing_details", Scope::BillingRead), | |
| 984 | ("set_budget", Scope::BillingWrite), | |
| Fixes from the page sweep | 985 | // Repositories. |
| 986 | ("list_repos", Scope::RepoRead), | |
| 987 | ("get_repo", Scope::RepoRead), | |
| Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's. | 988 | // A repository's files, as clone and fetch need. |
| 989 | ("download_tarball_archive", Scope::CodeRead), | |
| 990 | ("download_zipball_archive", Scope::CodeRead), | |
| Merge branch 'worktree-agent-ab8033ab973f8d99a' | 991 | ("why", Scope::CodeRead), |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 992 | // Projects follow their repositories. |
| 993 | ("list_projects", Scope::RepoRead), | |
| 994 | ("get_project", Scope::RepoRead), | |
| Fixes from the page sweep | 995 | ("search", Scope::RepoRead), |
| 996 | ("list_events", Scope::RepoRead), | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 997 | // What the default branch says about a repository, who starred it, and |
| 998 | // its releases. | |
| 999 | ("get_languages", Scope::RepoRead), | |
| 1000 | ("list_contributors", Scope::RepoRead), | |
| 1001 | ("get_license", Scope::RepoRead), | |
| 1002 | ("list_stargazers", Scope::RepoRead), | |
| 1003 | ("list_releases", Scope::RepoRead), | |
| 1004 | ("get_latest_release", Scope::RepoRead), | |
| 1005 | ("get_release_by_tag", Scope::RepoRead), | |
| 1006 | ("get_release", Scope::RepoRead), | |
| 1007 | ("create_release", Scope::RepoWrite), | |
| 1008 | ("update_release", Scope::RepoWrite), | |
| 1009 | ("delete_release", Scope::RepoWrite), | |
| Fixes from the page sweep | 1010 | ("list_labels", Scope::RepoRead), |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1011 | ("list_milestones", Scope::RepoRead), |
| 1012 | ("get_milestone", Scope::RepoRead), | |
| 1013 | ("create_label", Scope::IssuesWrite), | |
| 1014 | ("update_label", Scope::IssuesWrite), | |
| 1015 | ("delete_label", Scope::IssuesWrite), | |
| 1016 | ("add_default_labels", Scope::IssuesWrite), | |
| 1017 | ("create_milestone", Scope::IssuesWrite), | |
| 1018 | ("update_milestone", Scope::IssuesWrite), | |
| 1019 | ("delete_milestone", Scope::IssuesWrite), | |
| Fixes from the page sweep | 1020 | ("get_repo_settings", Scope::RepoRead), |
| 1021 | ("list_check_names", Scope::RepoRead), | |
| 1022 | ("list_deleted_repos", Scope::RepoRead), | |
| 1023 | ("list_security_alerts", Scope::RepoRead), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1024 | ("get_codeowners_errors", Scope::RepoRead), |
| Fixes from the page sweep | 1025 | ("create_repo", Scope::RepoWrite), |
| 1026 | ("update_repo", Scope::RepoWrite), | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 1027 | ("update_project", Scope::RepoWrite), |
| Fixes from the page sweep | 1028 | ("update_repo_settings", Scope::RepoWrite), |
| Merge rulesets: branch and tag rules, agent-first, enforced on push and merge | 1029 | // Rulesets: reading them is reading the repository; changing them |
| 1030 | // changes what everyone, agents included, may do, so it is admin. | |
| 1031 | ("list_repo_rulesets", Scope::RepoRead), | |
| 1032 | ("get_repo_ruleset", Scope::RepoRead), | |
| 1033 | ("get_branch_rules", Scope::RepoRead), | |
| 1034 | ("list_rule_evaluations", Scope::RepoRead), | |
| 1035 | ("create_repo_ruleset", Scope::RepoAdmin), | |
| 1036 | ("update_repo_ruleset", Scope::RepoAdmin), | |
| 1037 | ("delete_repo_ruleset", Scope::RepoAdmin), | |
| Fixes from the page sweep | 1038 | ("rename_branch", Scope::RepoWrite), |
| The Branches page does things to branches now. It has tabs (Overview, Yours, Active, Stale, All, kept in the address as ?tab=) and search; Overview shows the default branch, then the first five of your branches, the active ones and the stale ones, each linking to the rest. Each branch is a row with its name in a code chip with a copy button, who made its latest commit and when, that commit's checks, behind and ahead counts with bars drawn against the largest shown, and its pull request (the open one, else the newest merged or closed) with its state; on a phone the columns fold under the name. Yours is branches whose latest commit is the viewer's; stale is no commit in 90 days. | 1039 | ("create_branch", Scope::RepoWrite), |
| 1040 | ("delete_branch", Scope::RepoWrite), | |
| Fixes from the page sweep | 1041 | ("rename_repo", Scope::RepoAdmin), |
| 1042 | ("transfer_repo", Scope::RepoAdmin), | |
| 1043 | ("archive_repo", Scope::RepoAdmin), | |
| 1044 | ("unarchive_repo", Scope::RepoAdmin), | |
| 1045 | ("set_repo_visibility", Scope::RepoAdmin), | |
| 1046 | ("delete_repo", Scope::RepoAdmin), | |
| 1047 | ("restore_repo", Scope::RepoAdmin), | |
| 1048 | ("purge_repo", Scope::RepoAdmin), | |
| 1049 | // A dismissed secret is let through push protection. | |
| 1050 | ("dismiss_security_alert", Scope::RepoAdmin), | |
| 1051 | ("reopen_security_alert", Scope::RepoAdmin), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1052 | // The security suite: alerts, push protection, patterns, code |
| 1053 | // scanning, the supply chain and settings. | |
| 1054 | ("list_secret_scanning_alerts", Scope::SecurityRead), | |
| 1055 | ("get_secret_scanning_alert", Scope::SecurityRead), | |
| 1056 | ("list_secret_scanning_locations", Scope::SecurityRead), | |
| 1057 | ("list_bypass_requests", Scope::SecurityRead), | |
| 1058 | ("list_custom_patterns", Scope::SecurityRead), | |
| 1059 | ("list_code_scanning_alerts", Scope::SecurityRead), | |
| 1060 | ("get_code_scanning_alert", Scope::SecurityRead), | |
| 1061 | ("list_code_scanning_analyses", Scope::SecurityRead), | |
| 1062 | ("get_sarif_upload", Scope::SecurityRead), | |
| 1063 | ("list_vulnerability_alerts", Scope::SecurityRead), | |
| 1064 | ("get_vulnerability_alert", Scope::SecurityRead), | |
| 1065 | ("get_dependency_graph", Scope::SecurityRead), | |
| 1066 | ("get_sbom", Scope::SecurityRead), | |
| 1067 | ("compare_dependencies", Scope::SecurityRead), | |
| 1068 | ("get_security_settings", Scope::SecurityRead), | |
| 1069 | ("get_workspace_security_settings", Scope::SecurityRead), | |
| 1070 | ("get_security_overview", Scope::SecurityRead), | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 1071 | ("list_security_advisories", Scope::SecurityRead), |
| 1072 | ("get_security_advisory", Scope::SecurityRead), | |
| 1073 | ("list_vulnerability_reports", Scope::SecurityRead), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1074 | ("update_secret_scanning_alert", Scope::SecurityWrite), |
| 1075 | ("bypass_push_protection", Scope::SecurityWrite), | |
| 1076 | ("check_secret_validity", Scope::SecurityWrite), | |
| 1077 | ("review_bypass_request", Scope::SecurityWrite), | |
| 1078 | ("create_custom_pattern", Scope::SecurityWrite), | |
| 1079 | ("update_custom_pattern", Scope::SecurityWrite), | |
| 1080 | ("delete_custom_pattern", Scope::SecurityWrite), | |
| 1081 | ("dry_run_custom_pattern", Scope::SecurityWrite), | |
| 1082 | ("update_code_scanning_alert", Scope::SecurityWrite), | |
| 1083 | ("upload_sarif", Scope::SecurityWrite), | |
| 1084 | ("update_vulnerability_alert", Scope::SecurityWrite), | |
| 1085 | ("update_security_settings", Scope::SecurityWrite), | |
| 1086 | ("update_workspace_security_settings", Scope::SecurityWrite), | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9 | 1087 | ("create_security_advisory", Scope::SecurityWrite), |
| 1088 | ("update_security_advisory", Scope::SecurityWrite), | |
| 1089 | ("set_security_advisory_state", Scope::SecurityWrite), | |
| 1090 | ("report_vulnerability", Scope::SecurityWrite), | |
| 1091 | ("review_vulnerability_report", Scope::SecurityWrite), | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1092 | // Issues. |
| Fixes from the page sweep | 1093 | ("list_issues", Scope::IssuesRead), |
| 1094 | ("get_issue", Scope::IssuesRead), | |
| 1095 | ("create_issue", Scope::IssuesWrite), | |
| 1096 | ("update_issue", Scope::IssuesWrite), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1097 | ("list_issue_labels", Scope::IssuesRead), |
| 1098 | ("add_issue_labels", Scope::IssuesWrite), | |
| 1099 | ("set_issue_labels", Scope::IssuesWrite), | |
| 1100 | ("remove_issue_labels", Scope::IssuesWrite), | |
| Fixes from the page sweep | 1101 | ("close_issue", Scope::IssuesWrite), |
| 1102 | ("reopen_issue", Scope::IssuesWrite), | |
| 1103 | ("add_comment", Scope::IssuesWrite), | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 1104 | ("edit_comment", Scope::IssuesWrite), |
| 1105 | ("delete_comment", Scope::IssuesWrite), | |
| Fixes from the page sweep | 1106 | ("import_issue", Scope::IssuesWrite), |
| 1107 | // Pull requests. | |
| 1108 | ("list_pull_requests", Scope::PullRequestsRead), | |
| 1109 | ("get_pull_request", Scope::PullRequestsRead), | |
| 1110 | ("get_pull_request_changes", Scope::PullRequestsRead), | |
| 1111 | ("read_session", Scope::PullRequestsRead), | |
| 1112 | ("get_merge_queue", Scope::PullRequestsRead), | |
| Merge branch 'worktree-agent-ab8033ab973f8d99a' | 1113 | ("list_decisions", Scope::PullRequestsRead), |
| Fixes from the page sweep | 1114 | ("create_pull_request", Scope::PullRequestsWrite), |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1115 | ("update_pull_request", Scope::PullRequestsWrite), |
| Fixes from the page sweep | 1116 | ("record_session", Scope::PullRequestsWrite), |
| Merge branch 'worktree-agent-ab8033ab973f8d99a' | 1117 | ("record_decision", Scope::PullRequestsWrite), |
| Fixes from the page sweep | 1118 | ("mark_pull_request_ready", Scope::PullRequestsWrite), |
| 1119 | ("close_pull_request", Scope::PullRequestsWrite), | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 1120 | ("reopen_pull_request", Scope::PullRequestsWrite), |
| 1121 | ("convert_pull_request_to_draft", Scope::PullRequestsWrite), | |
| Fixes from the page sweep | 1122 | ("review_pull_request", Scope::PullRequestsWrite), |
| 1123 | ("merge_pull_request", Scope::PullRequestsWrite), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1124 | ("request_reviewers", Scope::PullRequestsWrite), |
| 1125 | ("remove_requested_reviewers", Scope::PullRequestsWrite), | |
| Fixes from the page sweep | 1126 | // Workflows. |
| 1127 | ("list_workflows", Scope::WorkflowsRead), | |
| 1128 | ("list_workflow_runs", Scope::WorkflowsRead), | |
| 1129 | ("get_workflow_run", Scope::WorkflowsRead), | |
| 1130 | ("get_job_logs", Scope::WorkflowsRead), | |
| 1131 | ("dispatch_workflow", Scope::WorkflowsWrite), | |
| 1132 | ("cancel_workflow_run", Scope::WorkflowsWrite), | |
| 1133 | ("rerun_workflow_run", Scope::WorkflowsWrite), | |
| 1134 | ("update_workflow", Scope::WorkflowsWrite), | |
| Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R2 | 1135 | ("list_artifacts", Scope::WorkflowsRead), |
| 1136 | ("list_workflow_run_artifacts", Scope::WorkflowsRead), | |
| 1137 | ("get_artifact", Scope::WorkflowsRead), | |
| 1138 | ("download_artifact", Scope::WorkflowsRead), | |
| 1139 | ("get_artifact_retention", Scope::WorkflowsRead), | |
| 1140 | ("delete_artifact", Scope::WorkflowsWrite), | |
| 1141 | ("set_artifact_retention", Scope::WorkflowsWrite), | |
| Merge checks: statuses and check runs on every commit | 1142 | // Checks: statuses, check runs and check suites on commits. |
| 1143 | ("list_commit_statuses", Scope::ChecksRead), | |
| 1144 | ("get_combined_status", Scope::ChecksRead), | |
| 1145 | ("list_check_runs_for_ref", Scope::ChecksRead), | |
| 1146 | ("get_check_run", Scope::ChecksRead), | |
| 1147 | ("list_check_run_annotations", Scope::ChecksRead), | |
| 1148 | ("list_check_suites_for_ref", Scope::ChecksRead), | |
| 1149 | ("get_check_suite", Scope::ChecksRead), | |
| 1150 | ("create_commit_status", Scope::ChecksWrite), | |
| 1151 | ("create_check_run", Scope::ChecksWrite), | |
| 1152 | ("update_check_run", Scope::ChecksWrite), | |
| 1153 | ("rerequest_check_run", Scope::ChecksWrite), | |
| 1154 | ("rerequest_check_suite", Scope::ChecksWrite), | |
| Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97 | 1155 | // Deployments, wherever they run: reading them, and reporting them. |
| 1156 | ("list_deployments", Scope::DeploymentsRead), | |
| 1157 | ("get_deployment", Scope::DeploymentsRead), | |
| 1158 | ("list_deployment_statuses", Scope::DeploymentsRead), | |
| 1159 | ("list_environments", Scope::DeploymentsRead), | |
| 1160 | ("get_environment", Scope::DeploymentsRead), | |
| 1161 | ("create_deployment", Scope::DeploymentsWrite), | |
| 1162 | ("create_deployment_status", Scope::DeploymentsWrite), | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1163 | ("start_deployment_build", Scope::DeploymentsWrite), |
| Projects have Observability: their requests, logs, traces, metrics and errors, from apps on g1t.page with nothing to set up, and from software anywhere else over OpenTelemetry. | 1164 | // Observability: what a project's software did, and how it is collected. |
| 1165 | ("get_observability_overview", Scope::ObservabilityRead), | |
| 1166 | ("list_requests", Scope::ObservabilityRead), | |
| 1167 | ("get_request", Scope::ObservabilityRead), | |
| 1168 | ("list_logs", Scope::ObservabilityRead), | |
| 1169 | ("list_traces", Scope::ObservabilityRead), | |
| 1170 | ("get_trace", Scope::ObservabilityRead), | |
| 1171 | ("list_errors", Scope::ObservabilityRead), | |
| 1172 | ("list_metrics", Scope::ObservabilityRead), | |
| 1173 | ("get_metric", Scope::ObservabilityRead), | |
| 1174 | ("get_observability_settings", Scope::ObservabilityRead), | |
| 1175 | ("list_ingest_tokens", Scope::ObservabilityRead), | |
| 1176 | ("list_telemetry_exports", Scope::ObservabilityRead), | |
| 1177 | ("get_observability_usage", Scope::ObservabilityRead), | |
| 1178 | ("update_error", Scope::ObservabilityWrite), | |
| 1179 | ("update_observability_settings", Scope::ObservabilityWrite), | |
| 1180 | ("create_ingest_token", Scope::ObservabilityWrite), | |
| 1181 | ("delete_ingest_token", Scope::ObservabilityWrite), | |
| 1182 | ("create_telemetry_export", Scope::ObservabilityWrite), | |
| 1183 | ("update_telemetry_export", Scope::ObservabilityWrite), | |
| 1184 | ("delete_telemetry_export", Scope::ObservabilityWrite), | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1185 | // What keeps runs safe: the runs environments hold and reviewing them, |
| 1186 | // approving a pull request's run, and a repository's own rules for | |
| 1187 | // its environments and tokens, which are an admin's. | |
| 1188 | ("get_pending_deployments", Scope::WorkflowsRead), | |
| 1189 | ("review_pending_deployments", Scope::WorkflowsWrite), | |
| 1190 | ("approve_workflow_run", Scope::WorkflowsWrite), | |
| 1191 | ("get_workflow_permissions", Scope::RepoRead), | |
| 1192 | ("get_fork_pr_approval", Scope::RepoRead), | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 1193 | ("get_actions_access", Scope::RepoRead), |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1194 | ("update_environment", Scope::RepoAdmin), |
| 1195 | ("delete_environment", Scope::RepoAdmin), | |
| 1196 | ("set_workflow_permissions", Scope::RepoAdmin), | |
| 1197 | ("set_fork_pr_approval", Scope::RepoAdmin), | |
| Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts | 1198 | ("set_actions_access", Scope::RepoAdmin), |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1199 | // Starting workflows from outside, as a push would. |
| 1200 | ("create_repository_dispatch", Scope::CodeWrite), | |
| 1201 | // A workspace's policy for its repositories' tokens. | |
| 1202 | ("get_workspace_workflow_permissions", Scope::WorkspaceRead), | |
| 1203 | ("set_workspace_workflow_permissions", Scope::WorkspaceAdmin), | |
| Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's. | 1204 | // The workflows a workspace runs in its repositories: its owners'. |
| 1205 | ("list_workspace_managed_workflows", Scope::WorkspaceRead), | |
| 1206 | ("create_workspace_managed_workflow", Scope::WorkspaceAdmin), | |
| 1207 | ("update_workspace_managed_workflow", Scope::WorkspaceAdmin), | |
| 1208 | ("delete_workspace_managed_workflow", Scope::WorkspaceAdmin), | |
| API and MCP for a workspace's personal access token rules, members' tokens and approvals | 1209 | // A workspace's rules for personal access tokens, and the members' |
| 1210 | // tokens that reach it: who has access. | |
| 1211 | ("get_token_policy", Scope::WorkspaceRead), | |
| 1212 | ("set_token_policy", Scope::WorkspaceAdmin), | |
| 1213 | ("list_member_tokens", Scope::AccessRead), | |
| 1214 | ("list_token_requests", Scope::AccessRead), | |
| 1215 | ("review_token_request", Scope::AccessAdmin), | |
| 1216 | ("revoke_member_token", Scope::AccessAdmin), | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1217 | // Memory. |
| Fixes from the page sweep | 1218 | ("recall", Scope::MemoryRead), |
| 1219 | ("get_context", Scope::MemoryRead), | |
| 1220 | ("remember", Scope::MemoryWrite), | |
| 1221 | // Who has access. | |
| 1222 | ("list_collaborators", Scope::AccessRead), | |
| 1223 | ("get_collaborator_permission", Scope::AccessRead), | |
| 1224 | ("list_repo_invitations", Scope::AccessRead), | |
| 1225 | ("list_outside_collaborators", Scope::AccessRead), | |
| 1226 | ("add_collaborator", Scope::AccessAdmin), | |
| 1227 | ("update_collaborator", Scope::AccessAdmin), | |
| 1228 | ("remove_collaborator", Scope::AccessAdmin), | |
| 1229 | ("revoke_repo_invitation", Scope::AccessAdmin), | |
| 1230 | ("set_base_permission", Scope::AccessAdmin), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 1231 | ("set_team_repo", Scope::AccessAdmin), |
| 1232 | ("remove_team_repo", Scope::AccessAdmin), | |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 1233 | // Deploy keys: each lets a machine reach one repository, so they |
| 1234 | // are part of who has access. | |
| 1235 | ("list_deploy_keys", Scope::AccessRead), | |
| 1236 | ("get_deploy_key", Scope::AccessRead), | |
| 1237 | ("create_deploy_key", Scope::AccessAdmin), | |
| 1238 | ("delete_deploy_key", Scope::AccessAdmin), | |
| Fixes from the page sweep | 1239 | // Webhooks. |
| 1240 | ("list_webhooks", Scope::WebhooksRead), | |
| 1241 | ("list_webhook_deliveries", Scope::WebhooksRead), | |
| 1242 | ("create_webhook", Scope::WebhooksAdmin), | |
| 1243 | ("update_webhook", Scope::WebhooksAdmin), | |
| 1244 | ("delete_webhook", Scope::WebhooksAdmin), | |
| 1245 | ("ping_webhook", Scope::WebhooksAdmin), | |
| 1246 | ("redeliver_webhook", Scope::WebhooksAdmin), | |
| 1247 | // Secrets and variables. | |
| 1248 | ("list_actions_secrets", Scope::SecretsRead), | |
| 1249 | ("list_actions_variables", Scope::SecretsRead), | |
| 1250 | ("set_actions_secret", Scope::SecretsAdmin), | |
| 1251 | ("delete_actions_secret", Scope::SecretsAdmin), | |
| 1252 | ("set_actions_variable", Scope::SecretsAdmin), | |
| 1253 | ("delete_actions_variable", Scope::SecretsAdmin), | |
| 1254 | // Self-hosted runners. | |
| 1255 | ("list_runners", Scope::RunnersRead), | |
| 1256 | ("list_runner_groups", Scope::RunnersRead), | |
| 1257 | ("get_runner_settings", Scope::RunnersRead), | |
| 1258 | ("create_runner_registration_token", Scope::RunnersAdmin), | |
| 1259 | ("remove_runner", Scope::RunnersAdmin), | |
| 1260 | ("create_runner_group", Scope::RunnersAdmin), | |
| 1261 | ("update_runner_group", Scope::RunnersAdmin), | |
| 1262 | ("delete_runner_group", Scope::RunnersAdmin), | |
| 1263 | ("update_runner_settings", Scope::RunnersAdmin), | |
| Merge packages: roles, Actions access, source label, soft delete, API | 1264 | // Packages: reading them, their versions and who may use them needs |
| 1265 | // `packages:read`; changing their settings, access and Manage Actions | |
| 1266 | // access `packages:write` (and the Admin role on the package, which the | |
| 1267 | // packages service checks); deleting and restoring packages and | |
| 1268 | // versions `packages:delete`, as the registries' own deletes do. | |
| 1269 | ("list_packages", Scope::PackagesRead), | |
| 1270 | ("get_package", Scope::PackagesRead), | |
| 1271 | ("list_package_versions", Scope::PackagesRead), | |
| 1272 | ("get_package_version", Scope::PackagesRead), | |
| 1273 | ("list_package_access", Scope::PackagesRead), | |
| 1274 | ("list_package_actions_access", Scope::PackagesRead), | |
| 1275 | ("update_package", Scope::PackagesWrite), | |
| 1276 | ("link_package", Scope::PackagesWrite), | |
| 1277 | ("unlink_package", Scope::PackagesWrite), | |
| 1278 | ("set_package_access", Scope::PackagesWrite), | |
| 1279 | ("remove_package_access", Scope::PackagesWrite), | |
| 1280 | ("set_package_actions_access", Scope::PackagesWrite), | |
| 1281 | ("remove_package_actions_access", Scope::PackagesWrite), | |
| 1282 | ("delete_package", Scope::PackagesDelete), | |
| 1283 | ("restore_package", Scope::PackagesDelete), | |
| 1284 | ("delete_package_version", Scope::PackagesDelete), | |
| 1285 | ("restore_package_version", Scope::PackagesDelete), | |
| Merge main into Artifacts Phase 2 | 1286 | // Artifacts mode's docs, slides, designs and dashboards (the `artifact` |
| 1287 | // MCP tool). Reading takes artifacts:read, making and changing them | |
| 1288 | // 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. | 1289 | // artifacts:admin. The artifacts service then checks the person's own role |
| Merge main into Artifacts Phase 2 | 1290 | // on each one. |
| 1291 | ("list_workspace_artifacts", Scope::ArtifactsRead), | |
| 1292 | ("search_workspace_artifacts", Scope::ArtifactsRead), | |
| 1293 | ("get_workspace_artifact", Scope::ArtifactsRead), | |
| 1294 | ("get_workspace_artifact_content", Scope::ArtifactsRead), | |
| 1295 | ("list_workspace_artifact_versions", Scope::ArtifactsRead), | |
| 1296 | ("get_workspace_artifact_access", Scope::ArtifactsRead), | |
| 1297 | ("list_workspace_artifact_templates", Scope::ArtifactsRead), | |
| 1298 | ("list_workspace_artifact_spaces", Scope::ArtifactsRead), | |
| 1299 | ("query_workspace_dataset", Scope::ArtifactsRead), | |
| 1300 | ("create_workspace_artifact", Scope::ArtifactsWrite), | |
| 1301 | ("update_workspace_artifact", Scope::ArtifactsWrite), | |
| 1302 | ("edit_workspace_artifact", Scope::ArtifactsWrite), | |
| 1303 | ("trash_workspace_artifact", Scope::ArtifactsWrite), | |
| 1304 | ("restore_workspace_artifact", Scope::ArtifactsWrite), | |
| 1305 | ("restore_workspace_artifact_version", Scope::ArtifactsWrite), | |
| 1306 | ("set_workspace_artifact_access", Scope::ArtifactsAdmin), | |
| 1307 | ("purge_workspace_artifact", Scope::ArtifactsAdmin), | |
| Fixes from the page sweep | 1308 | ]; |
| 1309 | ||
| 1310 | /// Operations any token may use: saying who it is. | |
| 1311 | pub const NO_SCOPE: &[&str] = &["whoami"]; | |
| 1312 | ||
| 1313 | /// The scope `operation` needs. `None` for one in [`NO_SCOPE`]; an | |
| 1314 | /// operation in neither list needs full access. | |
| 1315 | pub fn scope_for(operation: &str) -> Option<Scope> { | |
| 1316 | OPERATIONS | |
| 1317 | .iter() | |
| 1318 | .find(|(name, _)| *name == operation) | |
| 1319 | .map(|(_, scope)| *scope) | |
| 1320 | } | |
| 1321 | ||
| 1322 | /// What a token needs for `operation` with this input beyond its own | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1323 | /// scope: changing who has access, rerunning a workflow, and making a |
| Fixes from the page sweep | 1324 | /// repository public or private. |
| 1325 | pub fn extra_scopes(operation: &str, input: &serde_json::Value) -> Vec<Scope> { | |
| 1326 | let mut extra = Vec::new(); | |
| 1327 | // A workspace's base permission is who has access. | |
| 1328 | if operation == "update_workspace" && input.get("base_permission").is_some_and(|v| !v.is_null()) { | |
| 1329 | extra.push(Scope::AccessAdmin); | |
| 1330 | } | |
| Merge checks: statuses and check runs on every commit | 1331 | // Asking a g1t Actions job or run to run again reruns its workflow. |
| 1332 | if matches!(operation, "rerequest_check_run" | "rerequest_check_suite") | |
| 1333 | && input["id"].as_str().is_some_and(|id| id.starts_with("job_") || id.starts_with("run_")) | |
| 1334 | { | |
| 1335 | extra.push(Scope::WorkflowsWrite); | |
| 1336 | } | |
| Fixes from the page sweep | 1337 | 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())) { |
| 1338 | extra.push(Scope::RepoAdmin); | |
| 1339 | } | |
| 1340 | extra | |
| 1341 | } | |
| 1342 | ||
| 1343 | /// The scopes a call needs, its own first. | |
| 1344 | pub fn needed(operation: &str, input: &serde_json::Value) -> Vec<Scope> { | |
| 1345 | scope_for(operation) | |
| 1346 | .into_iter() | |
| 1347 | .chain(extra_scopes(operation, input)) | |
| 1348 | .collect() | |
| 1349 | } | |
| 1350 | ||
| 1351 | /// Whether `access` may use `operation` with `input`. The person's (or | |
| 1352 | /// workspace's) role is checked after this, by the service that owns what | |
| 1353 | /// was asked about. | |
| 1354 | pub fn decide(access: &TokenAccess, operation: &str, input: &serde_json::Value) -> Decision { | |
| 1355 | let rule = if access.legacy { "token:legacy" } else { "token:scope" }; | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1356 | // A workflow job may open or approve pull requests only where its |
| 1357 | // repository and workspace let it, as on GitHub. | |
| 1358 | if let Some(job) = &access.job | |
| 1359 | && !job.pull_requests | |
| 1360 | && (operation == "create_pull_request" || (operation == "review_pull_request" && input["verdict"].as_str() == Some("approve"))) | |
| 1361 | { | |
| 1362 | return Decision::deny( | |
| 1363 | "token:pull-requests", | |
| 1364 | "A workflow job cannot open or approve pull requests here: an admin can allow it under Settings, Actions.", | |
| 1365 | ); | |
| 1366 | } | |
| 1367 | if let Some(only) = access.repo.as_deref() | |
| 1368 | && !NO_SCOPE.contains(&operation) | |
| 1369 | { | |
| 1370 | match input["repo"].as_str() { | |
| 1371 | Some(repo) if access.reaches(repo) => {} | |
| 1372 | Some(repo) => { | |
| 1373 | return Decision::deny("token:repository", format!("This token is a workflow job's in {only}: it cannot reach {repo}.")); | |
| 1374 | } | |
| 1375 | None => { | |
| 1376 | return Decision::deny("token:repository", format!("This token is a workflow job's: it reaches only {only}, and {operation} is not about one repository.")); | |
| 1377 | } | |
| 1378 | } | |
| 1379 | } | |
| 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 | 1380 | // A token made for one workspace (or none) only reads outside it: |
| 1381 | // public repositories, as anyone may. Inside it, its repository | |
| 1382 | // selection is checked with its owner's role (`access::granted`). | |
| 1383 | if let Some(reach) = &access.reach | |
| Token reach: workflow_files scope, fine-grained reach, workspace token cap | 1384 | && let Some(repo) = input["repo"].as_str() |
| 1385 | && !NO_SCOPE.contains(&operation) | |
| 1386 | { | |
| 1387 | let namespace = repo.split('/').next().unwrap_or_default(); | |
| 1388 | let changes = needed(operation, input).iter().any(|scope| scope.level() != Level::Read); | |
| 1389 | 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 | 1390 | 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 | 1391 | return Decision::deny( |
| 1392 | "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 | 1393 | 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 | 1394 | ); |
| 1395 | } | |
| 1396 | } | |
| Fixes from the page sweep | 1397 | if access.scopes.is_some() { |
| 1398 | let known = NO_SCOPE.contains(&operation) || scope_for(operation).is_some(); | |
| 1399 | if !known { | |
| 1400 | return Decision::deny("token:scope", format!("This access token cannot use {operation}: it needs full access.")); | |
| 1401 | } | |
| 1402 | if let Some(missing) = needed(operation, input).into_iter().find(|scope| !access.allows(*scope)) { | |
| 1403 | return Decision::deny( | |
| 1404 | "token:scope", | |
| 1405 | format!("This access token needs the {} scope to use {operation}.", missing.as_str()), | |
| 1406 | ); | |
| 1407 | } | |
| 1408 | } | |
| 1409 | Decision::allow(rule) | |
| 1410 | } | |
| 1411 | ||
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1412 | /// Whether a token may use the repository `owner/name` at all: a refusal |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 1413 | /// for a workflow job's token or a deploy key in another repository, |
| 1414 | /// else `None`. Git and | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1415 | /// the package registries ask this before [`decide_git`] and |
| 1416 | /// [`decide_packages`]. | |
| 1417 | pub fn decide_repo(access: &TokenAccess, repo: &str) -> Option<Decision> { | |
| 1418 | let only = access.repo.as_deref()?; | |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 1419 | let why = if access.deploy_key.is_some() { |
| 1420 | format!("This deploy key is for {only}: it cannot reach {repo}.") | |
| 1421 | } else { | |
| 1422 | format!("This token is a workflow job's in {only}: it cannot reach {repo}.") | |
| 1423 | }; | |
| 1424 | (!access.reaches(repo)).then(|| Decision::deny("token:repository", why)) | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1425 | } |
| 1426 | ||
| Fixes from the page sweep | 1427 | /// Whether a token may clone or fetch (`write` false), or push to (`write` |
| 1428 | /// true), a repository with git. `public` is whether anyone may read it, | |
| 1429 | /// which needs no scope. | |
| 1430 | pub fn decide_git(access: &TokenAccess, write: bool, public: bool) -> Decision { | |
| 1431 | let needed = if write { Scope::CodeWrite } else { Scope::CodeRead }; | |
| 1432 | if !access.allows(needed) && (write || !public) { | |
| Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca | 1433 | if access.deploy_key.is_some() { |
| 1434 | return Decision::deny( | |
| 1435 | "token:scope", | |
| 1436 | "This deploy key is read-only. An admin of the repository can add it again with write access to push with it.", | |
| 1437 | ); | |
| 1438 | } | |
| Fixes from the page sweep | 1439 | return Decision::deny( |
| 1440 | "token:scope", | |
| 1441 | format!("This access token needs the {} scope to {} with git.", needed.as_str(), if write { "push" } else { "clone or fetch a private repository" }), | |
| 1442 | ); | |
| 1443 | } | |
| 1444 | Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" }) | |
| 1445 | } | |
| 1446 | ||
| 1447 | /// Whether a token may pull (`Level::Read`), push or publish | |
| 1448 | /// (`Level::Write`), or delete (`Level::Delete`) packages. `public` is | |
| 1449 | /// whether anyone may pull the package, which needs no scope. | |
| 1450 | pub fn decide_packages(access: &TokenAccess, level: Level, public: bool) -> Decision { | |
| 1451 | let (needed, doing) = match level { | |
| 1452 | Level::Read => (Scope::PackagesRead, "pull a private package"), | |
| 1453 | Level::Delete | Level::Admin => (Scope::PackagesDelete, "delete packages"), | |
| 1454 | Level::Write | Level::Run => (Scope::PackagesWrite, "push or publish packages"), | |
| 1455 | }; | |
| 1456 | if !access.allows(needed) && !(level == Level::Read && public) { | |
| 1457 | return Decision::deny( | |
| 1458 | "token:scope", | |
| 1459 | format!("This access token needs the {} scope to {doing}.", needed.as_str()), | |
| 1460 | ); | |
| 1461 | } | |
| 1462 | Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" }) | |
| 1463 | } | |
| 1464 | ||
| 1465 | #[cfg(test)] | |
| 1466 | mod tests { | |
| 1467 | use super::*; | |
| 1468 | use serde_json::json; | |
| 1469 | ||
| 1470 | fn token(scopes: &[Scope]) -> TokenAccess { | |
| 1471 | TokenAccess { | |
| 1472 | token_id: "tok_1".to_owned(), | |
| 1473 | scopes: Some(scopes.iter().map(|scope| scope.as_str().to_owned()).collect()), | |
| 1474 | legacy: false, | |
| Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens | 1475 | name: None, |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1476 | ..TokenAccess::default() |
| Fixes from the page sweep | 1477 | } |
| 1478 | } | |
| 1479 | ||
| 1480 | #[test] | |
| 1481 | fn every_scope_reads_back_and_belongs_to_a_resource() { | |
| 1482 | for scope in Scope::ALL { | |
| 1483 | assert_eq!(Scope::parse(scope.as_str()), Some(scope)); | |
| 1484 | assert!(scope.as_str().starts_with(scope.resource().as_str())); | |
| 1485 | assert!(scope.includes(scope)); | |
| 1486 | } | |
| 1487 | assert_eq!(Scope::parse(" Issues:Write "), Some(Scope::IssuesWrite)); | |
| 1488 | assert_eq!(Scope::parse("issues"), None); | |
| 1489 | } | |
| 1490 | ||
| 1491 | #[test] | |
| 1492 | fn a_higher_level_includes_the_lower_ones_of_its_resource_only() { | |
| 1493 | assert!(Scope::RepoAdmin.includes(Scope::RepoRead)); | |
| 1494 | assert!(Scope::RepoAdmin.includes(Scope::RepoWrite)); | |
| 1495 | assert!(Scope::IssuesWrite.includes(Scope::IssuesRead)); | |
| 1496 | assert!(!Scope::IssuesRead.includes(Scope::IssuesWrite)); | |
| 1497 | assert!(!Scope::RepoAdmin.includes(Scope::CodeWrite)); | |
| 1498 | assert!(!Scope::PullRequestsWrite.includes(Scope::IssuesWrite)); | |
| 1499 | } | |
| 1500 | ||
| 1501 | #[test] | |
| 1502 | fn operations_are_listed_once_and_never_also_free() { | |
| 1503 | let mut seen = std::collections::HashSet::new(); | |
| 1504 | for (name, _) in OPERATIONS { | |
| 1505 | assert!(seen.insert(*name), "{name} twice"); | |
| 1506 | assert!(!NO_SCOPE.contains(name), "{name}"); | |
| 1507 | } | |
| 1508 | } | |
| 1509 | ||
| 1510 | #[test] | |
| 1511 | fn scopes_are_parsed_from_oauth_text_leaving_out_unknown_ones() { | |
| 1512 | assert_eq!( | |
| 1513 | parse_scopes("issues:write repo:read,bogus:thing issues:write"), | |
| 1514 | vec![Scope::RepoRead, Scope::IssuesWrite] | |
| 1515 | ); | |
| 1516 | assert_eq!(scopes_text(&[Scope::RepoRead, Scope::IssuesWrite]), "repo:read issues:write"); | |
| 1517 | } | |
| 1518 | ||
| 1519 | #[test] | |
| 1520 | fn the_oauth_default_is_the_agent_preset_and_never_admin() { | |
| 1521 | let scopes = oauth_default(); | |
| 1522 | assert!(scopes.contains(&Scope::IssuesWrite)); | |
| 1523 | assert!(scopes.contains(&Scope::PullRequestsWrite)); | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1524 | assert!(!scopes.contains(&Scope::AgentsRun)); |
| Fixes from the page sweep | 1525 | 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 | 1526 | for read in Scope::ALL.into_iter().filter(|scope| scope.level() == Level::Read && scope.offered()) { |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 1527 | // Every read offered but the machines work runs on, and chat. |
| 1528 | assert_eq!(scopes.contains(&read), read != Scope::RunnersRead && read != Scope::ChatRead, "{read:?}"); | |
| Fixes from the page sweep | 1529 | } |
| 1530 | assert!(Preset::ReadOnly.scopes().unwrap().iter().all(|scope| scope.level() == Level::Read)); | |
| 1531 | assert_eq!(Preset::Full.scopes(), None); | |
| 1532 | } | |
| 1533 | ||
| 1534 | #[test] | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1535 | fn retired_scopes_parse_but_are_never_granted() { |
| 1536 | // Stored tokens and OAuth grants that hold them still read. | |
| 1537 | for text in ["agents:run", "models:read", "models:write"] { | |
| 1538 | let scope = Scope::parse(text).unwrap(); | |
| 1539 | assert!(scope.resource().retired() && !scope.offered(), "{text}"); | |
| 1540 | let back: Scope = serde_json::from_value(json!(text)).unwrap(); | |
| 1541 | assert_eq!(back, scope); | |
| 1542 | } | |
| 1543 | // But nothing hands them out, and asking for them gets the rest. | |
| 1544 | assert_eq!(parse_scopes("agents:run issues:read models:write"), vec![Scope::IssuesRead]); | |
| 1545 | assert!(!offered_scopes().iter().any(|scope| scope.resource().retired())); | |
| 1546 | assert!(!everything().iter().any(|scope| scope.resource().retired())); | |
| 1547 | for preset in Preset::ALL { | |
| 1548 | let scopes = preset.scopes().unwrap_or_default(); | |
| 1549 | assert!(!scopes.iter().any(|scope| scope.resource().retired()), "{}", preset.as_str()); | |
| 1550 | } | |
| 1551 | // And no operation needs them. | |
| 1552 | assert!(!OPERATIONS.iter().any(|(_, scope)| scope.resource().retired())); | |
| 1553 | } | |
| 1554 | ||
| 1555 | #[test] | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 1556 | fn billing_is_read_by_presets_and_changed_by_none_but_full_access() { |
| 1557 | assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::BillingRead)); | |
| 1558 | for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] { | |
| 1559 | assert!(!preset.scopes().unwrap().contains(&Scope::BillingWrite), "{}", preset.as_str()); | |
| 1560 | } | |
| 1561 | assert_eq!(scope_for("set_budget"), Some(Scope::BillingWrite)); | |
| 1562 | assert_eq!(scope_for("get_usage"), Some(Scope::BillingRead)); | |
| 1563 | let reader = token(&[Scope::BillingRead]); | |
| 1564 | assert!(decide(&reader, "list_invoices", &json!({})).allowed); | |
| 1565 | assert!(decide(&reader, "set_budget", &json!({})).reason.unwrap().contains("billing:write")); | |
| 1566 | } | |
| 1567 | ||
| 1568 | #[test] | |
| Merge main into Artifacts Phase 2 | 1569 | 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 | 1570 | for scope in [Scope::ArtifactsRead, Scope::ArtifactsWrite, Scope::ArtifactsAdmin] { |
| 1571 | assert_eq!(scope.resource(), Resource::Artifacts); | |
| Merge main into Artifacts Phase 2 | 1572 | assert!(scope.offered()); |
| 1573 | assert!(offered_scopes().contains(&scope)); | |
| 1574 | assert_eq!(parse_scopes(scope.as_str()), vec![scope]); | |
| 1575 | assert!(OPERATIONS.iter().any(|(_, needed)| *needed == scope), "{scope:?} gates nothing"); | |
| 1576 | } | |
| 1577 | // Reading them is a read like any other; changing them is chosen. | |
| 1578 | for preset in [Preset::ReadOnly, Preset::Agent] { | |
| 1579 | let scopes = preset.scopes().unwrap(); | |
| 1580 | assert!(scopes.contains(&Scope::ArtifactsRead), "{}", preset.as_str()); | |
| 1581 | 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 | 1582 | } |
| Merge main into Artifacts Phase 2 | 1583 | assert!(!Preset::Ci.scopes().unwrap().contains(&Scope::ArtifactsRead)); |
| 1584 | 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 | 1585 | assert!(Scope::ArtifactsAdmin.includes(Scope::ArtifactsWrite)); |
| 1586 | assert!(Scope::ArtifactsAdmin.dangerous()); | |
| Merge main into Artifacts Phase 2 | 1587 | 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 | 1588 | assert_eq!(Resource::Artifacts.group(), ResourceGroup::Workspace); |
| Merge main into Artifacts Phase 2 | 1589 | let asked = std::collections::BTreeMap::from([("artifacts".to_owned(), "write".to_owned())]); |
| 1590 | assert_eq!(resolve_permissions(&asked, true), Ok(vec![Scope::ArtifactsWrite])); | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1591 | assert!(offered_scopes().contains(&Scope::ArtifactsAdmin)); |
| Merge main into Artifacts Phase 2 | 1592 | // Sharing and deleting for good need admin; editing needs write. |
| 1593 | assert_eq!(scope_for("set_workspace_artifact_access"), Some(Scope::ArtifactsAdmin)); | |
| 1594 | assert_eq!(scope_for("purge_workspace_artifact"), Some(Scope::ArtifactsAdmin)); | |
| 1595 | assert_eq!(scope_for("edit_workspace_artifact"), Some(Scope::ArtifactsWrite)); | |
| 1596 | assert_eq!(scope_for("get_workspace_artifact_content"), Some(Scope::ArtifactsRead)); | |
| 1597 | let writer = token(&[Scope::ArtifactsWrite]); | |
| 1598 | assert!(decide(&writer, "edit_workspace_artifact", &json!({})).allowed); | |
| 1599 | assert!(decide(&writer, "list_workspace_artifacts", &json!({})).allowed, "write includes read"); | |
| 1600 | assert!(decide(&writer, "set_workspace_artifact_access", &json!({})).reason.unwrap().contains("artifacts:admin")); | |
| 1601 | // Workflow runs' artifacts are another thing, with their own scope. | |
| 1602 | let reader = token(&[Scope::ArtifactsRead]); | |
| 1603 | assert!(!decide(&reader, "list_artifacts", &json!({ "repo": "acme/web" })).allowed); | |
| 1604 | 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 | 1605 | } |
| 1606 | ||
| 1607 | #[test] | |
| The phone app: Chat in full with phone notifications, plus Code and Customize (#4) | 1608 | fn chat_is_a_persons_own_and_given_only_when_asked_for() { |
| 1609 | for scope in [Scope::ChatRead, Scope::ChatWrite] { | |
| 1610 | assert_eq!(scope.resource(), Resource::Chat); | |
| 1611 | assert!(!scope.dangerous()); | |
| 1612 | assert_eq!(Scope::parse(scope.as_str()), Some(scope)); | |
| 1613 | } | |
| 1614 | assert_eq!(Resource::Chat.label(), "Chat"); | |
| 1615 | assert_eq!(Resource::Chat.group(), ResourceGroup::Account); | |
| 1616 | assert!(Scope::ChatWrite.includes(Scope::ChatRead)); | |
| 1617 | assert!(!Scope::ChatRead.includes(Scope::ChatWrite)); | |
| 1618 | // No preset, and so no OAuth default, gives it. | |
| 1619 | for preset in Preset::ALL { | |
| 1620 | let scopes = preset.scopes().unwrap_or_default(); | |
| 1621 | assert!(!scopes.iter().any(|scope| scope.resource() == Resource::Chat), "{}", preset.as_str()); | |
| 1622 | } | |
| 1623 | assert!(!oauth_default().iter().any(|scope| scope.resource() == Resource::Chat)); | |
| 1624 | // An application that asks for it by name gets it; full access holds it. | |
| 1625 | assert_eq!(parse_scopes("chat:read chat:write"), vec![Scope::ChatRead, Scope::ChatWrite]); | |
| 1626 | assert!(offered_scopes().contains(&Scope::ChatWrite)); | |
| 1627 | assert!(everything().contains(&Scope::ChatWrite)); | |
| 1628 | assert!(TokenAccess::full().allows(Scope::ChatWrite)); | |
| 1629 | // Only a person's token may hold it. | |
| 1630 | let asked = std::collections::BTreeMap::from([("chat".to_owned(), "write".to_owned())]); | |
| 1631 | assert_eq!(resolve_permissions(&asked, true), Ok(vec![Scope::ChatWrite])); | |
| 1632 | assert!(resolve_permissions(&asked, false).is_err()); | |
| 1633 | } | |
| 1634 | ||
| 1635 | #[test] | |
| Merge checks: statuses and check runs on every commit | 1636 | fn checks_are_reported_with_checks_write_which_ci_gets() { |
| 1637 | assert_eq!(scope_for("create_check_run"), Some(Scope::ChecksWrite)); | |
| 1638 | assert_eq!(scope_for("create_commit_status"), Some(Scope::ChecksWrite)); | |
| 1639 | assert_eq!(scope_for("list_check_runs_for_ref"), Some(Scope::ChecksRead)); | |
| 1640 | let ci = Preset::Ci.scopes().unwrap(); | |
| 1641 | assert!(ci.contains(&Scope::ChecksWrite)); | |
| 1642 | assert!(!Preset::Agent.scopes().unwrap().contains(&Scope::ChecksWrite)); | |
| 1643 | let reporter = token(&[Scope::ChecksWrite]); | |
| 1644 | assert!(decide(&reporter, "update_check_run", &json!({ "id": "cr_1" })).allowed); | |
| 1645 | assert!(decide(&reporter, "rerequest_check_run", &json!({ "id": "cr_1" })).allowed); | |
| 1646 | // A g1t Actions job runs again as its workflow does. | |
| 1647 | let refused = decide(&reporter, "rerequest_check_run", &json!({ "id": "job_1" })); | |
| 1648 | assert!(refused.reason.unwrap().contains("workflows:write")); | |
| 1649 | } | |
| 1650 | ||
| 1651 | #[test] | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 1652 | fn a_job_token_reaches_its_repository_only() { |
| 1653 | let job = TokenAccess { | |
| 1654 | repo: Some("acme/web".into()), | |
| 1655 | job: Some(JobToken { run_id: "run_1".into(), job_id: "job_1".into(), pull_requests: false }), | |
| 1656 | ..token(&[Scope::RepoRead, Scope::IssuesWrite, Scope::IssuesRead, Scope::PullRequestsWrite]) | |
| 1657 | }; | |
| 1658 | assert!(decide(&job, "create_issue", &json!({ "repo": "acme/web" })).allowed); | |
| 1659 | assert!(decide(&job, "create_issue", &json!({ "repo": "Acme/Web" })).allowed, "names compare without case"); | |
| 1660 | let elsewhere = decide(&job, "create_issue", &json!({ "repo": "acme/api" })); | |
| 1661 | assert!(!elsewhere.allowed); | |
| 1662 | assert_eq!(elsewhere.rule, "token:repository"); | |
| 1663 | // Nothing beyond the one repository, a workspace's listing included. | |
| 1664 | assert!(!decide(&job, "list_repos", &json!({})).allowed); | |
| 1665 | assert!(decide(&job, "whoami", &json!({})).allowed); | |
| 1666 | // Its scopes still hold inside it. | |
| 1667 | assert!(!decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).allowed); | |
| 1668 | assert!(decide_repo(&job, "acme/web").is_none()); | |
| 1669 | assert!(!decide_repo(&job, "acme/api").unwrap().allowed); | |
| 1670 | assert!(decide_repo(&token(&[Scope::CodeRead]), "acme/api").is_none(), "other tokens reach what their owner can"); | |
| 1671 | // Opening and approving pull requests is off unless allowed. | |
| 1672 | assert_eq!(decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).rule, "token:pull-requests"); | |
| 1673 | assert!(!decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "approve" })).allowed); | |
| 1674 | assert!(decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "request_changes" })).allowed); | |
| 1675 | let allowed = TokenAccess { job: Some(JobToken { pull_requests: true, ..job.job.clone().unwrap() }), ..job.clone() }; | |
| 1676 | assert!(decide(&allowed, "create_pull_request", &json!({ "repo": "acme/web" })).allowed); | |
| 1677 | } | |
| 1678 | ||
| 1679 | #[test] | |
| Workflow files need workflow_files:write from a token; fine-grained permission table | 1680 | fn workflow_files_need_their_own_scope() { |
| 1681 | for path in [".g1t/workflows/ci.yml", ".github/workflows/deploy.yaml", "/.github/workflows/x.yml", ".GitHub/Workflows/ci.yml", ".github/workflows"] { | |
| 1682 | assert!(is_workflow_file(path), "{path}"); | |
| 1683 | } | |
| 1684 | for path in ["README.md", ".github/CODEOWNERS", ".github/workflowsx/ci.yml", "docs/.github/workflows/ci.yml", ".g1t/actions/ci.yml"] { | |
| 1685 | assert!(!is_workflow_file(path), "{path}"); | |
| 1686 | } | |
| 1687 | let code = token(&[Scope::CodeWrite]); | |
| 1688 | let refused = decide_workflow_files(Some(&code), ["README.md", ".github/workflows/ci.yml"]).unwrap(); | |
| 1689 | assert_eq!(refused.rule, "token:workflows"); | |
| 1690 | assert!(refused.reason.as_deref().unwrap().contains(".github/workflows/ci.yml")); | |
| 1691 | assert!(refused.reason.as_deref().unwrap().contains("workflow_files:write")); | |
| 1692 | assert!(decide_workflow_files(Some(&code), ["README.md"]).is_none()); | |
| 1693 | assert!(decide_workflow_files(Some(&token(&[Scope::CodeWrite, Scope::WorkflowFilesWrite])), [".g1t/workflows/ci.yml"]).is_none()); | |
| 1694 | assert!(decide_workflow_files(Some(&TokenAccess::full()), [".g1t/workflows/ci.yml"]).is_none(), "full access"); | |
| 1695 | assert!(decide_workflow_files(None, [".g1t/workflows/ci.yml"]).is_none(), "a signed-in person"); | |
| 1696 | // A job's token never may, as GITHUB_TOKEN never may. | |
| 1697 | let job = TokenAccess { job: Some(JobToken::default()), ..TokenAccess::full() }; | |
| 1698 | assert!(decide_workflow_files(Some(&job), [".g1t/workflows/ci.yml"]).unwrap().reason.unwrap().contains("job")); | |
| 1699 | // Nothing in a preset changes workflow files but full access. | |
| 1700 | for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] { | |
| 1701 | assert!(!preset.scopes().unwrap().contains(&Scope::WorkflowFilesWrite), "{}", preset.as_str()); | |
| 1702 | } | |
| 1703 | assert!(!Scope::WorkflowFilesWrite.includes(Scope::WorkflowsWrite) && !Scope::WorkflowsWrite.includes(Scope::WorkflowFilesWrite)); | |
| 1704 | } | |
| 1705 | ||
| 1706 | #[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 | 1707 | fn a_narrowed_token_only_reads_outside_its_workspace() { |
| 1708 | let reach = TokenReach { workspace: Some("acme".into()), repositories: RepositorySelection::All, repo_ids: Vec::new() }; | |
| 1709 | 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 | 1710 | assert!(decide(&fine, "create_issue", &json!({ "repo": "acme/web" })).allowed); |
| 1711 | assert!(decide(&fine, "create_issue", &json!({ "repo": "Acme/web" })).allowed); | |
| 1712 | let elsewhere = decide(&fine, "create_issue", &json!({ "repo": "globex/site" })); | |
| 1713 | assert_eq!(elsewhere.rule, "token:resource-owner"); | |
| 1714 | assert!(elsewhere.reason.unwrap().contains("acme")); | |
| 1715 | assert!(decide(&fine, "get_issue", &json!({ "repo": "globex/site" })).allowed, "public repositories elsewhere read"); | |
| 1716 | 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 | 1717 | let mine = TokenAccess { reach: Some(TokenReach::default()), ..token(&[Scope::IssuesWrite]) }; |
| Workflow files need workflow_files:write from a token; fine-grained permission table | 1718 | assert!(decide(&mine, "create_issue", &json!({ "repo": "acme/web" })).reason.unwrap().contains("your account")); |
| 1719 | 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 | 1720 | 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 | 1721 | 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 | 1722 | let public = TokenReach { repositories: RepositorySelection::Public, ..selected.clone() }; |
| Workflow files need workflow_files:write from a token; fine-grained permission table | 1723 | 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 | 1724 | 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 | 1725 | assert_eq!(RepositorySelection::parse("public_only"), Some(RepositorySelection::Public)); |
| 1726 | } | |
| 1727 | ||
| 1728 | #[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 | 1729 | fn permissions_are_scopes_read_per_resource() { |
| 1730 | let asked: std::collections::BTreeMap<String, String> = | |
| 1731 | [("issues", "write"), ("repo", "read"), ("code", "none"), ("packages", "delete")].iter().map(|(a, b)| ((*a).to_owned(), (*b).to_owned())).collect(); | |
| 1732 | let scopes = resolve_permissions(&asked, true).unwrap(); | |
| 1733 | assert_eq!(scopes, vec![Scope::RepoRead, Scope::PackagesDelete, Scope::IssuesWrite]); | |
| 1734 | let back = permissions_of(&scopes); | |
| 1735 | assert_eq!(back.get("issues").map(String::as_str), Some("write")); | |
| 1736 | assert_eq!(back.get("packages").map(String::as_str), Some("delete")); | |
| 1737 | assert!(!back.contains_key("code")); | |
| 1738 | // Lower levels held beside a higher one say nothing more. | |
| 1739 | 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 | 1740 | // 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 | 1741 | 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 | 1742 | assert_eq!(all.len(), Resource::ALL.into_iter().filter(|resource| resource.offered()).count()); |
| 1743 | 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 | 1744 | assert!(all.iter().any(|held| held.includes(scope)), "{scope:?}"); |
| 1745 | } | |
| 1746 | } | |
| 1747 | ||
| 1748 | #[test] | |
| 1749 | fn permissions_are_checked_by_name_level_and_owner() { | |
| 1750 | let one = |name: &str, level: &str| -> std::collections::BTreeMap<String, String> { [(name.to_owned(), level.to_owned())].into() }; | |
| 1751 | assert!(resolve_permissions(&one("wiki", "read"), true).unwrap_err().contains("wiki")); | |
| 1752 | assert!(resolve_permissions(&one("issues", "admin"), true).unwrap_err().contains("read, write")); | |
| 1753 | assert!(resolve_permissions(&one("workflow_files", "read"), true).is_err(), "workflow files are written only"); | |
| 1754 | assert!(resolve_permissions(&one("notifications", "read"), false).unwrap_err().contains("account")); | |
| 1755 | assert_eq!(resolve_permissions(&one("notifications", "read"), true).unwrap(), vec![Scope::NotificationsRead]); | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1756 | assert!(resolve_permissions(&one("agents", "run"), false).unwrap_err().contains("agents"), "retired"); |
| 1757 | assert!(resolve_permissions(&one("models", "read"), false).is_err(), "retired"); | |
| 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 | 1758 | for resource in Resource::ALL { |
| 1759 | assert_eq!(Resource::parse(resource.as_str()), Some(resource)); | |
| 1760 | assert!(!resource.scopes().is_empty()); | |
| 1761 | } | |
| 1762 | } | |
| 1763 | ||
| 1764 | #[test] | |
| Fixes from the page sweep | 1765 | fn a_legacy_token_can_do_everything() { |
| 1766 | let legacy = TokenAccess { legacy: true, ..TokenAccess::full() }; | |
| 1767 | for (operation, _) in OPERATIONS { | |
| 1768 | assert!(decide(&legacy, operation, &json!({})).allowed, "{operation}"); | |
| 1769 | } | |
| 1770 | assert_eq!(decide(&legacy, "delete_repo", &json!({})).rule, "token:legacy"); | |
| 1771 | } | |
| 1772 | ||
| 1773 | #[test] | |
| 1774 | fn a_missing_scope_is_named() { | |
| 1775 | let read = token(&[Scope::IssuesRead]); | |
| 1776 | assert!(decide(&read, "get_issue", &json!({})).allowed); | |
| 1777 | assert!(decide(&read, "whoami", &json!({})).allowed); | |
| 1778 | let refused = decide(&read, "create_issue", &json!({})); | |
| 1779 | assert!(!refused.allowed); | |
| 1780 | assert_eq!(refused.reason.as_deref(), Some("This access token needs the issues:write scope to use create_issue.")); | |
| 1781 | // An operation the table does not know needs full access. | |
| 1782 | assert!(!decide(&read, "something_new", &json!({})).allowed); | |
| 1783 | } | |
| 1784 | ||
| 1785 | #[test] | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1786 | fn making_a_repository_public_or_private_needs_repo_admin() { |
| Fixes from the page sweep | 1787 | let maintainer = token(&[Scope::RepoWrite]); |
| 1788 | assert!(decide(&maintainer, "update_repo", &json!({ "description": "x" })).allowed); | |
| 1789 | assert!(!decide(&maintainer, "update_repo", &json!({ "private": true })).allowed); | |
| 1790 | } | |
| 1791 | ||
| 1792 | #[test] | |
| 1793 | fn a_workspaces_base_permission_needs_access_admin_too() { | |
| 1794 | let admin = token(&[Scope::WorkspaceAdmin]); | |
| 1795 | assert!(decide(&admin, "update_workspace", &json!({ "name": "Acme" })).allowed); | |
| 1796 | let refused = decide(&admin, "update_workspace", &json!({ "name": "Acme", "base_permission": "read" })); | |
| 1797 | assert!(refused.reason.unwrap().contains("access:admin")); | |
| 1798 | let both = token(&[Scope::WorkspaceAdmin, Scope::AccessAdmin]); | |
| 1799 | assert!(decide(&both, "update_workspace", &json!({ "base_permission": "read" })).allowed); | |
| 1800 | assert!(!decide(&token(&[Scope::WorkspaceRead]), "update_workspace", &json!({ "name": "Acme" })).allowed); | |
| 1801 | } | |
| 1802 | ||
| 1803 | #[test] | |
| 1804 | fn git_push_needs_code_write_and_private_reads_need_code_read() { | |
| 1805 | let reader = token(&[Scope::CodeRead]); | |
| 1806 | assert!(decide_git(&reader, false, false).allowed); | |
| 1807 | let refused = decide_git(&reader, true, false); | |
| 1808 | assert!(!refused.allowed); | |
| 1809 | assert!(refused.reason.unwrap().contains("code:write")); | |
| 1810 | let issues = token(&[Scope::IssuesWrite]); | |
| 1811 | assert!(!decide_git(&issues, false, false).allowed); | |
| 1812 | assert!(decide_git(&issues, false, true).allowed, "public code needs no scope"); | |
| 1813 | assert!(!decide_git(&issues, true, true).allowed, "pushing to public code still needs code:write"); | |
| 1814 | let writer = token(&[Scope::CodeWrite]); | |
| 1815 | assert!(decide_git(&writer, true, false).allowed); | |
| 1816 | assert!(decide_git(&writer, false, false).allowed, "code:write includes code:read"); | |
| 1817 | assert!(decide_git(&TokenAccess::full(), true, false).allowed); | |
| 1818 | } | |
| 1819 | ||
| 1820 | #[test] | |
| 1821 | fn packages_need_their_own_scopes_and_public_pulls_none() { | |
| 1822 | let reader = token(&[Scope::PackagesRead]); | |
| 1823 | assert!(decide_packages(&reader, Level::Read, false).allowed); | |
| 1824 | assert!(!decide_packages(&reader, Level::Write, false).allowed); | |
| 1825 | let code = token(&[Scope::CodeWrite]); | |
| 1826 | assert!(!decide_packages(&code, Level::Read, false).allowed, "code scopes are not package scopes"); | |
| 1827 | assert!(decide_packages(&code, Level::Read, true).allowed, "public packages pull with any token"); | |
| 1828 | let writer = token(&[Scope::PackagesWrite]); | |
| 1829 | assert!(decide_packages(&writer, Level::Write, false).allowed); | |
| 1830 | assert!(decide_packages(&writer, Level::Read, false).allowed, "packages:write includes packages:read"); | |
| 1831 | let refused = decide_packages(&writer, Level::Delete, false); | |
| 1832 | assert!(refused.reason.unwrap().contains("packages:delete")); | |
| 1833 | assert!(decide_packages(&token(&[Scope::PackagesDelete]), Level::Write, false).allowed); | |
| 1834 | assert!(Scope::PackagesDelete.dangerous()); | |
| 1835 | // Tokens made before these scopes, and full-access ones, keep working. | |
| 1836 | let legacy = TokenAccess { legacy: true, ..TokenAccess::full() }; | |
| 1837 | assert!(decide_packages(&legacy, Level::Delete, false).allowed); | |
| 1838 | assert!(decide_packages(&TokenAccess::full(), Level::Write, false).allowed); | |
| 1839 | } | |
| 1840 | ||
| 1841 | #[test] | |
| 1842 | fn token_access_travels_as_json() { | |
| 1843 | let access = token(&[Scope::IssuesRead]); | |
| 1844 | let wire = serde_json::to_value(&access).unwrap(); | |
| 1845 | assert_eq!(wire["scopes"], json!(["issues:read"])); | |
| 1846 | assert!(wire.get("resources").is_none()); | |
| 1847 | let back: TokenAccess = serde_json::from_value(wire).unwrap(); | |
| 1848 | assert_eq!(back, access); | |
| 1849 | let full: TokenAccess = serde_json::from_value(json!({})).unwrap(); | |
| 1850 | assert!(full.is_full()); | |
| 1851 | // A reach written by an older version is ignored: a token reaches | |
| 1852 | // whatever its owner can. | |
| 1853 | let older: TokenAccess = serde_json::from_value(json!({ | |
| 1854 | "token_id": "tok_1", | |
| 1855 | "scopes": ["issues:read"], | |
| 1856 | "resources": { "kind": "repositories", "repositories": ["acme/rocket"] }, | |
| 1857 | })) | |
| 1858 | .unwrap(); | |
| 1859 | assert_eq!(older, access); | |
| Merge main into Artifacts Phase 2 | 1860 | // Using the website is off unless set, and said only when on. |
| 1861 | assert!(!access.website); | |
| 1862 | assert!(wire_of(&access).get("website").is_none()); | |
| 1863 | let website = TokenAccess { website: true, ..access }; | |
| 1864 | assert_eq!(wire_of(&website)["website"], json!(true)); | |
| 1865 | // Never part of full access. | |
| 1866 | assert!(!TokenAccess::full().website); | |
| Fixes from the page sweep | 1867 | } |
| 1868 | ||
| Merge main into Artifacts Phase 2 | 1869 | fn wire_of(access: &TokenAccess) -> serde_json::Value { |
| 1870 | serde_json::to_value(access).unwrap() | |
| 1871 | } | |
| 1872 | ||
| Fixes from the page sweep | 1873 | /// The site's copy of the table, `packages/contracts/src/scopes.ts`, |
| 1874 | /// lists the same scopes in the same order, the same operations with | |
| 1875 | /// the same scopes, and the same presets. | |
| 1876 | #[test] | |
| 1877 | fn the_typescript_mirror_has_the_same_table() { | |
| 1878 | let ts = include_str!("../../../packages/contracts/src/scopes.ts"); | |
| 1879 | let section = |start: &str| { | |
| 1880 | ts.split_once(start) | |
| 1881 | .and_then(|(_, rest)| rest.split_once("] as const")) | |
| 1882 | .map(|(table, _)| table) | |
| 1883 | .unwrap_or_else(|| panic!("{start} in scopes.ts")) | |
| 1884 | }; | |
| 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 | 1885 | let names = |table: &str| -> Vec<String> { |
| 1886 | section(table) | |
| 1887 | .lines() | |
| 1888 | .filter_map(|line| line.split_once("scope: \"").and_then(|(_, rest)| rest.split_once('"')).map(|(scope, _)| scope.to_owned())) | |
| 1889 | .collect() | |
| 1890 | }; | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1891 | // Offered scopes in `SCOPES`, retired ones in `RETIRED_SCOPES`, the |
| 1892 | // rest in `UPCOMING_SCOPES`. | |
| 1893 | let listed = |keep: fn(&Scope) -> bool| -> Vec<String> { Scope::ALL.iter().filter(|scope| keep(scope)).map(|scope| scope.as_str().to_owned()).collect() }; | |
| 1894 | assert_eq!(names("export const SCOPES = ["), listed(|scope| scope.offered())); | |
| 1895 | assert_eq!(names("export const UPCOMING_SCOPES"), listed(|scope| !scope.offered() && !scope.resource().retired())); | |
| 1896 | assert_eq!(names("export const RETIRED_SCOPES"), listed(|scope| scope.resource().retired())); | |
| Fixes from the page sweep | 1897 | let operations: Vec<(String, String)> = section("export const OPERATION_SCOPES = [") |
| 1898 | .lines() | |
| 1899 | .filter_map(|line| { | |
| 1900 | let mut quoted = line.split('"').skip(1).step_by(2); | |
| 1901 | Some((quoted.next()?.to_owned(), quoted.next()?.to_owned())) | |
| 1902 | }) | |
| 1903 | .collect(); | |
| 1904 | let expected: Vec<(String, String)> = OPERATIONS | |
| 1905 | .iter() | |
| 1906 | .map(|(name, scope)| ((*name).to_owned(), scope.as_str().to_owned())) | |
| 1907 | .collect(); | |
| 1908 | assert_eq!(operations, expected); | |
| 1909 | for preset in Preset::ALL { | |
| 1910 | let list = section(&format!("{}: [", preset.as_str())); | |
| 1911 | let mirrored: Vec<&str> = list | |
| 1912 | .split(',') | |
| 1913 | .map(|item| item.trim().trim_matches('"')) | |
| 1914 | .filter(|item| !item.is_empty()) | |
| 1915 | .collect(); | |
| 1916 | let expected: Vec<&str> = preset | |
| 1917 | .scopes() | |
| 1918 | .map(|scopes| scopes.iter().map(|scope| scope.as_str()).collect()) | |
| 1919 | .unwrap_or_else(|| vec!["*"]); | |
| 1920 | assert_eq!(mirrored, expected, "{}", preset.as_str()); | |
| 1921 | } | |
| 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 | 1922 | // Each resource with its group, in the same order: offered ones in |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1923 | // `SCOPE_RESOURCES`, retired ones in `RETIRED_RESOURCES`, the rest in |
| 1924 | // `UPCOMING_RESOURCES`. | |
| 1925 | let tables: [(&str, fn(Resource) -> bool); 3] = [ | |
| 1926 | ("export const SCOPE_RESOURCES", |resource| resource.offered()), | |
| 1927 | ("export const UPCOMING_RESOURCES", |resource| !resource.offered() && !resource.retired()), | |
| 1928 | ("export const RETIRED_RESOURCES", |resource| resource.retired()), | |
| 1929 | ]; | |
| 1930 | for (table, keep) in tables { | |
| 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 | 1931 | let resources = ts |
| 1932 | .split_once(table) | |
| 1933 | .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 | 1934 | ];")) |
| 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 | 1935 | .map(|(table, _)| table) |
| 1936 | .unwrap_or_else(|| panic!("{table} in scopes.ts")); | |
| 1937 | let rows: Vec<&str> = resources.lines().filter(|line| line.trim_start().starts_with("{ resource:")).collect(); | |
| The contracts no longer describe g1t's own agents, and workspaces can keep their AI providers. Every consumer had already stopped using them, so the TypeScript and Rust contracts drop the workspace-agent, ability, look, skill-library, context, runner-agent and mention modules, the chat and docs RPCs agents posted and read through, the folio agent methods, the agent_waiting notification, agent.asked and memory.changed events, AI credit, token and gateway billing types, model routes and sessions, team agents, agent tokens, plan, review, message and confidence types, command rules, cost caps and confidence rules. The agents:run and models:* scopes are no longer offered by any preset, OAuth default or permission form, but a stored token or grant that names them still reads; and the removed operations are gone from the scope table, so the API's check that every operation has exactly one scope is strict again. What stored data needs in order to parse (old run and credential kinds, an agent team lead, retired rules and webhook events, old guardrail and integration settings) still parses, and a run's token is still PrincipalKind::Agent, which now means a run's token and the @g1t rows written before. The customize, marketplace, skills and claude-plugin contracts stay only until the mobile app drops its Customize screens. | 1938 | let expected: Vec<Resource> = Resource::ALL.into_iter().filter(|resource| keep(*resource)).collect(); |
| 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 | 1939 | assert_eq!(rows.len(), expected.len(), "{table}"); |
| 1940 | for (row, resource) in rows.iter().zip(expected) { | |
| 1941 | assert!(row.contains(&format!("resource: \"{}\"", resource.as_str())), "{row}"); | |
| 1942 | assert!(row.contains(&format!("group: \"{}\"", resource.group().as_str())), "{row}"); | |
| 1943 | } | |
| 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 | 1944 | } |
| Fixes from the page sweep | 1945 | } |
| 1946 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.