Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
1,959 linesCodeBlameRaw

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

The Packages page names every registry it has1//! Scopes: what an access token may do on its owner's behalf.
2//!
3//! A personal access token, a workspace's token and an application signed
4//! in with OAuth each carry a set of scopes. A token reaches whatever the
5//! one it acts as can reach: a person's token, that person's workspaces and
6//! repositories; a workspace's token, that workspace. What a request may do
7//! is the intersection of two things: the role of whoever the token acts as
8//! (see [`crate::access`]) and the token's scopes.
9//!
10//! Each scope is a resource and a level, written `resource:level`, such as
11//! `issues:write`. A higher level of a resource includes the lower ones:
12//! `repo:admin` includes `repo:write`, which includes `repo:read`.
13//!
14//! This module is the one source of truth: the API (REST and MCP) and git
15//! enforce it, and identity stores it. `packages/contracts/src/scopes.ts`
16//! mirrors the table for the site; a test keeps the two the same.
17
18use serde::{Deserialize, Serialize};
19
20use crate::credentials::Decision;
21
22/// Something a token can be given access to.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
24pub enum Resource {
25 Account,
API: notifications over REST and MCP, with notifications scopes26 Notifications,
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,
The Packages page names every registry it has31 Workspace,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit32 Billing,
The Packages page names every registry it has33 Repo,
34 Code,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar35 Security,
The Packages page names every registry it has36 Packages,
37 Issues,
38 PullRequests,
39 Agents,
40 Workflows,
Token reach: workflow_files scope, fine-grained reach, workspace token cap41 WorkflowFiles,
Merge checks: statuses and check runs on every commit42 Checks,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9743 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,
The Packages page names every registry it has47 Memory,
48 Access,
49 Webhooks,
50 Secrets,
51 Runners,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens52 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 yet53 /// Artifacts mode's docs, slides, designs and dashboards (folios in
Merge main into Artifacts Phase 254 /// 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 yet56 Artifacts,
The Packages page names every registry it has57}
58
59impl 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] = [
The Packages page names every registry it has61 Resource::Repo,
62 Resource::Code,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar63 Resource::Security,
The Packages page names every registry it has64 Resource::Packages,
65 Resource::Issues,
66 Resource::PullRequests,
67 Resource::Agents,
68 Resource::Workflows,
Token reach: workflow_files scope, fine-grained reach, workspace token cap69 Resource::WorkflowFiles,
Merge checks: statuses and check runs on every commit70 Resource::Checks,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb9771 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,
The Packages page names every registry it has73 Resource::Memory,
74 Resource::Account,
API: notifications over REST and MCP, with notifications scopes75 Resource::Notifications,
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)76 Resource::Chat,
The Packages page names every registry it has77 Resource::Workspace,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit78 Resource::Billing,
The Packages page names every registry it has79 Resource::Access,
80 Resource::Webhooks,
81 Resource::Secrets,
82 Resource::Runners,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens83 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 yet84 Resource::Artifacts,
The Packages page names every registry it has85 ];
86
87 pub fn as_str(self) -> &'static str {
88 match self {
89 Resource::Account => "account",
API: notifications over REST and MCP, with notifications scopes90 Resource::Notifications => "notifications",
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)91 Resource::Chat => "chat",
The Packages page names every registry it has92 Resource::Workspace => "workspace",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit93 Resource::Billing => "billing",
The Packages page names every registry it has94 Resource::Repo => "repo",
95 Resource::Code => "code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar96 Resource::Security => "security",
The Packages page names every registry it has97 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 cap102 Resource::WorkflowFiles => "workflow_files",
Merge checks: statuses and check runs on every commit103 Resource::Checks => "checks",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97104 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",
The Packages page names every registry it has106 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 tokens111 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 yet112 Resource::Artifacts => "artifacts",
The Packages page names every registry it has113 }
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 scopes120 Resource::Notifications => "Notifications",
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)121 Resource::Chat => "Chat",
The Packages page names every registry it has122 Resource::Workspace => "Workspaces",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit123 Resource::Billing => "Billing",
The Packages page names every registry it has124 Resource::Repo => "Repositories",
125 Resource::Code => "Code",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar126 Resource::Security => "Security",
The Packages page names every registry it has127 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 cap132 Resource::WorkflowFiles => "Workflow files",
Merge checks: statuses and check runs on every commit133 Resource::Checks => "Checks and statuses",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97134 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",
The Packages page names every registry it has137 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 tokens141 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 yet142 Resource::Artifacts => "Artifacts",
The Packages page names every registry it has143 }
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 yet145
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 2147 /// 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 yet152 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 yet164 }
The Packages page names every registry it has165}
166
167/// How much of a resource.
168#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
169pub 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.
The Packages page names every registry it has173 Run,
174 /// Deleting what cannot be brought back, such as a package's versions.
175 Delete,
176 Admin,
177}
178
179impl 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)]
194pub 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 bar200 SecurityRead,
201 SecurityWrite,
The Packages page names every registry it has202 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.
The Packages page names every registry it has211 AgentsRun,
212 WorkflowsRead,
213 WorkflowsWrite,
Token reach: workflow_files scope, fine-grained reach, workspace token cap214 WorkflowFilesWrite,
Merge checks: statuses and check runs on every commit215 ChecksRead,
216 ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97217 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,
The Packages page names every registry it has221 MemoryRead,
222 MemoryWrite,
223 AccountRead,
224 AccountWrite,
API: notifications over REST and MCP, with notifications scopes225 NotificationsRead,
226 NotificationsWrite,
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)227 ChatRead,
228 ChatWrite,
The Packages page names every registry it has229 WorkspaceRead,
230 WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit231 BillingRead,
232 BillingWrite,
The Packages page names every registry it has233 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 tokens243 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 yet245 ArtifactsRead,
246 ArtifactsWrite,
247 ArtifactsAdmin,
The Packages page names every registry it has248}
249
250impl 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] = [
The Packages page names every registry it has253 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 bar258 Scope::SecurityRead,
259 Scope::SecurityWrite,
The Packages page names every registry it has260 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 cap270 Scope::WorkflowFilesWrite,
Merge checks: statuses and check runs on every commit271 Scope::ChecksRead,
272 Scope::ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97273 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,
The Packages page names every registry it has277 Scope::MemoryRead,
278 Scope::MemoryWrite,
279 Scope::AccountRead,
280 Scope::AccountWrite,
API: notifications over REST and MCP, with notifications scopes281 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,
The Packages page names every registry it has285 Scope::WorkspaceRead,
286 Scope::WorkspaceAdmin,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit287 Scope::BillingRead,
288 Scope::BillingWrite,
The Packages page names every registry it has289 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 tokens297 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 yet299 Scope::ArtifactsRead,
300 Scope::ArtifactsWrite,
301 Scope::ArtifactsAdmin,
The Packages page names every registry it has302 ];
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 bar311 Scope::SecurityRead => "security:read",
312 Scope::SecurityWrite => "security:write",
The Packages page names every registry it has313 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 cap323 Scope::WorkflowFilesWrite => "workflow_files:write",
Merge checks: statuses and check runs on every commit324 Scope::ChecksRead => "checks:read",
325 Scope::ChecksWrite => "checks:write",
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97326 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",
The Packages page names every registry it has330 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 scopes334 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",
The Packages page names every registry it has338 Scope::WorkspaceRead => "workspace:read",
339 Scope::WorkspaceAdmin => "workspace:admin",
Usage, Billing settings and prepaid AI credit; fixes from the UX audit340 Scope::BillingRead => "billing:read",
341 Scope::BillingWrite => "billing:write",
The Packages page names every registry it has342 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 tokens350 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 yet352 Scope::ArtifactsRead => "artifacts:read",
353 Scope::ArtifactsWrite => "artifacts:write",
354 Scope::ArtifactsAdmin => "artifacts:admin",
The Packages page names every registry it has355 }
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-a69aeabc4b0deeb97396 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 merge398 Scope::RepoAdmin => "Rename, archive, transfer, delete or change who can see a repository, change its rulesets, and dismiss security alerts",
The Packages page names every registry it has399 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 bar401 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",
The Packages page names every registry it has403 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, API405 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",
The Packages page names every registry it has407 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",
The Packages page names every registry it has411 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 cap413 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 commit414 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-a69aeabc4b0deeb97416 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-a69aeabc4b0deeb97422 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 scopes424 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 merge429 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",
The Packages page names every registry it has432 Scope::AccessRead => "See who has access to repositories",
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar433 Scope::AccessAdmin => "Give and take away access to repositories, a team's included",
The Packages page names every registry it has434 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",
The Packages page names every registry it has439 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 yet442 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",
The Packages page names every registry it has445 }
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 yet447
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.
455pub fn offered_scopes() -> Vec<Scope> {
456 Scope::ALL.into_iter().filter(|scope| scope.offered()).collect()
The Packages page names every registry it has457}
458
459impl 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
465impl<'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.
475pub 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 yet479 .filter(|scope| scope.offered())
The Packages page names every registry it has480 .collect();
481 normalize(&mut scopes);
482 scopes
483}
484
485/// In table order, without repeats.
486pub 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.
492pub 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 managers496/// 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")]
499pub 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
508impl 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
520impl 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 yet524 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 managers525 _ => 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.
550pub 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.
562pub 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 yet563 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 managers564}
565
566/// Scopes as permissions: each resource held, by name, at its highest
567/// level held.
568pub 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.
578pub 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 yet581 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 managers582 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
The Packages page names every registry it has600/// What a token stores for full access, which is not a scope a client can
601/// ask for by name.
602pub const FULL_ACCESS: &str = "*";
603
604/// Starting points for choosing scopes.
605#[derive(Clone, Copy, Debug, PartialEq, Eq)]
606pub enum Preset {
607 ReadOnly,
608 Agent,
609 Ci,
610 Full,
611}
612
613impl 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 };
The Packages page names every registry it has643 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 scopes649 // And answering what needs the person it works for: marking
650 // it done, subscribing, watching.
The Packages page names every registry it has651 scopes.extend([
652 Scope::CodeWrite,
653 Scope::IssuesWrite,
654 Scope::PullRequestsWrite,
655 Scope::MemoryWrite,
API: notifications over REST and MCP, with notifications scopes656 Scope::NotificationsWrite,
The Packages page names every registry it has657 ]);
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 commit669 Scope::ChecksRead,
670 Scope::ChecksWrite,
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97671 Scope::DeploymentsRead,
672 Scope::DeploymentsWrite,
The Packages page names every registry it has673 ]),
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.
681pub 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)]
689pub 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 tokens711 /// 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 managers715 /// 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 cap724 /// 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 2733 /// 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 cap739}
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 managers741/// Which repositories a token reaches in the workspace it is made for.
Token reach: workflow_files scope, fine-grained reach, workspace token cap742#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
743#[serde(rename_all = "snake_case")]
744pub 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 managers752 /// With no workspace: the owner's account and public repositories only.
Token reach: workflow_files scope, fine-grained reach, workspace token cap753 Public,
The Packages page names every registry it has754}
755
Token reach: workflow_files scope, fine-grained reach, workspace token cap756impl 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 managers775/// What a narrowed token reaches, as identity resolves it on each use.
Token reach: workflow_files scope, fine-grained reach, workspace token cap776#[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 managers777pub 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 cap781 #[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 managers790impl TokenReach {
Token reach: workflow_files scope, fine-grained reach, workspace token cap791 /// 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 managers807 /// Whether it is made for the workspace `slug`.
Token reach: workflow_files scope, fine-grained reach, workspace token cap808 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.
816pub const WORKFLOW_DIRS: [&str; 2] = [".g1t/workflows/", ".github/workflows/"];
817
818/// Whether `path` is a workflow file, or a file in one's directory.
819pub 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.
829pub 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)]
848pub 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
The Packages page names every registry it has860impl 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
The Packages page names every registry it has872 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 cap889
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 managers891 /// 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 cap893 /// 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 managers895 self.reach.as_ref().is_none_or(|reach| reach.covers(repo_id, namespace))
Token reach: workflow_files scope, fine-grained reach, workspace token cap896 }
The Packages page names every registry it has897}
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.
902pub 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),
The Packages page names every registry it has907 ("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),
The Packages page names every registry it has912 ("list_my_repo_invitations", Scope::AccountRead),
913 ("accept_repo_invitation", Scope::AccountWrite),
914 ("decline_repo_invitation", Scope::AccountWrite),
API: pinned projects over REST and MCP915 // 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-a69aeabc4b0deeb97918 // 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 MCP923 ("unpin_project", Scope::AccountWrite),
924 ("reorder_pinned_projects", Scope::AccountWrite),
API: notifications over REST and MCP, with notifications scopes925 // 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),
The Packages page names every registry it has940 // 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),
The Packages page names every registry it has944 ("update_workspace", Scope::WorkspaceAdmin),
Merge main (membership, two-factor, GitHub repo roles) into tokens945 // 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),
The Packages page names every registry it has951 ("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 providers956 ("update_integration", Scope::WorkspaceAdmin),
The Packages page names every registry it has957 ("disconnect_integration", Scope::WorkspaceAdmin),
958 ("test_integration", Scope::WorkspaceAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar959 // 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 merge968 ("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 bar974 ("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 audit980 ("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),
The Packages page names every registry it has985 // 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-a69aeabc4b0deeb97992 // Projects follow their repositories.
993 ("list_projects", Scope::RepoRead),
994 ("get_project", Scope::RepoRead),
The Packages page names every registry it has995 ("search", Scope::RepoRead),
996 ("list_events", Scope::RepoRead),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb97997 // 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),
The Packages page names every registry it has1010 ("list_labels", Scope::RepoRead),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1011 ("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),
The Packages page names every registry it has1020 ("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 bar1024 ("get_codeowners_errors", Scope::RepoRead),
The Packages page names every registry it has1025 ("create_repo", Scope::RepoWrite),
1026 ("update_repo", Scope::RepoWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971027 ("update_project", Scope::RepoWrite),
The Packages page names every registry it has1028 ("update_repo_settings", Scope::RepoWrite),
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge1029 // 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),
The Packages page names every registry it has1038 ("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),
The Packages page names every registry it has1041 ("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 bar1052 // 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-7nm8r91071 ("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 bar1074 ("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-7nm8r91087 ("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.
The Packages page names every registry it has1093 ("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 bar1097 ("list_issue_labels", Scope::IssuesRead),
1098 ("add_issue_labels", Scope::IssuesWrite),
1099 ("set_issue_labels", Scope::IssuesWrite),
1100 ("remove_issue_labels", Scope::IssuesWrite),
The Packages page names every registry it has1101 ("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 timeouts1104 ("edit_comment", Scope::IssuesWrite),
1105 ("delete_comment", Scope::IssuesWrite),
The Packages page names every registry it has1106 ("import_issue", Scope::IssuesWrite),
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'1107 // Coordinating work on issues: claims, and the context to pick one up.
1108 ("list_claims", Scope::IssuesRead),
1109 ("get_work", Scope::IssuesRead),
1110 ("claim_issue", Scope::IssuesWrite),
1111 ("release_claim", Scope::IssuesWrite),
1112 ("hand_off_issue", Scope::IssuesWrite),
The Packages page names every registry it has1113 // Pull requests.
1114 ("list_pull_requests", Scope::PullRequestsRead),
1115 ("get_pull_request", Scope::PullRequestsRead),
1116 ("get_pull_request_changes", Scope::PullRequestsRead),
1117 ("read_session", Scope::PullRequestsRead),
1118 ("get_merge_queue", Scope::PullRequestsRead),
Merge branch 'worktree-agent-ab8033ab973f8d99a'1119 ("list_decisions", Scope::PullRequestsRead),
The Packages page names every registry it has1120 ("create_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1121 ("update_pull_request", Scope::PullRequestsWrite),
The Packages page names every registry it has1122 ("record_session", Scope::PullRequestsWrite),
Merge branch 'worktree-agent-ab8033ab973f8d99a'1123 ("record_decision", Scope::PullRequestsWrite),
The Packages page names every registry it has1124 ("mark_pull_request_ready", Scope::PullRequestsWrite),
1125 ("close_pull_request", Scope::PullRequestsWrite),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1126 ("reopen_pull_request", Scope::PullRequestsWrite),
1127 ("convert_pull_request_to_draft", Scope::PullRequestsWrite),
The Packages page names every registry it has1128 ("review_pull_request", Scope::PullRequestsWrite),
1129 ("merge_pull_request", Scope::PullRequestsWrite),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1130 ("request_reviewers", Scope::PullRequestsWrite),
1131 ("remove_requested_reviewers", Scope::PullRequestsWrite),
Merge branch 'worktree-agent-a6ec0a89d7fe7a9ae'1132 // Harness sessions, which record how pull requests are made.
1133 ("get_session", Scope::PullRequestsRead),
1134 ("list_sessions", Scope::PullRequestsRead),
1135 ("start_session", Scope::PullRequestsWrite),
1136 ("update_session", Scope::PullRequestsWrite),
1137 ("end_session", Scope::PullRequestsWrite),
1138 ("record_session_entries", Scope::PullRequestsWrite),
The Packages page names every registry it has1139 // Workflows.
1140 ("list_workflows", Scope::WorkflowsRead),
1141 ("list_workflow_runs", Scope::WorkflowsRead),
1142 ("get_workflow_run", Scope::WorkflowsRead),
1143 ("get_job_logs", Scope::WorkflowsRead),
1144 ("dispatch_workflow", Scope::WorkflowsWrite),
1145 ("cancel_workflow_run", Scope::WorkflowsWrite),
1146 ("rerun_workflow_run", Scope::WorkflowsWrite),
1147 ("update_workflow", Scope::WorkflowsWrite),
Actions: OIDC tokens, the toolkit's cache and artifact services, and artifacts in R21148 ("list_artifacts", Scope::WorkflowsRead),
1149 ("list_workflow_run_artifacts", Scope::WorkflowsRead),
1150 ("get_artifact", Scope::WorkflowsRead),
1151 ("download_artifact", Scope::WorkflowsRead),
1152 ("get_artifact_retention", Scope::WorkflowsRead),
1153 ("delete_artifact", Scope::WorkflowsWrite),
1154 ("set_artifact_retention", Scope::WorkflowsWrite),
Merge checks: statuses and check runs on every commit1155 // Checks: statuses, check runs and check suites on commits.
1156 ("list_commit_statuses", Scope::ChecksRead),
1157 ("get_combined_status", Scope::ChecksRead),
1158 ("list_check_runs_for_ref", Scope::ChecksRead),
1159 ("get_check_run", Scope::ChecksRead),
1160 ("list_check_run_annotations", Scope::ChecksRead),
1161 ("list_check_suites_for_ref", Scope::ChecksRead),
1162 ("get_check_suite", Scope::ChecksRead),
1163 ("create_commit_status", Scope::ChecksWrite),
1164 ("create_check_run", Scope::ChecksWrite),
1165 ("update_check_run", Scope::ChecksWrite),
1166 ("rerequest_check_run", Scope::ChecksWrite),
1167 ("rerequest_check_suite", Scope::ChecksWrite),
Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971168 // Deployments, wherever they run: reading them, and reporting them.
1169 ("list_deployments", Scope::DeploymentsRead),
1170 ("get_deployment", Scope::DeploymentsRead),
1171 ("list_deployment_statuses", Scope::DeploymentsRead),
1172 ("list_environments", Scope::DeploymentsRead),
1173 ("get_environment", Scope::DeploymentsRead),
1174 ("create_deployment", Scope::DeploymentsWrite),
1175 ("create_deployment_status", Scope::DeploymentsWrite),
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91176 ("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.1177 // Observability: what a project's software did, and how it is collected.
1178 ("get_observability_overview", Scope::ObservabilityRead),
1179 ("list_requests", Scope::ObservabilityRead),
1180 ("get_request", Scope::ObservabilityRead),
1181 ("list_logs", Scope::ObservabilityRead),
1182 ("list_traces", Scope::ObservabilityRead),
1183 ("get_trace", Scope::ObservabilityRead),
1184 ("list_errors", Scope::ObservabilityRead),
1185 ("list_metrics", Scope::ObservabilityRead),
1186 ("get_metric", Scope::ObservabilityRead),
1187 ("get_observability_settings", Scope::ObservabilityRead),
1188 ("list_ingest_tokens", Scope::ObservabilityRead),
1189 ("list_telemetry_exports", Scope::ObservabilityRead),
1190 ("get_observability_usage", Scope::ObservabilityRead),
1191 ("update_error", Scope::ObservabilityWrite),
1192 ("update_observability_settings", Scope::ObservabilityWrite),
1193 ("create_ingest_token", Scope::ObservabilityWrite),
1194 ("delete_ingest_token", Scope::ObservabilityWrite),
1195 ("create_telemetry_export", Scope::ObservabilityWrite),
1196 ("update_telemetry_export", Scope::ObservabilityWrite),
1197 ("delete_telemetry_export", Scope::ObservabilityWrite),
Merge branch 'worktree-agent-a3abfcce648e87dca'1198 // What keeps runs safe: the runs environments hold and reviewing them,
1199 // approving a pull request's run, and a repository's own rules for
1200 // its environments and tokens, which are an admin's.
1201 ("get_pending_deployments", Scope::WorkflowsRead),
1202 ("review_pending_deployments", Scope::WorkflowsWrite),
1203 ("approve_workflow_run", Scope::WorkflowsWrite),
1204 ("get_workflow_permissions", Scope::RepoRead),
1205 ("get_fork_pr_approval", Scope::RepoRead),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1206 ("get_actions_access", Scope::RepoRead),
Merge branch 'worktree-agent-a3abfcce648e87dca'1207 ("update_environment", Scope::RepoAdmin),
1208 ("delete_environment", Scope::RepoAdmin),
1209 ("set_workflow_permissions", Scope::RepoAdmin),
1210 ("set_fork_pr_approval", Scope::RepoAdmin),
Merge Actions: cross-repo workflows and actions, release and deployment triggers, step timeouts1211 ("set_actions_access", Scope::RepoAdmin),
Merge branch 'worktree-agent-a3abfcce648e87dca'1212 // Starting workflows from outside, as a push would.
1213 ("create_repository_dispatch", Scope::CodeWrite),
1214 // A workspace's policy for its repositories' tokens.
1215 ("get_workspace_workflow_permissions", Scope::WorkspaceRead),
1216 ("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.1217 // The workflows a workspace runs in its repositories: its owners'.
1218 ("list_workspace_managed_workflows", Scope::WorkspaceRead),
1219 ("create_workspace_managed_workflow", Scope::WorkspaceAdmin),
1220 ("update_workspace_managed_workflow", Scope::WorkspaceAdmin),
1221 ("delete_workspace_managed_workflow", Scope::WorkspaceAdmin),
API and MCP for a workspace's personal access token rules, members' tokens and approvals1222 // A workspace's rules for personal access tokens, and the members'
1223 // tokens that reach it: who has access.
1224 ("get_token_policy", Scope::WorkspaceRead),
1225 ("set_token_policy", Scope::WorkspaceAdmin),
1226 ("list_member_tokens", Scope::AccessRead),
1227 ("list_token_requests", Scope::AccessRead),
1228 ("review_token_request", Scope::AccessAdmin),
1229 ("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.1230 // Memory.
The Packages page names every registry it has1231 ("recall", Scope::MemoryRead),
1232 ("get_context", Scope::MemoryRead),
1233 ("remember", Scope::MemoryWrite),
1234 // Who has access.
1235 ("list_collaborators", Scope::AccessRead),
1236 ("get_collaborator_permission", Scope::AccessRead),
1237 ("list_repo_invitations", Scope::AccessRead),
1238 ("list_outside_collaborators", Scope::AccessRead),
1239 ("add_collaborator", Scope::AccessAdmin),
1240 ("update_collaborator", Scope::AccessAdmin),
1241 ("remove_collaborator", Scope::AccessAdmin),
1242 ("revoke_repo_invitation", Scope::AccessAdmin),
1243 ("set_base_permission", Scope::AccessAdmin),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1244 ("set_team_repo", Scope::AccessAdmin),
1245 ("remove_team_repo", Scope::AccessAdmin),
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1246 // Deploy keys: each lets a machine reach one repository, so they
1247 // are part of who has access.
1248 ("list_deploy_keys", Scope::AccessRead),
1249 ("get_deploy_key", Scope::AccessRead),
1250 ("create_deploy_key", Scope::AccessAdmin),
1251 ("delete_deploy_key", Scope::AccessAdmin),
The Packages page names every registry it has1252 // Webhooks.
1253 ("list_webhooks", Scope::WebhooksRead),
1254 ("list_webhook_deliveries", Scope::WebhooksRead),
1255 ("create_webhook", Scope::WebhooksAdmin),
1256 ("update_webhook", Scope::WebhooksAdmin),
1257 ("delete_webhook", Scope::WebhooksAdmin),
1258 ("ping_webhook", Scope::WebhooksAdmin),
1259 ("redeliver_webhook", Scope::WebhooksAdmin),
1260 // Secrets and variables.
1261 ("list_actions_secrets", Scope::SecretsRead),
1262 ("list_actions_variables", Scope::SecretsRead),
1263 ("set_actions_secret", Scope::SecretsAdmin),
1264 ("delete_actions_secret", Scope::SecretsAdmin),
1265 ("set_actions_variable", Scope::SecretsAdmin),
1266 ("delete_actions_variable", Scope::SecretsAdmin),
1267 // Self-hosted runners.
1268 ("list_runners", Scope::RunnersRead),
1269 ("list_runner_groups", Scope::RunnersRead),
1270 ("get_runner_settings", Scope::RunnersRead),
1271 ("create_runner_registration_token", Scope::RunnersAdmin),
1272 ("remove_runner", Scope::RunnersAdmin),
1273 ("create_runner_group", Scope::RunnersAdmin),
1274 ("update_runner_group", Scope::RunnersAdmin),
1275 ("delete_runner_group", Scope::RunnersAdmin),
1276 ("update_runner_settings", Scope::RunnersAdmin),
Merge packages: roles, Actions access, source label, soft delete, API1277 // Packages: reading them, their versions and who may use them needs
1278 // `packages:read`; changing their settings, access and Manage Actions
1279 // access `packages:write` (and the Admin role on the package, which the
1280 // packages service checks); deleting and restoring packages and
1281 // versions `packages:delete`, as the registries' own deletes do.
1282 ("list_packages", Scope::PackagesRead),
1283 ("get_package", Scope::PackagesRead),
1284 ("list_package_versions", Scope::PackagesRead),
1285 ("get_package_version", Scope::PackagesRead),
1286 ("list_package_access", Scope::PackagesRead),
1287 ("list_package_actions_access", Scope::PackagesRead),
1288 ("update_package", Scope::PackagesWrite),
1289 ("link_package", Scope::PackagesWrite),
1290 ("unlink_package", Scope::PackagesWrite),
1291 ("set_package_access", Scope::PackagesWrite),
1292 ("remove_package_access", Scope::PackagesWrite),
1293 ("set_package_actions_access", Scope::PackagesWrite),
1294 ("remove_package_actions_access", Scope::PackagesWrite),
1295 ("delete_package", Scope::PackagesDelete),
1296 ("restore_package", Scope::PackagesDelete),
1297 ("delete_package_version", Scope::PackagesDelete),
1298 ("restore_package_version", Scope::PackagesDelete),
Merge main into Artifacts Phase 21299 // Artifacts mode's docs, slides, designs and dashboards (the `artifact`
1300 // MCP tool). Reading takes artifacts:read, making and changing them
1301 // 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.1302 // artifacts:admin. The artifacts service then checks the person's own role
Merge main into Artifacts Phase 21303 // on each one.
1304 ("list_workspace_artifacts", Scope::ArtifactsRead),
1305 ("search_workspace_artifacts", Scope::ArtifactsRead),
1306 ("get_workspace_artifact", Scope::ArtifactsRead),
1307 ("get_workspace_artifact_content", Scope::ArtifactsRead),
1308 ("list_workspace_artifact_versions", Scope::ArtifactsRead),
1309 ("get_workspace_artifact_access", Scope::ArtifactsRead),
1310 ("list_workspace_artifact_templates", Scope::ArtifactsRead),
1311 ("list_workspace_artifact_spaces", Scope::ArtifactsRead),
1312 ("query_workspace_dataset", Scope::ArtifactsRead),
1313 ("create_workspace_artifact", Scope::ArtifactsWrite),
1314 ("update_workspace_artifact", Scope::ArtifactsWrite),
1315 ("edit_workspace_artifact", Scope::ArtifactsWrite),
1316 ("trash_workspace_artifact", Scope::ArtifactsWrite),
1317 ("restore_workspace_artifact", Scope::ArtifactsWrite),
1318 ("restore_workspace_artifact_version", Scope::ArtifactsWrite),
1319 ("set_workspace_artifact_access", Scope::ArtifactsAdmin),
1320 ("purge_workspace_artifact", Scope::ArtifactsAdmin),
The Packages page names every registry it has1321];
1322
1323/// Operations any token may use: saying who it is.
1324pub const NO_SCOPE: &[&str] = &["whoami"];
1325
1326/// The scope `operation` needs. `None` for one in [`NO_SCOPE`]; an
1327/// operation in neither list needs full access.
1328pub fn scope_for(operation: &str) -> Option<Scope> {
1329 OPERATIONS
1330 .iter()
1331 .find(|(name, _)| *name == operation)
1332 .map(|(_, scope)| *scope)
1333}
1334
1335/// 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.1336/// scope: changing who has access, rerunning a workflow, and making a
The Packages page names every registry it has1337/// repository public or private.
1338pub fn extra_scopes(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
1339 let mut extra = Vec::new();
1340 // A workspace's base permission is who has access.
1341 if operation == "update_workspace" && input.get("base_permission").is_some_and(|v| !v.is_null()) {
1342 extra.push(Scope::AccessAdmin);
1343 }
Merge checks: statuses and check runs on every commit1344 // Asking a g1t Actions job or run to run again reruns its workflow.
1345 if matches!(operation, "rerequest_check_run" | "rerequest_check_suite")
1346 && input["id"].as_str().is_some_and(|id| id.starts_with("job_") || id.starts_with("run_"))
1347 {
1348 extra.push(Scope::WorkflowsWrite);
1349 }
The Packages page names every registry it has1350 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())) {
1351 extra.push(Scope::RepoAdmin);
1352 }
1353 extra
1354}
1355
1356/// The scopes a call needs, its own first.
1357pub fn needed(operation: &str, input: &serde_json::Value) -> Vec<Scope> {
1358 scope_for(operation)
1359 .into_iter()
1360 .chain(extra_scopes(operation, input))
1361 .collect()
1362}
1363
1364/// Whether `access` may use `operation` with `input`. The person's (or
1365/// workspace's) role is checked after this, by the service that owns what
1366/// was asked about.
1367pub fn decide(access: &TokenAccess, operation: &str, input: &serde_json::Value) -> Decision {
1368 let rule = if access.legacy { "token:legacy" } else { "token:scope" };
Merge branch 'worktree-agent-a3abfcce648e87dca'1369 // A workflow job may open or approve pull requests only where its
1370 // repository and workspace let it, as on GitHub.
1371 if let Some(job) = &access.job
1372 && !job.pull_requests
1373 && (operation == "create_pull_request" || (operation == "review_pull_request" && input["verdict"].as_str() == Some("approve")))
1374 {
1375 return Decision::deny(
1376 "token:pull-requests",
1377 "A workflow job cannot open or approve pull requests here: an admin can allow it under Settings, Actions.",
1378 );
1379 }
1380 if let Some(only) = access.repo.as_deref()
1381 && !NO_SCOPE.contains(&operation)
1382 {
1383 match input["repo"].as_str() {
1384 Some(repo) if access.reaches(repo) => {}
1385 Some(repo) => {
1386 return Decision::deny("token:repository", format!("This token is a workflow job's in {only}: it cannot reach {repo}."));
1387 }
1388 None => {
1389 return Decision::deny("token:repository", format!("This token is a workflow job's: it reaches only {only}, and {operation} is not about one repository."));
1390 }
1391 }
1392 }
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1393 // A token made for one workspace (or none) only reads outside it:
1394 // public repositories, as anyone may. Inside it, its repository
1395 // selection is checked with its owner's role (`access::granted`).
1396 if let Some(reach) = &access.reach
Token reach: workflow_files scope, fine-grained reach, workspace token cap1397 && let Some(repo) = input["repo"].as_str()
1398 && !NO_SCOPE.contains(&operation)
1399 {
1400 let namespace = repo.split('/').next().unwrap_or_default();
1401 let changes = needed(operation, input).iter().any(|scope| scope.level() != Level::Read);
1402 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 managers1403 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 cap1404 return Decision::deny(
1405 "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 managers1406 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 cap1407 );
1408 }
1409 }
The Packages page names every registry it has1410 if access.scopes.is_some() {
1411 let known = NO_SCOPE.contains(&operation) || scope_for(operation).is_some();
1412 if !known {
1413 return Decision::deny("token:scope", format!("This access token cannot use {operation}: it needs full access."));
1414 }
1415 if let Some(missing) = needed(operation, input).into_iter().find(|scope| !access.allows(*scope)) {
1416 return Decision::deny(
1417 "token:scope",
1418 format!("This access token needs the {} scope to use {operation}.", missing.as_str()),
1419 );
1420 }
1421 }
1422 Decision::allow(rule)
1423}
1424
Merge branch 'worktree-agent-a3abfcce648e87dca'1425/// Whether a token may use the repository `owner/name` at all: a refusal
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1426/// for a workflow job's token or a deploy key in another repository,
1427/// else `None`. Git and
Merge branch 'worktree-agent-a3abfcce648e87dca'1428/// the package registries ask this before [`decide_git`] and
1429/// [`decide_packages`].
1430pub fn decide_repo(access: &TokenAccess, repo: &str) -> Option<Decision> {
1431 let only = access.repo.as_deref()?;
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1432 let why = if access.deploy_key.is_some() {
1433 format!("This deploy key is for {only}: it cannot reach {repo}.")
1434 } else {
1435 format!("This token is a workflow job's in {only}: it cannot reach {repo}.")
1436 };
1437 (!access.reaches(repo)).then(|| Decision::deny("token:repository", why))
Merge branch 'worktree-agent-a3abfcce648e87dca'1438}
1439
The Packages page names every registry it has1440/// Whether a token may clone or fetch (`write` false), or push to (`write`
1441/// true), a repository with git. `public` is whether anyone may read it,
1442/// which needs no scope.
1443pub fn decide_git(access: &TokenAccess, write: bool, public: bool) -> Decision {
1444 let needed = if write { Scope::CodeWrite } else { Scope::CodeRead };
1445 if !access.allows(needed) && (write || !public) {
Merge branch 'worktree-agent-a5a5de74d8863d554' into worktree-agent-a16631325aecf58ca1446 if access.deploy_key.is_some() {
1447 return Decision::deny(
1448 "token:scope",
1449 "This deploy key is read-only. An admin of the repository can add it again with write access to push with it.",
1450 );
1451 }
The Packages page names every registry it has1452 return Decision::deny(
1453 "token:scope",
1454 format!("This access token needs the {} scope to {} with git.", needed.as_str(), if write { "push" } else { "clone or fetch a private repository" }),
1455 );
1456 }
1457 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
1458}
1459
1460/// Whether a token may pull (`Level::Read`), push or publish
1461/// (`Level::Write`), or delete (`Level::Delete`) packages. `public` is
1462/// whether anyone may pull the package, which needs no scope.
1463pub fn decide_packages(access: &TokenAccess, level: Level, public: bool) -> Decision {
1464 let (needed, doing) = match level {
1465 Level::Read => (Scope::PackagesRead, "pull a private package"),
1466 Level::Delete | Level::Admin => (Scope::PackagesDelete, "delete packages"),
1467 Level::Write | Level::Run => (Scope::PackagesWrite, "push or publish packages"),
1468 };
1469 if !access.allows(needed) && !(level == Level::Read && public) {
1470 return Decision::deny(
1471 "token:scope",
1472 format!("This access token needs the {} scope to {doing}.", needed.as_str()),
1473 );
1474 }
1475 Decision::allow(if access.legacy { "token:legacy" } else { "token:scope" })
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480 use super::*;
1481 use serde_json::json;
1482
1483 fn token(scopes: &[Scope]) -> TokenAccess {
1484 TokenAccess {
1485 token_id: "tok_1".to_owned(),
1486 scopes: Some(scopes.iter().map(|scope| scope.as_str().to_owned()).collect()),
1487 legacy: false,
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens1488 name: None,
Merge branch 'worktree-agent-a3abfcce648e87dca'1489 ..TokenAccess::default()
The Packages page names every registry it has1490 }
1491 }
1492
1493 #[test]
1494 fn every_scope_reads_back_and_belongs_to_a_resource() {
1495 for scope in Scope::ALL {
1496 assert_eq!(Scope::parse(scope.as_str()), Some(scope));
1497 assert!(scope.as_str().starts_with(scope.resource().as_str()));
1498 assert!(scope.includes(scope));
1499 }
1500 assert_eq!(Scope::parse(" Issues:Write "), Some(Scope::IssuesWrite));
1501 assert_eq!(Scope::parse("issues"), None);
1502 }
1503
1504 #[test]
1505 fn a_higher_level_includes_the_lower_ones_of_its_resource_only() {
1506 assert!(Scope::RepoAdmin.includes(Scope::RepoRead));
1507 assert!(Scope::RepoAdmin.includes(Scope::RepoWrite));
1508 assert!(Scope::IssuesWrite.includes(Scope::IssuesRead));
1509 assert!(!Scope::IssuesRead.includes(Scope::IssuesWrite));
1510 assert!(!Scope::RepoAdmin.includes(Scope::CodeWrite));
1511 assert!(!Scope::PullRequestsWrite.includes(Scope::IssuesWrite));
1512 }
1513
1514 #[test]
1515 fn operations_are_listed_once_and_never_also_free() {
1516 let mut seen = std::collections::HashSet::new();
1517 for (name, _) in OPERATIONS {
1518 assert!(seen.insert(*name), "{name} twice");
1519 assert!(!NO_SCOPE.contains(name), "{name}");
1520 }
1521 }
1522
1523 #[test]
1524 fn scopes_are_parsed_from_oauth_text_leaving_out_unknown_ones() {
1525 assert_eq!(
1526 parse_scopes("issues:write repo:read,bogus:thing issues:write"),
1527 vec![Scope::RepoRead, Scope::IssuesWrite]
1528 );
1529 assert_eq!(scopes_text(&[Scope::RepoRead, Scope::IssuesWrite]), "repo:read issues:write");
1530 }
1531
1532 #[test]
1533 fn the_oauth_default_is_the_agent_preset_and_never_admin() {
1534 let scopes = oauth_default();
1535 assert!(scopes.contains(&Scope::IssuesWrite));
1536 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.1537 assert!(!scopes.contains(&Scope::AgentsRun));
The Packages page names every registry it has1538 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 yet1539 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)1540 // Every read offered but the machines work runs on, and chat.
1541 assert_eq!(scopes.contains(&read), read != Scope::RunnersRead && read != Scope::ChatRead, "{read:?}");
The Packages page names every registry it has1542 }
1543 assert!(Preset::ReadOnly.scopes().unwrap().iter().all(|scope| scope.level() == Level::Read));
1544 assert_eq!(Preset::Full.scopes(), None);
1545 }
1546
1547 #[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.1548 fn retired_scopes_parse_but_are_never_granted() {
1549 // Stored tokens and OAuth grants that hold them still read.
1550 for text in ["agents:run", "models:read", "models:write"] {
1551 let scope = Scope::parse(text).unwrap();
1552 assert!(scope.resource().retired() && !scope.offered(), "{text}");
1553 let back: Scope = serde_json::from_value(json!(text)).unwrap();
1554 assert_eq!(back, scope);
1555 }
1556 // But nothing hands them out, and asking for them gets the rest.
1557 assert_eq!(parse_scopes("agents:run issues:read models:write"), vec![Scope::IssuesRead]);
1558 assert!(!offered_scopes().iter().any(|scope| scope.resource().retired()));
1559 assert!(!everything().iter().any(|scope| scope.resource().retired()));
1560 for preset in Preset::ALL {
1561 let scopes = preset.scopes().unwrap_or_default();
1562 assert!(!scopes.iter().any(|scope| scope.resource().retired()), "{}", preset.as_str());
1563 }
1564 // And no operation needs them.
1565 assert!(!OPERATIONS.iter().any(|(_, scope)| scope.resource().retired()));
1566 }
1567
1568 #[test]
Usage, Billing settings and prepaid AI credit; fixes from the UX audit1569 fn billing_is_read_by_presets_and_changed_by_none_but_full_access() {
1570 assert!(Preset::ReadOnly.scopes().unwrap().contains(&Scope::BillingRead));
1571 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
1572 assert!(!preset.scopes().unwrap().contains(&Scope::BillingWrite), "{}", preset.as_str());
1573 }
1574 assert_eq!(scope_for("set_budget"), Some(Scope::BillingWrite));
1575 assert_eq!(scope_for("get_usage"), Some(Scope::BillingRead));
1576 let reader = token(&[Scope::BillingRead]);
1577 assert!(decide(&reader, "list_invoices", &json!({})).allowed);
1578 assert!(decide(&reader, "set_budget", &json!({})).reason.unwrap().contains("billing:write"));
1579 }
1580
1581 #[test]
Merge main into Artifacts Phase 21582 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 yet1583 for scope in [Scope::ArtifactsRead, Scope::ArtifactsWrite, Scope::ArtifactsAdmin] {
1584 assert_eq!(scope.resource(), Resource::Artifacts);
Merge main into Artifacts Phase 21585 assert!(scope.offered());
1586 assert!(offered_scopes().contains(&scope));
1587 assert_eq!(parse_scopes(scope.as_str()), vec![scope]);
1588 assert!(OPERATIONS.iter().any(|(_, needed)| *needed == scope), "{scope:?} gates nothing");
1589 }
1590 // Reading them is a read like any other; changing them is chosen.
1591 for preset in [Preset::ReadOnly, Preset::Agent] {
1592 let scopes = preset.scopes().unwrap();
1593 assert!(scopes.contains(&Scope::ArtifactsRead), "{}", preset.as_str());
1594 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 yet1595 }
Merge main into Artifacts Phase 21596 assert!(!Preset::Ci.scopes().unwrap().contains(&Scope::ArtifactsRead));
1597 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 yet1598 assert!(Scope::ArtifactsAdmin.includes(Scope::ArtifactsWrite));
1599 assert!(Scope::ArtifactsAdmin.dangerous());
Merge main into Artifacts Phase 21600 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 yet1601 assert_eq!(Resource::Artifacts.group(), ResourceGroup::Workspace);
Merge main into Artifacts Phase 21602 let asked = std::collections::BTreeMap::from([("artifacts".to_owned(), "write".to_owned())]);
1603 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.1604 assert!(offered_scopes().contains(&Scope::ArtifactsAdmin));
Merge main into Artifacts Phase 21605 // Sharing and deleting for good need admin; editing needs write.
1606 assert_eq!(scope_for("set_workspace_artifact_access"), Some(Scope::ArtifactsAdmin));
1607 assert_eq!(scope_for("purge_workspace_artifact"), Some(Scope::ArtifactsAdmin));
1608 assert_eq!(scope_for("edit_workspace_artifact"), Some(Scope::ArtifactsWrite));
1609 assert_eq!(scope_for("get_workspace_artifact_content"), Some(Scope::ArtifactsRead));
1610 let writer = token(&[Scope::ArtifactsWrite]);
1611 assert!(decide(&writer, "edit_workspace_artifact", &json!({})).allowed);
1612 assert!(decide(&writer, "list_workspace_artifacts", &json!({})).allowed, "write includes read");
1613 assert!(decide(&writer, "set_workspace_artifact_access", &json!({})).reason.unwrap().contains("artifacts:admin"));
1614 // Workflow runs' artifacts are another thing, with their own scope.
1615 let reader = token(&[Scope::ArtifactsRead]);
1616 assert!(!decide(&reader, "list_artifacts", &json!({ "repo": "acme/web" })).allowed);
1617 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 yet1618 }
1619
1620 #[test]
The phone app: Chat in full with phone notifications, plus Code and Customize (#4)1621 fn chat_is_a_persons_own_and_given_only_when_asked_for() {
1622 for scope in [Scope::ChatRead, Scope::ChatWrite] {
1623 assert_eq!(scope.resource(), Resource::Chat);
1624 assert!(!scope.dangerous());
1625 assert_eq!(Scope::parse(scope.as_str()), Some(scope));
1626 }
1627 assert_eq!(Resource::Chat.label(), "Chat");
1628 assert_eq!(Resource::Chat.group(), ResourceGroup::Account);
1629 assert!(Scope::ChatWrite.includes(Scope::ChatRead));
1630 assert!(!Scope::ChatRead.includes(Scope::ChatWrite));
1631 // No preset, and so no OAuth default, gives it.
1632 for preset in Preset::ALL {
1633 let scopes = preset.scopes().unwrap_or_default();
1634 assert!(!scopes.iter().any(|scope| scope.resource() == Resource::Chat), "{}", preset.as_str());
1635 }
1636 assert!(!oauth_default().iter().any(|scope| scope.resource() == Resource::Chat));
1637 // An application that asks for it by name gets it; full access holds it.
1638 assert_eq!(parse_scopes("chat:read chat:write"), vec![Scope::ChatRead, Scope::ChatWrite]);
1639 assert!(offered_scopes().contains(&Scope::ChatWrite));
1640 assert!(everything().contains(&Scope::ChatWrite));
1641 assert!(TokenAccess::full().allows(Scope::ChatWrite));
1642 // Only a person's token may hold it.
1643 let asked = std::collections::BTreeMap::from([("chat".to_owned(), "write".to_owned())]);
1644 assert_eq!(resolve_permissions(&asked, true), Ok(vec![Scope::ChatWrite]));
1645 assert!(resolve_permissions(&asked, false).is_err());
1646 }
1647
1648 #[test]
Merge checks: statuses and check runs on every commit1649 fn checks_are_reported_with_checks_write_which_ci_gets() {
1650 assert_eq!(scope_for("create_check_run"), Some(Scope::ChecksWrite));
1651 assert_eq!(scope_for("create_commit_status"), Some(Scope::ChecksWrite));
1652 assert_eq!(scope_for("list_check_runs_for_ref"), Some(Scope::ChecksRead));
1653 let ci = Preset::Ci.scopes().unwrap();
1654 assert!(ci.contains(&Scope::ChecksWrite));
1655 assert!(!Preset::Agent.scopes().unwrap().contains(&Scope::ChecksWrite));
1656 let reporter = token(&[Scope::ChecksWrite]);
1657 assert!(decide(&reporter, "update_check_run", &json!({ "id": "cr_1" })).allowed);
1658 assert!(decide(&reporter, "rerequest_check_run", &json!({ "id": "cr_1" })).allowed);
1659 // A g1t Actions job runs again as its workflow does.
1660 let refused = decide(&reporter, "rerequest_check_run", &json!({ "id": "job_1" }));
1661 assert!(refused.reason.unwrap().contains("workflows:write"));
1662 }
1663
1664 #[test]
Merge branch 'worktree-agent-a3abfcce648e87dca'1665 fn a_job_token_reaches_its_repository_only() {
1666 let job = TokenAccess {
1667 repo: Some("acme/web".into()),
1668 job: Some(JobToken { run_id: "run_1".into(), job_id: "job_1".into(), pull_requests: false }),
1669 ..token(&[Scope::RepoRead, Scope::IssuesWrite, Scope::IssuesRead, Scope::PullRequestsWrite])
1670 };
1671 assert!(decide(&job, "create_issue", &json!({ "repo": "acme/web" })).allowed);
1672 assert!(decide(&job, "create_issue", &json!({ "repo": "Acme/Web" })).allowed, "names compare without case");
1673 let elsewhere = decide(&job, "create_issue", &json!({ "repo": "acme/api" }));
1674 assert!(!elsewhere.allowed);
1675 assert_eq!(elsewhere.rule, "token:repository");
1676 // Nothing beyond the one repository, a workspace's listing included.
1677 assert!(!decide(&job, "list_repos", &json!({})).allowed);
1678 assert!(decide(&job, "whoami", &json!({})).allowed);
1679 // Its scopes still hold inside it.
1680 assert!(!decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).allowed);
1681 assert!(decide_repo(&job, "acme/web").is_none());
1682 assert!(!decide_repo(&job, "acme/api").unwrap().allowed);
1683 assert!(decide_repo(&token(&[Scope::CodeRead]), "acme/api").is_none(), "other tokens reach what their owner can");
1684 // Opening and approving pull requests is off unless allowed.
1685 assert_eq!(decide(&job, "create_pull_request", &json!({ "repo": "acme/web" })).rule, "token:pull-requests");
1686 assert!(!decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "approve" })).allowed);
1687 assert!(decide(&job, "review_pull_request", &json!({ "repo": "acme/web", "verdict": "request_changes" })).allowed);
1688 let allowed = TokenAccess { job: Some(JobToken { pull_requests: true, ..job.job.clone().unwrap() }), ..job.clone() };
1689 assert!(decide(&allowed, "create_pull_request", &json!({ "repo": "acme/web" })).allowed);
1690 }
1691
1692 #[test]
Workflow files need workflow_files:write from a token; fine-grained permission table1693 fn workflow_files_need_their_own_scope() {
1694 for path in [".g1t/workflows/ci.yml", ".github/workflows/deploy.yaml", "/.github/workflows/x.yml", ".GitHub/Workflows/ci.yml", ".github/workflows"] {
1695 assert!(is_workflow_file(path), "{path}");
1696 }
1697 for path in ["README.md", ".github/CODEOWNERS", ".github/workflowsx/ci.yml", "docs/.github/workflows/ci.yml", ".g1t/actions/ci.yml"] {
1698 assert!(!is_workflow_file(path), "{path}");
1699 }
1700 let code = token(&[Scope::CodeWrite]);
1701 let refused = decide_workflow_files(Some(&code), ["README.md", ".github/workflows/ci.yml"]).unwrap();
1702 assert_eq!(refused.rule, "token:workflows");
1703 assert!(refused.reason.as_deref().unwrap().contains(".github/workflows/ci.yml"));
1704 assert!(refused.reason.as_deref().unwrap().contains("workflow_files:write"));
1705 assert!(decide_workflow_files(Some(&code), ["README.md"]).is_none());
1706 assert!(decide_workflow_files(Some(&token(&[Scope::CodeWrite, Scope::WorkflowFilesWrite])), [".g1t/workflows/ci.yml"]).is_none());
1707 assert!(decide_workflow_files(Some(&TokenAccess::full()), [".g1t/workflows/ci.yml"]).is_none(), "full access");
1708 assert!(decide_workflow_files(None, [".g1t/workflows/ci.yml"]).is_none(), "a signed-in person");
1709 // A job's token never may, as GITHUB_TOKEN never may.
1710 let job = TokenAccess { job: Some(JobToken::default()), ..TokenAccess::full() };
1711 assert!(decide_workflow_files(Some(&job), [".g1t/workflows/ci.yml"]).unwrap().reason.unwrap().contains("job"));
1712 // Nothing in a preset changes workflow files but full access.
1713 for preset in [Preset::ReadOnly, Preset::Agent, Preset::Ci] {
1714 assert!(!preset.scopes().unwrap().contains(&Scope::WorkflowFilesWrite), "{}", preset.as_str());
1715 }
1716 assert!(!Scope::WorkflowFilesWrite.includes(Scope::WorkflowsWrite) && !Scope::WorkflowsWrite.includes(Scope::WorkflowFilesWrite));
1717 }
1718
1719 #[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 managers1720 fn a_narrowed_token_only_reads_outside_its_workspace() {
1721 let reach = TokenReach { workspace: Some("acme".into()), repositories: RepositorySelection::All, repo_ids: Vec::new() };
1722 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 table1723 assert!(decide(&fine, "create_issue", &json!({ "repo": "acme/web" })).allowed);
1724 assert!(decide(&fine, "create_issue", &json!({ "repo": "Acme/web" })).allowed);
1725 let elsewhere = decide(&fine, "create_issue", &json!({ "repo": "globex/site" }));
1726 assert_eq!(elsewhere.rule, "token:resource-owner");
1727 assert!(elsewhere.reason.unwrap().contains("acme"));
1728 assert!(decide(&fine, "get_issue", &json!({ "repo": "globex/site" })).allowed, "public repositories elsewhere read");
1729 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 managers1730 let mine = TokenAccess { reach: Some(TokenReach::default()), ..token(&[Scope::IssuesWrite]) };
Workflow files need workflow_files:write from a token; fine-grained permission table1731 assert!(decide(&mine, "create_issue", &json!({ "repo": "acme/web" })).reason.unwrap().contains("your account"));
1732 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 managers1733 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 table1734 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 managers1735 let public = TokenReach { repositories: RepositorySelection::Public, ..selected.clone() };
Workflow files need workflow_files:write from a token; fine-grained permission table1736 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 managers1737 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 table1738 assert_eq!(RepositorySelection::parse("public_only"), Some(RepositorySelection::Public));
1739 }
1740
1741 #[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 managers1742 fn permissions_are_scopes_read_per_resource() {
1743 let asked: std::collections::BTreeMap<String, String> =
1744 [("issues", "write"), ("repo", "read"), ("code", "none"), ("packages", "delete")].iter().map(|(a, b)| ((*a).to_owned(), (*b).to_owned())).collect();
1745 let scopes = resolve_permissions(&asked, true).unwrap();
1746 assert_eq!(scopes, vec![Scope::RepoRead, Scope::PackagesDelete, Scope::IssuesWrite]);
1747 let back = permissions_of(&scopes);
1748 assert_eq!(back.get("issues").map(String::as_str), Some("write"));
1749 assert_eq!(back.get("packages").map(String::as_str), Some("delete"));
1750 assert!(!back.contains_key("code"));
1751 // Lower levels held beside a higher one say nothing more.
1752 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 yet1753 // 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 managers1754 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 yet1755 assert_eq!(all.len(), Resource::ALL.into_iter().filter(|resource| resource.offered()).count());
1756 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 managers1757 assert!(all.iter().any(|held| held.includes(scope)), "{scope:?}");
1758 }
1759 }
1760
1761 #[test]
1762 fn permissions_are_checked_by_name_level_and_owner() {
1763 let one = |name: &str, level: &str| -> std::collections::BTreeMap<String, String> { [(name.to_owned(), level.to_owned())].into() };
1764 assert!(resolve_permissions(&one("wiki", "read"), true).unwrap_err().contains("wiki"));
1765 assert!(resolve_permissions(&one("issues", "admin"), true).unwrap_err().contains("read, write"));
1766 assert!(resolve_permissions(&one("workflow_files", "read"), true).is_err(), "workflow files are written only");
1767 assert!(resolve_permissions(&one("notifications", "read"), false).unwrap_err().contains("account"));
1768 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.1769 assert!(resolve_permissions(&one("agents", "run"), false).unwrap_err().contains("agents"), "retired");
1770 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 managers1771 for resource in Resource::ALL {
1772 assert_eq!(Resource::parse(resource.as_str()), Some(resource));
1773 assert!(!resource.scopes().is_empty());
1774 }
1775 }
1776
1777 #[test]
The Packages page names every registry it has1778 fn a_legacy_token_can_do_everything() {
1779 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
1780 for (operation, _) in OPERATIONS {
1781 assert!(decide(&legacy, operation, &json!({})).allowed, "{operation}");
1782 }
1783 assert_eq!(decide(&legacy, "delete_repo", &json!({})).rule, "token:legacy");
1784 }
1785
1786 #[test]
1787 fn a_missing_scope_is_named() {
1788 let read = token(&[Scope::IssuesRead]);
1789 assert!(decide(&read, "get_issue", &json!({})).allowed);
1790 assert!(decide(&read, "whoami", &json!({})).allowed);
1791 let refused = decide(&read, "create_issue", &json!({}));
1792 assert!(!refused.allowed);
1793 assert_eq!(refused.reason.as_deref(), Some("This access token needs the issues:write scope to use create_issue."));
1794 // An operation the table does not know needs full access.
1795 assert!(!decide(&read, "something_new", &json!({})).allowed);
1796 }
1797
1798 #[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.1799 fn making_a_repository_public_or_private_needs_repo_admin() {
The Packages page names every registry it has1800 let maintainer = token(&[Scope::RepoWrite]);
1801 assert!(decide(&maintainer, "update_repo", &json!({ "description": "x" })).allowed);
1802 assert!(!decide(&maintainer, "update_repo", &json!({ "private": true })).allowed);
1803 }
1804
1805 #[test]
1806 fn a_workspaces_base_permission_needs_access_admin_too() {
1807 let admin = token(&[Scope::WorkspaceAdmin]);
1808 assert!(decide(&admin, "update_workspace", &json!({ "name": "Acme" })).allowed);
1809 let refused = decide(&admin, "update_workspace", &json!({ "name": "Acme", "base_permission": "read" }));
1810 assert!(refused.reason.unwrap().contains("access:admin"));
1811 let both = token(&[Scope::WorkspaceAdmin, Scope::AccessAdmin]);
1812 assert!(decide(&both, "update_workspace", &json!({ "base_permission": "read" })).allowed);
1813 assert!(!decide(&token(&[Scope::WorkspaceRead]), "update_workspace", &json!({ "name": "Acme" })).allowed);
1814 }
1815
1816 #[test]
1817 fn git_push_needs_code_write_and_private_reads_need_code_read() {
1818 let reader = token(&[Scope::CodeRead]);
1819 assert!(decide_git(&reader, false, false).allowed);
1820 let refused = decide_git(&reader, true, false);
1821 assert!(!refused.allowed);
1822 assert!(refused.reason.unwrap().contains("code:write"));
1823 let issues = token(&[Scope::IssuesWrite]);
1824 assert!(!decide_git(&issues, false, false).allowed);
1825 assert!(decide_git(&issues, false, true).allowed, "public code needs no scope");
1826 assert!(!decide_git(&issues, true, true).allowed, "pushing to public code still needs code:write");
1827 let writer = token(&[Scope::CodeWrite]);
1828 assert!(decide_git(&writer, true, false).allowed);
1829 assert!(decide_git(&writer, false, false).allowed, "code:write includes code:read");
1830 assert!(decide_git(&TokenAccess::full(), true, false).allowed);
1831 }
1832
1833 #[test]
1834 fn packages_need_their_own_scopes_and_public_pulls_none() {
1835 let reader = token(&[Scope::PackagesRead]);
1836 assert!(decide_packages(&reader, Level::Read, false).allowed);
1837 assert!(!decide_packages(&reader, Level::Write, false).allowed);
1838 let code = token(&[Scope::CodeWrite]);
1839 assert!(!decide_packages(&code, Level::Read, false).allowed, "code scopes are not package scopes");
1840 assert!(decide_packages(&code, Level::Read, true).allowed, "public packages pull with any token");
1841 let writer = token(&[Scope::PackagesWrite]);
1842 assert!(decide_packages(&writer, Level::Write, false).allowed);
1843 assert!(decide_packages(&writer, Level::Read, false).allowed, "packages:write includes packages:read");
1844 let refused = decide_packages(&writer, Level::Delete, false);
1845 assert!(refused.reason.unwrap().contains("packages:delete"));
1846 assert!(decide_packages(&token(&[Scope::PackagesDelete]), Level::Write, false).allowed);
1847 assert!(Scope::PackagesDelete.dangerous());
1848 // Tokens made before these scopes, and full-access ones, keep working.
1849 let legacy = TokenAccess { legacy: true, ..TokenAccess::full() };
1850 assert!(decide_packages(&legacy, Level::Delete, false).allowed);
1851 assert!(decide_packages(&TokenAccess::full(), Level::Write, false).allowed);
1852 }
1853
1854 #[test]
1855 fn token_access_travels_as_json() {
1856 let access = token(&[Scope::IssuesRead]);
1857 let wire = serde_json::to_value(&access).unwrap();
1858 assert_eq!(wire["scopes"], json!(["issues:read"]));
1859 assert!(wire.get("resources").is_none());
1860 let back: TokenAccess = serde_json::from_value(wire).unwrap();
1861 assert_eq!(back, access);
1862 let full: TokenAccess = serde_json::from_value(json!({})).unwrap();
1863 assert!(full.is_full());
1864 // A reach written by an older version is ignored: a token reaches
1865 // whatever its owner can.
1866 let older: TokenAccess = serde_json::from_value(json!({
1867 "token_id": "tok_1",
1868 "scopes": ["issues:read"],
1869 "resources": { "kind": "repositories", "repositories": ["acme/rocket"] },
1870 }))
1871 .unwrap();
1872 assert_eq!(older, access);
Merge main into Artifacts Phase 21873 // Using the website is off unless set, and said only when on.
1874 assert!(!access.website);
1875 assert!(wire_of(&access).get("website").is_none());
1876 let website = TokenAccess { website: true, ..access };
1877 assert_eq!(wire_of(&website)["website"], json!(true));
1878 // Never part of full access.
1879 assert!(!TokenAccess::full().website);
The Packages page names every registry it has1880 }
1881
Merge main into Artifacts Phase 21882 fn wire_of(access: &TokenAccess) -> serde_json::Value {
1883 serde_json::to_value(access).unwrap()
1884 }
1885
The Packages page names every registry it has1886 /// The site's copy of the table, `packages/contracts/src/scopes.ts`,
1887 /// lists the same scopes in the same order, the same operations with
1888 /// the same scopes, and the same presets.
1889 #[test]
1890 fn the_typescript_mirror_has_the_same_table() {
1891 let ts = include_str!("../../../packages/contracts/src/scopes.ts");
1892 let section = |start: &str| {
1893 ts.split_once(start)
1894 .and_then(|(_, rest)| rest.split_once("] as const"))
1895 .map(|(table, _)| table)
1896 .unwrap_or_else(|| panic!("{start} in scopes.ts"))
1897 };
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 yet1898 let names = |table: &str| -> Vec<String> {
1899 section(table)
1900 .lines()
1901 .filter_map(|line| line.split_once("scope: \"").and_then(|(_, rest)| rest.split_once('"')).map(|(scope, _)| scope.to_owned()))
1902 .collect()
1903 };
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.1904 // Offered scopes in `SCOPES`, retired ones in `RETIRED_SCOPES`, the
1905 // rest in `UPCOMING_SCOPES`.
1906 let listed = |keep: fn(&Scope) -> bool| -> Vec<String> { Scope::ALL.iter().filter(|scope| keep(scope)).map(|scope| scope.as_str().to_owned()).collect() };
1907 assert_eq!(names("export const SCOPES = ["), listed(|scope| scope.offered()));
1908 assert_eq!(names("export const UPCOMING_SCOPES"), listed(|scope| !scope.offered() && !scope.resource().retired()));
1909 assert_eq!(names("export const RETIRED_SCOPES"), listed(|scope| scope.resource().retired()));
The Packages page names every registry it has1910 let operations: Vec<(String, String)> = section("export const OPERATION_SCOPES = [")
1911 .lines()
1912 .filter_map(|line| {
1913 let mut quoted = line.split('"').skip(1).step_by(2);
1914 Some((quoted.next()?.to_owned(), quoted.next()?.to_owned()))
1915 })
1916 .collect();
1917 let expected: Vec<(String, String)> = OPERATIONS
1918 .iter()
1919 .map(|(name, scope)| ((*name).to_owned(), scope.as_str().to_owned()))
1920 .collect();
1921 assert_eq!(operations, expected);
1922 for preset in Preset::ALL {
1923 let list = section(&format!("{}: [", preset.as_str()));
1924 let mirrored: Vec<&str> = list
1925 .split(',')
1926 .map(|item| item.trim().trim_matches('"'))
1927 .filter(|item| !item.is_empty())
1928 .collect();
1929 let expected: Vec<&str> = preset
1930 .scopes()
1931 .map(|scopes| scopes.iter().map(|scope| scope.as_str()).collect())
1932 .unwrap_or_else(|| vec!["*"]);
1933 assert_eq!(mirrored, expected, "{}", preset.as_str());
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 yet1935 // 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.1936 // `SCOPE_RESOURCES`, retired ones in `RETIRED_RESOURCES`, the rest in
1937 // `UPCOMING_RESOURCES`.
1938 let tables: [(&str, fn(Resource) -> bool); 3] = [
1939 ("export const SCOPE_RESOURCES", |resource| resource.offered()),
1940 ("export const UPCOMING_RESOURCES", |resource| !resource.offered() && !resource.retired()),
1941 ("export const RETIRED_RESOURCES", |resource| resource.retired()),
1942 ];
1943 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 yet1944 let resources = ts
1945 .split_once(table)
1946 .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 managers1947];"))
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 yet1948 .map(|(table, _)| table)
1949 .unwrap_or_else(|| panic!("{table} in scopes.ts"));
1950 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.1951 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 yet1952 assert_eq!(rows.len(), expected.len(), "{table}");
1953 for (row, resource) in rows.iter().zip(expected) {
1954 assert!(row.contains(&format!("resource: \"{}\"", resource.as_str())), "{row}");
1955 assert!(row.contains(&format!("group: \"{}\"", resource.group().as_str())), "{row}");
1956 }
One kind of access token; presence and status; usernames keep their case; the tour is a miniature of the real app; icons for password managers1957 }
The Packages page names every registry it has1958 }
1959}

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