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

Commit

The Agent preset creates repositories, with a new repo:create scope (#52)

An agent signed in to MCP with the Agent preset (what Claude Code's plugin gets) could not create a repository: create_repo needed repo:write, which the preset leaves out. repo:write is too much to hand an agent by default: besides creating repositories it changes a repository's merge settings, required checks and approvals (update_repo_settings), updates the repository and its project (update_repo, update_project), creates, renames and deletes branches, publishes and deletes releases, and forks and syncs forks. So repo:create is a new level of Repositories between read and write: it creates repositories and nothing that changes or deletes one. create_repo now needs repo:create, which repo:write and repo:admin include as before, so every token that could create a repository still can. The Agent preset, and with it the OAuth default, adds repo:create. The site's mirror of the table, the token form's level order and label ("Read and create"), the scope checklist, the docs' permission and scope tables, the preset description, the MCP reference, llms.txt and the build-and-ship guide say so; the OpenAPI copy and the CLI's ops table are regenerated. An OAuth grant stores its scopes when it is approved, so a plugin signed in before this keeps what it had until it is signed in again or given repo:create under Connected applications; the guides say so.

Checked:
cargo test -p g1t-contracts (313, with a new test that the Agent preset creates repositories and is refused update_repo, update_repo_settings, delete_branch, rename_branch, delete_release, fork_repo, set_repo_visibility, delete_repo and transfer_repo), -p g1t-api (208), -p g1t-identity, -p g1t-actions, -p g1t; cargo check --workspace --tests; apps/web npm test (955 pass) and typecheck against this worktree's contracts; packages/contracts npm test; docs build and npm test.
G1t-Session:
ses_01m4ynr1peevvs8zwaqakycv2e
syntaqxcommitted Parente5f11c4Browse files
10 files+89−250/10 viewed
+9−5
774774
775775 | Permission | Levels | Scope names |
776776 | --- | --- | --- |
777−| Repositories | read, read and write, admin | `repo:read`, `repo:write`, `repo:admin` |
777+| Repositories | read, read and create, read and write, admin | `repo:read`, `repo:create`, `repo:write`, `repo:admin` |
778778 | Code | read, read and write | `code:read`, `code:write` |
779779 | Security | read, read and write | `security:read`, `security:write` |
780780 | Packages | read, read and write, read, write and delete | `packages:read`, `packages:write`, `packages:delete` |
839839 | Preset | Permissions |
840840 | --- | --- |
841841 | Read only | Every resource at read except Chat. Changes nothing. |
842−| Agent | Every resource at read except Self-hosted runners and Chat, and Code, Issues, Pull requests and Notifications at read and write. For a [coding agent](/guides/bring-your-own-agent/): reads everything, works on issues and pull requests, pushes code, and answers your inbox. No admin level. |
842+| Agent | Every resource at read except Self-hosted runners and Chat, Repositories at read and create, and Code, Issues, Pull requests and Notifications at read and write. For a [coding agent](/guides/bring-your-own-agent/): reads everything, creates repositories, works on issues and pull requests, pushes code, and answers your inbox. It cannot change or delete a repository, its settings, branches or releases. No admin level. |
843843 | CI | Repositories: read; Code, Packages, Workflows, Checks and statuses, and Deployments: read and write. Clones and pushes code, pushes and pulls packages, runs workflows, and reports [checks](/guides/checks/) and deployments. |
844844
845845 **All** is not a preset that grows: it ticks every permission there is
854854 `issues:write`. A token stores the highest scope of each resource it
855855 holds, and every check reads them. A higher level includes the lower ones
856856 of the same resource: `repo:admin` includes `repo:write`, which includes
857−`repo:read`. It never includes another resource: `repo:admin` does not let
857+`repo:create`, which includes `repo:read`. It never includes another resource: `repo:admin` does not let
858858 a token push, which is `code:write`.
859859
860860 Applications that [sign in with OAuth](#signing-in-with-oauth) ask for
864864 | Scope | What it lets a token do |
865865 | --- | --- |
866866 | `repo:read` | See repositories, their settings, labels, timelines, releases, languages, contributors and security alerts, and search |
867+| `repo:create` | Create repositories, and nothing that changes or deletes one |
867868 | `repo:write` | Create repositories, rename branches, change how pull requests merge and publish releases |
868869 | `repo:admin` | Rename, archive, transfer, delete or change who can see a repository, and dismiss security alerts |
869870 | `code:read` | Clone and fetch private repositories with git |
11001101 everything you can.
11011102
11021103 An application that asks for no scopes in particular gets the
1103−[Agent preset](#presets): every `read` scope except `runners:read` and `chat:read`, and `code:write`,
1104−`issues:write`, `pull_requests:write` and `notifications:write`.
1104+[Agent preset](#presets): every `read` scope except `runners:read` and `chat:read`, and `repo:create`,
1105+`code:write`, `issues:write`, `pull_requests:write` and `notifications:write`.
1106+The scopes are kept when you approve it: an application approved before
1107+`repo:create` joined the preset gets it only once you add it under
1108+Connected applications, or sign it in again.
11051109 It never gets an admin scope unless it asks for one and you leave it
11061110 ticked, and it gets `chat:read` and `chat:write` only by asking for them;
11071111 with them it can use [Chat](#chat-and-notifications-without-the-website).
+7−6
4141 | | The g1t CLI | g1t's MCP tools |
4242 | --- | --- | --- |
4343 | Signed in by | `g1t auth login` | `/mcp`, **plugin:g1t:g1t** |
44−| What it may do | Everything you can: the sign-in carries every scope, bounded by your roles | The [Agent preset](/guides/authentication/#presets), unless you change it: reading across g1t, and writing code, issues, pull requests and notifications |
45−| Creates a repository | Yes | Only once you add `repo:write` |
44+| What it may do | Everything you can: the sign-in carries every scope, bounded by your roles | The [Agent preset](/guides/authentication/#presets), unless you change it: reading across g1t, creating repositories, and writing code, issues, pull requests and notifications |
45+| Creates a repository | Yes | Yes, with `repo:create` |
4646
4747 The plugin's skills and hooks use the CLI, so Claude reaches for `g1t`
48−commands first. To let the MCP tools create repositories too, open
48+commands first. A plugin signed in before the Agent preset gave
49+`repo:create` keeps the scopes it had: open
4950 [Settings → Connected applications](https://g1t.sh/settings/applications),
50−choose **Change access** on Claude Code, tick `repo:write` and choose
51−**Save access**.
51+choose **Change access** on Claude Code, tick `repo:create` and choose
52+**Save access**, or sign it in again with `/mcp`.
5253
5354 ## Build and ship the first version
5455
199200
200201 | | The agent | How | Needs |
201202 | --- | --- | --- | --- |
202−| Create a repository | Yes | `g1t repo create`, or `repository` `create` | `repo:write`, which the Agent preset leaves out |
203+| Create a repository | Yes | `g1t repo create`, or `repository` `create` | `repo:create`, which the Agent preset gives |
203204 | Push code | Yes | git, with the CLI's credentials (`g1t auth setup-git`) or an [access token](/guides/authentication/#access-tokens) | `code:write`; a workflow file also needs `workflow_files:write` |
204205 | Open and claim an issue | Yes | `g1t work claim`, or `issue` `claim` | `issues:write` |
205206 | Open a pull request and mark it ready | Yes | `g1t pr create` and `g1t pr ready`, or `pull_request` `create` and `ready` | `pull_requests:write` |
+1−1
259259 | [`contents`](/reference/api/repositories/get-contents/) | What it holds at `path` (the root when left out) at `ref` (the default branch when left out): a file's `text` (null for a binary one; at most 200,000 characters), or a directory's `entries` and its README. | `repo` | `code:read` |
260260 | [`tarball`](/reference/api/repositories/download-tarball-archive/) | A link to a branch, tag or commit (`ref`, the default branch when left out) as a `.tar.gz` that needs no token, good for 10 minutes, as `url`, with the `commit` it is of. | `repo` | `code:read` |
261261 | [`zipball`](/reference/api/repositories/download-zipball-archive/) | The same as a `.zip`. | `repo` | `code:read` |
262−| [`create`](/reference/api/repositories/create-repo/) | Create a repository in one of your workspaces, empty or as a copy of a public git repository (`import_url`). `description` becomes its [project's](/guides/projects/), as a repository has none of its own. `workspace` may be left out if you belong to exactly one. | `name` | `repo:write` |
262+| [`create`](/reference/api/repositories/create-repo/) | Create a repository in one of your workspaces, empty or as a copy of a public git repository (`import_url`). `description` becomes its [project's](/guides/projects/), as a repository has none of its own. `workspace` may be left out if you belong to exactly one. | `name` | `repo:create`, which `repo:write` includes and the [Agent preset](/guides/authentication/#presets) gives |
263263 | [`update`](/reference/api/repositories/update-repo/) | Change its `website`, `topics` and `default_branch`, whether its default branch is `protected`, whether it is `private`, and whether a private repository may be forked (`allow_forking`). Maintain role; `protected`, `private`, `allow_forking` and `default_branch` need Admin, and `private` the [member privileges](/guides/workspaces/#member-privileges) unless you are an owner. | `repo` | `repo:write` |
264264 | [`get_settings`](/reference/api/repositories/get-repo-settings/) | How it handles pull requests: how they merge, and the default branch's required checks, approvals, bypassing checks, being up to date and merge queue as its [rulesets](/guides/rules/) stack. | `repo` | `repo:read` |
265265 | [`update_settings`](/reference/api/repositories/update-repo-settings/) | Change those settings. Only the fields given change; `required_checks` replaces the whole list. Maintain role. | `repo` | `repo:write` |
+0−0

Binary or large file; its contents are not shown.

+2−0
9595 return "No access";
9696 case "read":
9797 return "Read";
98+ case "create":
99+ return "Read and create";
98100 case "write":
99101 return levelsOf(resource).includes("read") ? "Read and write" : "Write";
100102 case "run":
+1−1
2727 type ScopeResource,
2828 } from "@g1t/contracts/scopes";
2929
30−const RANK: Record<ScopeLevel, number> = { read: 0, write: 1, run: 2, delete: 3, admin: 4 };
30+const RANK: Record<ScopeLevel, number> = { read: 0, create: 1, write: 2, run: 3, delete: 4, admin: 5 };
3131
3232 /** Anything with FormData's getters, so tests can pass a plain map. */
3333 export type FormLike = { get(name: string): unknown; getAll(name: string): unknown[] };
+1−1
371371 only public repositories of it, with permissions (`contents`, `issues`,
372372 `workflows`, …) mapped to these scopes; a workspace may require an owner
373373 to approve one first. For a coding agent, use the Agent preset (every read
374−scope but `runners:read` and `chat:read`, plus `code:write`,
374+scope but `runners:read` and `chat:read`, plus `repo:create`, `code:write`,
375375 `issues:write`, `pull_requests:write`, `notifications:write`). For CI, the CI preset (`repo:read`, `code:read`,
376376 `code:write`, `packages:read`, `packages:write`, `workflows:read`,
377377 `workflows:write`, `checks:read`, `checks:write`, `deployments:read`,
+60−4
99 //!
1010 //! Each scope is a resource and a level, written `resource:level`, such as
1111 //! `issues:write`. A higher level of a resource includes the lower ones:
12−//! `repo:admin` includes `repo:write`, which includes `repo:read`.
12+//! `repo:admin` includes `repo:write`, which includes `repo:create`, which
13+//! includes `repo:read`.
1314 //!
1415 //! This module is the one source of truth: the API (REST and MCP) and git
1516 //! enforce it, and identity stores it. `packages/contracts/src/scopes.ts`
170171 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
171172 pub enum Level {
172173 Read,
174+ /// Making something new without changing what is there: only
175+ /// `repo:create`, which creates repositories and nothing more.
176+ Create,
173177 Write,
174178 /// Starting g1t's own agents: only `agents:run`, which is retired.
175179 Run,
182186 pub fn as_str(self) -> &'static str {
183187 match self {
184188 Level::Read => "read",
189+ Level::Create => "create",
185190 Level::Write => "write",
186191 Level::Run => "run",
187192 Level::Delete => "delete",
195200 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
196201 pub enum Scope {
197202 RepoRead,
203+ /// Creating repositories, and nothing that changes or deletes one: what
204+ /// the Agent preset gives, so an agent can start a repository.
205+ RepoCreate,
198206 RepoWrite,
199207 RepoAdmin,
200208 CodeRead,
253261
254262 impl Scope {
255263 /// Every scope, grouped by resource, least first.
256− pub const ALL: [Scope; 49] = [
264+ pub const ALL: [Scope; 50] = [
257265 Scope::RepoRead,
266+ Scope::RepoCreate,
258267 Scope::RepoWrite,
259268 Scope::RepoAdmin,
260269 Scope::CodeRead,
308317 pub fn as_str(self) -> &'static str {
309318 match self {
310319 Scope::RepoRead => "repo:read",
320+ Scope::RepoCreate => "repo:create",
311321 Scope::RepoWrite => "repo:write",
312322 Scope::RepoAdmin => "repo:admin",
313323 Scope::CodeRead => "code:read",
374384
375385 pub fn level(self) -> Level {
376386 match self.as_str().rsplit_once(':').map_or("", |(_, level)| level) {
387+ "create" => Level::Create,
377388 "write" => Level::Write,
378389 "run" => Level::Run,
379390 "delete" => Level::Delete,
398409 pub fn describe(self) -> &'static str {
399410 match self {
400411 Scope::RepoRead => "See repositories, their settings, labels, timelines, releases, languages, contributors and security alerts, and search",
412+ Scope::RepoCreate => "Create repositories, and nothing that changes or deletes one",
401413 Scope::RepoWrite => "Create repositories, create, rename and delete branches, change how pull requests merge and publish releases",
402414 Scope::RepoAdmin => "Rename, archive, transfer, delete or change who can see a repository, change its rulesets, and dismiss security alerts",
403415 Scope::CodeRead => "Clone and fetch private repositories with git",
652664 let mut scopes: Vec<Scope> = reads().filter(|scope| scope.resource() != Resource::Runners).collect();
653665 // And answering what needs the person it works for: marking
654666 // it done, subscribing, watching.
667+ // And starting a repository for what it builds, which changes
668+ // nothing already there: `repo:write` would let it change
669+ // merge settings and delete branches and releases.
655670 scopes.extend([
671+ Scope::RepoCreate,
656672 Scope::CodeWrite,
657673 Scope::IssuesWrite,
658674 Scope::PullRequestsWrite,
10471063 ("list_deleted_repos", Scope::RepoRead),
10481064 ("list_security_alerts", Scope::RepoRead),
10491065 ("get_codeowners_errors", Scope::RepoRead),
1050− ("create_repo", Scope::RepoWrite),
1066+ // Creating one changes nothing already there: `repo:create`, which
1067+ // `repo:write` includes.
1068+ ("create_repo", Scope::RepoCreate),
10511069 ("update_repo", Scope::RepoWrite),
10521070 ("update_project", Scope::RepoWrite),
10531071 ("update_repo_settings", Scope::RepoWrite),
15431561 let (needed, doing) = match level {
15441562 Level::Read => (Scope::PackagesRead, "pull a private package"),
15451563 Level::Delete | Level::Admin => (Scope::PackagesDelete, "delete packages"),
1546− Level::Write | Level::Run => (Scope::PackagesWrite, "push or publish packages"),
1564+ Level::Create | Level::Write | Level::Run => (Scope::PackagesWrite, "push or publish packages"),
15471565 };
15481566 if !access.allows(needed) && !(level == Level::Read && public) {
15491567 return Decision::deny(
15841602 fn a_higher_level_includes_the_lower_ones_of_its_resource_only() {
15851603 assert!(Scope::RepoAdmin.includes(Scope::RepoRead));
15861604 assert!(Scope::RepoAdmin.includes(Scope::RepoWrite));
1605+ assert!(Scope::RepoWrite.includes(Scope::RepoCreate));
1606+ assert!(Scope::RepoCreate.includes(Scope::RepoRead));
1607+ assert!(!Scope::RepoCreate.includes(Scope::RepoWrite));
15871608 assert!(Scope::IssuesWrite.includes(Scope::IssuesRead));
15881609 assert!(!Scope::IssuesRead.includes(Scope::IssuesWrite));
15891610 assert!(!Scope::RepoAdmin.includes(Scope::CodeWrite));
17251746 }
17261747
17271748 #[test]
1749+ fn the_agent_preset_creates_repositories_and_changes_none() {
1750+ let agent = Preset::Agent.scopes().unwrap();
1751+ assert!(agent.contains(&Scope::RepoCreate));
1752+ assert!(!agent.contains(&Scope::RepoWrite) && !agent.contains(&Scope::RepoAdmin));
1753+ assert!(oauth_default().contains(&Scope::RepoCreate));
1754+ assert_eq!(Scope::parse("repo:create"), Some(Scope::RepoCreate));
1755+ assert_eq!(Scope::RepoCreate.level(), Level::Create);
1756+ assert!(!Scope::RepoCreate.dangerous());
1757+ // Creating needs repo:create, which repo:write still gives.
1758+ assert_eq!(scope_for("create_repo"), Some(Scope::RepoCreate));
1759+ let creator = token(&agent);
1760+ assert!(decide(&creator, "create_repo", &json!({ "name": "web" })).allowed);
1761+ assert!(decide(&token(&[Scope::RepoWrite]), "create_repo", &json!({ "name": "web" })).allowed);
1762+ assert!(!decide(&token(&[Scope::RepoRead]), "create_repo", &json!({ "name": "web" })).allowed);
1763+ // And nothing that changes or deletes what is there.
1764+ for op in [
1765+ "update_repo",
1766+ "update_repo_settings",
1767+ "delete_branch",
1768+ "rename_branch",
1769+ "delete_release",
1770+ "fork_repo",
1771+ "set_repo_visibility",
1772+ "delete_repo",
1773+ "transfer_repo",
1774+ ] {
1775+ assert!(!decide(&creator, op, &json!({ "repo": "acme/web" })).allowed, "{op}");
1776+ }
1777+ // As a permission it is one level of repositories.
1778+ let asked = std::collections::BTreeMap::from([("repo".to_owned(), "create".to_owned())]);
1779+ assert_eq!(resolve_permissions(&asked, true), Ok(vec![Scope::RepoCreate]));
1780+ assert_eq!(top_scopes(&[Scope::RepoRead, Scope::RepoCreate]), vec![Scope::RepoCreate]);
1781+ }
1782+
1783+ #[test]
17281784 fn checks_are_reported_with_checks_write_which_ci_gets() {
17291785 assert_eq!(scope_for("create_check_run"), Some(Scope::ChecksWrite));
17301786 assert_eq!(scope_for("create_commit_status"), Some(Scope::ChecksWrite));
+1−1
41624162 path: "/repos",
41634163 tag: "Repositories",
41644164 summary: "Create a repository",
4165− scope: Some("repo:write"),
4165+ scope: Some("repo:create"),
41664166 params: &[],
41674167 body: &[
41684168 Field { name: "description", kind: "string", required: false, values: &[] },
+7−6
3535 | "models"
3636 | "artifacts";
3737
38−export type ScopeLevel = "read" | "write" | "run" | "delete" | "admin";
38+export type ScopeLevel = "read" | "create" | "write" | "run" | "delete" | "admin";
3939
4040 /** Every scope, grouped by resource, least first. */
4141 export const SCOPES = [
4242 { scope: "repo:read", description: "See repositories, their settings, labels, timelines, releases, languages, contributors and security alerts, and search" },
43+ { scope: "repo:create", description: "Create repositories, and nothing that changes or deletes one" },
4344 { scope: "repo:write", description: "Create repositories, create, rename and delete branches, change how pull requests merge and publish releases" },
4445 { scope: "repo:admin", description: "Rename, archive, transfer, delete or change who can see a repository, change its rulesets, and dismiss security alerts" },
4546 { scope: "code:read", description: "Clone and fetch private repositories with git" },
152153 { resource: "models", label: "AI Gateway", group: "workspace" },
153154 ];
154155
155−const LEVEL_ORDER: Record<ScopeLevel, number> = { read: 0, write: 1, run: 2, delete: 3, admin: 4 };
156+const LEVEL_ORDER: Record<ScopeLevel, number> = { read: 0, create: 1, write: 2, run: 3, delete: 4, admin: 5 };
156157
157158 export function scopeResource(scope: Scope): ScopeResource {
158159 return scope.split(":")[0] as ScopeResource;
254255 "repo:read", "code:read", "security:read", "packages:read", "issues:read", "pull_requests:read", "workflows:read", "checks:read", "deployments:read", "observability:read", "account:read", "notifications:read", "workspace:read", "billing:read", "access:read", "webhooks:read", "secrets:read", "runners:read", "artifacts:read",
255256 ] as const,
256257 agent: [
257− "repo:read", "code:read", "code:write", "security:read", "packages:read", "issues:read", "issues:write", "pull_requests:read", "pull_requests:write", "workflows:read", "checks:read", "deployments:read", "observability:read", "account:read", "notifications:read", "notifications:write", "workspace:read", "billing:read", "access:read", "webhooks:read", "secrets:read", "artifacts:read",
258+ "repo:read", "repo:create", "code:read", "code:write", "security:read", "packages:read", "issues:read", "issues:write", "pull_requests:read", "pull_requests:write", "workflows:read", "checks:read", "deployments:read", "observability:read", "account:read", "notifications:read", "notifications:write", "workspace:read", "billing:read", "access:read", "webhooks:read", "secrets:read", "artifacts:read",
258259 ] as const,
259260 ci: [
260261 "repo:read", "code:read", "code:write", "packages:read", "packages:write", "workflows:read", "workflows:write", "checks:read", "checks:write", "deployments:read", "deployments:write",
266267
267268 export const PRESETS: { id: PresetId; label: string; description: string }[] = [
268269 { id: "read_only", label: "Read only", description: "Read everything you can read; change nothing." },
269− { id: "agent", label: "Agent", description: "Read everything, work on issues and pull requests, and push code." },
270+ { id: "agent", label: "Agent", description: "Read everything, work on issues and pull requests, push code, and create repositories." },
270271 { id: "ci", label: "CI", description: "Clone and push code, push and pull packages, run workflows, and report checks and deployments." },
271272 { id: "full", label: "Full access", description: "Everything you can do, including deleting repositories and changing who has access." },
272273 ];
416417 ["list_deleted_repos", "repo:read"],
417418 ["list_security_alerts", "repo:read"],
418419 ["get_codeowners_errors", "repo:read"],
419− ["create_repo", "repo:write"],
420+ ["create_repo", "repo:create"],
420421 ["update_repo", "repo:write"],
421422 ["update_project", "repo:write"],
422423 ["update_repo_settings", "repo:write"],
732733 * own (see `DANGEROUS_SCOPES`). Every other scope is in exactly one group.
733734 */
734735 export const SCOPE_GROUPS: { id: string; label: string; scopes: Scope[] }[] = [
735− { id: "code", label: "Repositories & code", scopes: ["repo:read", "repo:write", "code:read", "code:write"] },
736+ { id: "code", label: "Repositories & code", scopes: ["repo:read", "repo:create", "repo:write", "code:read", "code:write"] },
736737 { id: "security", label: "Security", scopes: ["security:read", "security:write"] },
737738 { id: "packages", label: "Packages", scopes: ["packages:read", "packages:write"] },
738739 { id: "work", label: "Issues & pull requests", scopes: ["issues:read", "issues:write", "pull_requests:read", "pull_requests:write"] },

Comments

Sign in to comment.