Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA
Conflicts: Op::ALL is 281 (the run-protection operations and the five member operations); identity keeps the job-token RPCs beside the audited remove_access_token. Fixed on the way, both found by CI's own commands on the merged tree: - DEFAULT_MEMBER_PRIVILEGES lives in contracts' access.ts, its only user, so access.ts has no value import without an extension: services that test with plain `node --test` (context, deployments, projects) could not load it. - The routing-savings tests price on fixed tiers. They read the live routing, so Haiku 5.5 on the fast tier (and plans starting there) broke their arithmetic and would have failed every pull request's CI.
| 1007 | 1007 | "g1t-secrets", | |
| 1008 | 1008 | "getrandom 0.2.17", | |
| 1009 | 1009 | "hex", | |
| 1010 | + | "hmac 0.12.1", | |
| 1010 | 1011 | "pbkdf2 0.12.2", | |
| 1011 | 1012 | "serde", | |
| 1012 | 1013 | "serde_json", | |
| 1014 | + | "sha1 0.10.7", | |
| 1013 | 1015 | "sha2 0.10.9", | |
| 1014 | 1016 | "worker", | |
| 1015 | 1017 | ] |
| 61 | 61 | &[Op::GetWorkspace, Op::CreateWorkspace, Op::UpdateWorkspace, Op::DeleteWorkspace], | |
| 62 | 62 | ), | |
| 63 | 63 | ( | |
| 64 | + | "Members", | |
| 65 | + | "A workspace's members and owners: who belongs to it, their roles (owner or member, with billing manager and security manager on top), handing it to another member, and leaving it.", | |
| 66 | + | &[Op::ListMembers, Op::UpdateMember, Op::RemoveMember, Op::TransferOwnership, Op::LeaveWorkspace], | |
| 67 | + | ), | |
| 68 | + | ( | |
| 64 | 69 | "Invites", | |
| 65 | 70 | "While g1t is invite-only, every new account needs an invite. Your invites, and inviting people into a workspace by email.", | |
| 66 | 71 | &[ | |
| 481 | 486 | Op::CreateWorkspace => "Create a workspace", | |
| 482 | 487 | Op::DeleteWorkspace => "Delete a workspace", | |
| 483 | 488 | Op::UpdateWorkspace => "Update a workspace", | |
| 489 | + | Op::ListMembers => "List a workspace's members", | |
| 490 | + | Op::UpdateMember => "Change a member's role", | |
| 491 | + | Op::RemoveMember => "Remove a member", | |
| 492 | + | Op::TransferOwnership => "Transfer a workspace's ownership", | |
| 493 | + | Op::LeaveWorkspace => "Leave a workspace", | |
| 484 | 494 | Op::ListEmails => "List your email addresses", | |
| 485 | 495 | Op::AddEmail => "Add an email address", | |
| 486 | 496 | Op::RemoveEmail => "Remove an email address", |
| 101 | 101 | CreateWorkspace, | |
| 102 | 102 | DeleteWorkspace, | |
| 103 | 103 | UpdateWorkspace, | |
| 104 | + | ListMembers, | |
| 105 | + | UpdateMember, | |
| 106 | + | RemoveMember, | |
| 107 | + | TransferOwnership, | |
| 108 | + | LeaveWorkspace, | |
| 104 | 109 | ListEmails, | |
| 105 | 110 | AddEmail, | |
| 106 | 111 | RemoveEmail, | |
| 652 | 657 | } | |
| 653 | 658 | ||
| 654 | 659 | impl Op { | |
| 655 | − | pub const ALL: [Op; 276] = [ | |
| 660 | + | pub const ALL: [Op; 281] = [ | |
| 656 | 661 | Op::Whoami, | |
| 657 | 662 | Op::GetWorkspace, | |
| 658 | 663 | Op::CreateWorkspace, | |
| 659 | 664 | Op::DeleteWorkspace, | |
| 660 | 665 | Op::UpdateWorkspace, | |
| 666 | + | Op::ListMembers, | |
| 667 | + | Op::UpdateMember, | |
| 668 | + | Op::RemoveMember, | |
| 669 | + | Op::TransferOwnership, | |
| 670 | + | Op::LeaveWorkspace, | |
| 661 | 671 | Op::ListEmails, | |
| 662 | 672 | Op::AddEmail, | |
| 663 | 673 | Op::RemoveEmail, | |
| 943 | 953 | Op::CreateWorkspace => "create_workspace", | |
| 944 | 954 | Op::DeleteWorkspace => "delete_workspace", | |
| 945 | 955 | Op::UpdateWorkspace => "update_workspace", | |
| 956 | + | Op::ListMembers => "list_members", | |
| 957 | + | Op::UpdateMember => "update_member", | |
| 958 | + | Op::RemoveMember => "remove_member", | |
| 959 | + | Op::TransferOwnership => "transfer_ownership", | |
| 960 | + | Op::LeaveWorkspace => "leave_workspace", | |
| 946 | 961 | Op::ListEmails => "list_emails", | |
| 947 | 962 | Op::AddEmail => "add_email", | |
| 948 | 963 | Op::RemoveEmail => "remove_email", | |
| 1167 | 1182 | "Delete a workspace and everything in it. Owners only, signed in as a person, and confirm must be the workspace's slug. Billing must be able to settle it: no unpaid invoice, no prepaid credit left, and no usage this month still being metered; what it owes is charged to its card at once and its plan ends. Its repositories, projects and apps go with it at once, nobody can reach it, and its access tokens stop working. It is kept for 30 days, when g1t's support can restore it as it was; then it is purged, with its webhooks, integrations and workspace secrets. Its statements, invoices and audit log are kept. The slug is never given to another workspace; the person whose username it is may create it again once it is purged. Some workspaces, such as Flagon's, can never be deleted." | |
| 1168 | 1183 | } | |
| 1169 | 1184 | Op::GetWorkspace => { | |
| 1170 | − | "One workspace you belong to: its name, description and member count, what every member gets on each of its repositories (base_permission), and who may create its teams (team_creation: members or owners). Members only." | |
| 1185 | + | "One workspace you belong to: its name, description and member count, what every member gets on each of its repositories (base_permission), who may create its teams (team_creation: members or owners), its member privileges (members_can_create_public_repositories, members_can_create_private_repositories, members_can_change_repo_visibility, members_can_delete_repositories, members_can_invite_outside_collaborators), and whether it requires two-factor authentication (two_factor_requirement_enabled). Members only." | |
| 1171 | 1186 | } | |
| 1172 | 1187 | Op::UpdateWorkspace => { | |
| 1173 | − | "Change a workspace's display name and description, what every member gets on each of its repositories (base_permission: none, read, write or admin), and who may create its teams (team_creation: members or owners). Only the fields given are changed; give at least one. An empty name falls back to the slug, which this never changes (that is a rename, on Settings); an empty description clears it. Owners only, signed in as a person. Returns the workspace as it is now." | |
| 1188 | + | "Change a workspace's display name and description, what every member gets on each of its repositories (base_permission: none, read, write or admin), who may create its teams (team_creation: members or owners), its member privileges, and whether it requires two-factor authentication. The member privileges are: members_can_create_public_repositories and members_can_create_private_repositories (who may create each kind; owners always can), members_can_change_repo_visibility (members with the Admin role on a repository may make it public or private), members_can_delete_repositories (they may delete or transfer it) and members_can_invite_outside_collaborators (they may give a role to someone outside the workspace). two_factor_requirement_enabled true holds every member and outside collaborator without two-factor authentication out of the workspace until they turn it on; you need it on yourself first. Only the fields given are changed; give at least one. An empty name falls back to the slug, which this never changes (that is a rename, on Settings); an empty description clears it. Owners only, signed in as a person. Returns the workspace as it is now." | |
| 1189 | + | } | |
| 1190 | + | Op::ListMembers => { | |
| 1191 | + | "A workspace's members, owners first, then by username. Each has their `username`, `name`, `avatar`, `role` (`owner` or `member`), the roles they hold besides it (`org_roles`: `billing_manager`, `security_manager`), and, when an owner asks, whether they have two-factor authentication on (`two_factor`; null for anyone else). Members only." | |
| 1192 | + | } | |
| 1193 | + | Op::UpdateMember => { | |
| 1194 | + | "Change a member's role in a workspace: `role` (`owner` or `member`) and the roles they hold besides it (`org_roles`, a list of `billing_manager` and `security_manager`, which replaces the one they have). Only the fields given are changed. A billing manager manages the workspace's billing as an owner does, and gets nothing on repositories from it; a security manager reads every repository and sees and manages its security alerts and security settings. Refused with `409` when it would leave the workspace without an owner. Owners only, signed in as a person. Returns the member." | |
| 1195 | + | } | |
| 1196 | + | Op::RemoveMember => { | |
| 1197 | + | "Remove someone from a workspace. Their roles on its repositories and their place in its teams go too; to keep them on a repository, add them back to it as an outside collaborator. Removing yourself is leaving (leave_workspace). Refused with `409` for the last owner. Owners only, signed in as a person." | |
| 1198 | + | } | |
| 1199 | + | Op::TransferOwnership => { | |
| 1200 | + | "Hand a workspace to another of its members: they become an owner and you a member, in one step. A workspace can have several owners; to add one without stepping down, use update_member with role owner. Owners only, signed in as a person." | |
| 1201 | + | } | |
| 1202 | + | Op::LeaveWorkspace => { | |
| 1203 | + | "Leave a workspace you belong to. Your roles on its repositories and your place in its teams go too. The last owner cannot leave (`409`): make another member an owner first, or delete the workspace. People only." | |
| 1174 | 1204 | } | |
| 1175 | 1205 | Op::ListRepos => "Repositories you can see, optionally filtered by a search query.", | |
| 1176 | 1206 | Op::GetRepo => "One repository's details.", | |
| 1177 | 1207 | Op::UpdateRepo => { | |
| 1178 | − | "Change a repository's description, website, topics and default branch, whether its default branch is protected, and whether it is private. Only the fields given are changed. Its description, website and topics, and protecting its default branch, need the Maintain role or higher; making it public or private and changing its default branch need the Admin role, and a free workspace takes a private repository only while its private storage has room. A protected branch refuses pushes and changes only by merging a pull request. A new default branch must already exist; open pull requests then merge into it." | |
| 1208 | + | "Change a repository's description, website, topics and default branch, whether its default branch is protected, and whether it is private. Only the fields given are changed. Its description, website and topics need the Maintain role or higher; protecting its default branch, making it public or private and changing its default branch need the Admin role (and making it public or private, the workspace's member privileges to allow it, unless you are an owner), and a free workspace takes a private repository only while its private storage has room. A protected branch refuses pushes and changes only by merging a pull request. A new default branch must already exist; open pull requests then merge into it." | |
| 1179 | 1209 | } | |
| 1180 | 1210 | Op::RenameRepo => { | |
| 1181 | 1211 | "Give a repository a new name in its workspace. Needs the Admin role. Everything stays with it: git data, issues, pull requests, workflow runs, deployments, secrets and webhooks. Its old address keeps working: web pages, git remotes and API calls redirect to the new one until a repository is made at the old address. The new name must be free in the workspace, including names held by recently deleted repositories." | |
| 1190 | 1220 | "Unarchive a repository: make it writable again. Needs the Admin role. Pushes, merges, issues, pull requests, agents and workflows work again; nothing that was refused while it was archived runs by itself." | |
| 1191 | 1221 | } | |
| 1192 | 1222 | Op::SetRepoVisibility => { | |
| 1193 | − | "Make a repository public or private. Needs the Admin role, and confirm must be its full name, owner/name. Making it public shows it, its code, issues and pull requests to everyone and adds it to search for everyone. Making it private hides it from everyone without a role on it; a free workspace takes it only while its private storage has room. Nothing else about it changes." | |
| 1223 | + | "Make a repository public or private. Needs the Admin role, and confirm must be its full name, owner/name. Unless you are an owner of its workspace, the workspace's member privileges must let repository admins change visibility (members_can_change_repo_visibility) and let members create a repository of that kind. Making it public shows it, its code, issues and pull requests to everyone and adds it to search for everyone. Making it private hides it from everyone without a role on it; a free workspace takes it only while its private storage has room. Nothing else about it changes." | |
| 1194 | 1224 | } | |
| 1195 | 1225 | Op::DeleteRepo => { | |
| 1196 | 1226 | "Delete a repository. Owners only, and confirm must be its full name, owner/name. It disappears at once: git refuses it, agents and workflows stop, its deployments are taken down, and search drops it. For 30 days an owner can restore it with restore_repo, as it was; then it is purged, its git data with it. Its name stays taken until it is purged. list_deleted_repos shows what can be restored." | |
| 1211 | 1241 | "How a repository handles pull requests: how g1t's agents are reviewed, revised and merged, and its default branch's protection as the rules of its rulesets stack there: the checks that must pass (required_checks), the approvals a merge needs, whether its code owners must approve (`require_code_owner_review`), whether required checks can be bypassed, whether a pull request must be up to date, and the merge queue. The same rules hold for a person's pull request and an agent's. list_repo_rulesets and get_branch_rules show every rule." | |
| 1212 | 1242 | } | |
| 1213 | 1243 | Op::UpdateRepoSettings => { | |
| 1214 | − | "Change how a repository handles pull requests. Only the fields given are changed; required_checks replaces the whole list. The branch protection fields (required_checks, require_up_to_date, required_approvals, count_agent_approvals, allow_ignoring_checks, merge_queue, require_code_owner_review) are written to the repository's \"Default branch protection\" ruleset, made when it has none; rules only rulesets have stay as they are. A required check is named as list_check_names gives it: a workflow's name, such as CI, or another status's context, such as g1t / deploy. Needs the Maintain role or higher." | |
| 1244 | + | "Change how a repository handles pull requests. Only the fields given are changed; required_checks replaces the whole list. The branch protection fields (required_checks, require_up_to_date, required_approvals, count_agent_approvals, allow_ignoring_checks, merge_queue, require_code_owner_review) are written to the repository's \"Default branch protection\" ruleset, made when it has none; rules only rulesets have stay as they are. A required check is named as list_check_names gives it: a workflow's name, such as CI, or another status's context, such as g1t / deploy. Needs the Maintain role or higher, and the Admin role to change a branch protection field." | |
| 1215 | 1245 | } | |
| 1216 | 1246 | Op::ListCheckNames => { | |
| 1217 | 1247 | "The check names reported on a repository's commits in the last 30 days, most recent first, with the events each was reported for: the names update_repo_settings takes in required_checks. A workflow's runs report a check named after the workflow; a check required on the default branch must be reported on a pull request's head (pull_request events) and, with the merge queue on, on its queued state (merge_group events)." | |
| 1281 | 1311 | "A repository's labels, by name: each one's color (six hex digits), description, and how many issues and pull requests carry it. A new repository starts with bug, documentation, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix, dependencies and security." | |
| 1282 | 1312 | } | |
| 1283 | 1313 | Op::CreateLabel => { | |
| 1284 | − | "Create a label, named by label. Names are lowercase and unique in a repository, at most 50 characters; color is six hex digits (one is chosen from the name when left out), description at most 100 characters. Needs the Triage role or higher." | |
| 1314 | + | "Create a label, named by label. Names are lowercase and unique in a repository, at most 50 characters; color is six hex digits (one is chosen from the name when left out), description at most 100 characters. Needs the Write role or higher; applying labels and milestones needs Triage." | |
| 1285 | 1315 | } | |
| 1286 | 1316 | Op::UpdateLabel => { | |
| 1287 | − | "Change a label's name, color or description; only the fields given change. Renaming it renames it on every issue and pull request that carries it. Needs the Triage role or higher." | |
| 1317 | + | "Change a label's name, color or description; only the fields given change. Renaming it renames it on every issue and pull request that carries it. Needs the Write role or higher; applying labels and milestones needs Triage." | |
| 1288 | 1318 | } | |
| 1289 | 1319 | Op::DeleteLabel => { | |
| 1290 | − | "Delete a label. It is taken off every issue and pull request that carries it, without events for each. Needs the Triage role or higher." | |
| 1320 | + | "Delete a label. It is taken off every issue and pull request that carries it, without events for each. Needs the Write role or higher; applying labels and milestones needs Triage." | |
| 1291 | 1321 | } | |
| 1292 | 1322 | Op::AddDefaultLabels => { | |
| 1293 | − | "Add the default labels a repository does not have yet: bug, documentation, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix, dependencies and security. Labels it has already are left as they are. Returns every label it has now. Needs the Triage role or higher." | |
| 1323 | + | "Add the default labels a repository does not have yet: bug, documentation, duplicate, enhancement, good first issue, help wanted, invalid, question, wontfix, dependencies and security. Labels it has already are left as they are. Returns every label it has now. Needs the Write role or higher; applying labels and milestones needs Triage." | |
| 1294 | 1324 | } | |
| 1295 | 1325 | Op::ListIssueLabels => { | |
| 1296 | 1326 | "The labels an issue or a pull request carries, with their colors and descriptions. Issues and pull requests share numbers." | |
| 1297 | 1327 | } | |
| 1298 | 1328 | Op::AddIssueLabels => { | |
| 1299 | − | "Add labels to an issue or a pull request, keeping the ones it has. A name the repository does not have yet is created when you have the Triage role or higher; without it, you may use the repository's labels on what you opened. Each label added is an issue.labeled or pull.labeled event. Returns its labels now, at most 20." | |
| 1329 | + | "Add labels to an issue or a pull request, keeping the ones it has. A name the repository does not have yet is created when you have the Write role or higher; without it, you may use the repository's labels on what you opened. Each label added is an issue.labeled or pull.labeled event. Returns its labels now, at most 20." | |
| 1300 | 1330 | } | |
| 1301 | 1331 | Op::SetIssueLabels => { | |
| 1302 | 1332 | "Replace the labels of an issue or a pull request with these; an empty list takes them all off. The same rules as add_issue_labels. Returns its labels now." | |
| 1309 | 1339 | } | |
| 1310 | 1340 | Op::GetMilestone => "A milestone, with every issue and pull request in it, newest first.", | |
| 1311 | 1341 | Op::CreateMilestone => { | |
| 1312 | − | "Create a milestone: a title, unique in the repository, at most 100 characters; a description in Markdown; and a due_on day (YYYY-MM-DD). Milestones are numbered from 1 in each repository, apart from issues. Needs the Triage role or higher." | |
| 1342 | + | "Create a milestone: a title, unique in the repository, at most 100 characters; a description in Markdown; and a due_on day (YYYY-MM-DD). Milestones are numbered from 1 in each repository, apart from issues. Needs the Write role or higher; applying labels and milestones needs Triage." | |
| 1313 | 1343 | } | |
| 1314 | 1344 | Op::UpdateMilestone => { | |
| 1315 | − | "Change a milestone's title, description, due date or state (open or closed); only the fields given change, and due_on \"\" clears its due date. Needs the Triage role or higher." | |
| 1345 | + | "Change a milestone's title, description, due date or state (open or closed); only the fields given change, and due_on \"\" clears its due date. Needs the Write role or higher; applying labels and milestones needs Triage." | |
| 1316 | 1346 | } | |
| 1317 | 1347 | Op::DeleteMilestone => { | |
| 1318 | − | "Delete a milestone. The issues and pull requests in it are in no milestone afterwards. Needs the Triage role or higher." | |
| 1348 | + | "Delete a milestone. The issues and pull requests in it are in no milestone afterwards. Needs the Write role or higher; applying labels and milestones needs Triage." | |
| 1319 | 1349 | } | |
| 1320 | 1350 | Op::AddComment => { | |
| 1321 | 1351 | "Comment on an issue or a pull request. On a pull request, give path and line to comment on one line of the change." | |
| 1480 | 1510 | "Decline an invitation to a repository sent to you. Whoever sent it can invite you again. People only." | |
| 1481 | 1511 | } | |
| 1482 | 1512 | Op::SetBasePermission => { | |
| 1483 | − | "Set what every member of a workspace gets on each of its repositories: none, read, write (the default) or admin. Owners always have Admin, and a role given on a repository directly still counts where it is higher. With none, members see only the private repositories they are given a role on. Owners only, signed in as a person." | |
| 1513 | + | "Set what every member of a workspace gets on each of its repositories: none, read (what a new workspace starts with), write or admin. Owners always have Admin, and a role given on a repository directly still counts where it is higher. With none, members see only the private repositories they are given a role on. Owners only, signed in as a person." | |
| 1484 | 1514 | } | |
| 1485 | 1515 | Op::ListOutsideCollaborators => { | |
| 1486 | 1516 | "The people with a role on some of a workspace's repositories who are not its members, each with the repositories they can reach and their role on each. Owners only." | |
| 1489 | 1519 | "A repository's security alerts: secrets found in what was pushed or in its history (`kind` `secret`), and dependencies with a known vulnerability (`kind` `dependency`), secrets first. Each has a `state`: `open`, `dismissed` (someone said why it can stay) or `fixed` (a secret revoked, a dependency no longer vulnerable). Filter with `state` and `kind`; both are left out for all. A secret is never returned, only a `preview`. Needs the Write role on the repository; anyone else is told it does not exist, whether or not the repository is public." | |
| 1490 | 1520 | } | |
| 1491 | 1521 | Op::DismissSecurityAlert => { | |
| 1492 | − | "Dismiss an alert with a reason and an optional comment. A secret takes false_positive, used_in_tests, revoked or wont_fix; a dependency takes fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used. A dismissed secret is let through push protection from then on, unless the reason is `revoked`, which marks it fixed, so dismissing a secret needs the Admin role on the repository; a dependency needs Write. Returns the alert as it is now. Reopen it with reopen_security_alert." | |
| 1522 | + | "Dismiss an alert with a reason and an optional comment. A secret takes false_positive, used_in_tests, revoked or wont_fix; a dependency takes fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used. A dismissed secret is let through push protection from then on, unless the reason is `revoked`, which marks it fixed. Dismissing either needs the Write role on the repository, or a security manager of its workspace. Returns the alert as it is now. Reopen it with reopen_security_alert." | |
| 1493 | 1523 | } | |
| 1494 | 1524 | Op::ReopenSecurityAlert => { | |
| 1495 | 1525 | "Open a dismissed alert again. A reopened secret stops pushes that carry it again. The same roles as dismissing: Admin for a secret, Write for a dependency. Returns the alert as it is now." | |
| 1764 | 1794 | "enum": g1t_contracts::teams::TeamCreation::ALL.map(|setting| setting.as_str()), | |
| 1765 | 1795 | "description": "Who may create the workspace's teams: members (any member, the default) or owners (owners only).", | |
| 1766 | 1796 | }, | |
| 1797 | + | "members_can_create_public_repositories": { | |
| 1798 | + | "type": "boolean", | |
| 1799 | + | "description": "Members may create public repositories. Owners always can. On by default.", | |
| 1800 | + | }, | |
| 1801 | + | "members_can_create_private_repositories": { | |
| 1802 | + | "type": "boolean", | |
| 1803 | + | "description": "Members may create private repositories. Owners always can. On by default.", | |
| 1804 | + | }, | |
| 1805 | + | "members_can_change_repo_visibility": { | |
| 1806 | + | "type": "boolean", | |
| 1807 | + | "description": "Members with the Admin role on a repository may make it public or private. On by default; off, only owners can.", | |
| 1808 | + | }, | |
| 1809 | + | "members_can_delete_repositories": { | |
| 1810 | + | "type": "boolean", | |
| 1811 | + | "description": "Members with the Admin role on a repository may delete or transfer it. Off by default: only owners can.", | |
| 1812 | + | }, | |
| 1813 | + | "members_can_invite_outside_collaborators": { | |
| 1814 | + | "type": "boolean", | |
| 1815 | + | "description": "Members with the Admin role on a repository may give a role on it to someone outside the workspace. On by default; off, only owners can.", | |
| 1816 | + | }, | |
| 1817 | + | "two_factor_requirement_enabled": { | |
| 1818 | + | "type": "boolean", | |
| 1819 | + | "description": "Require two-factor authentication of every member and outside collaborator. Those without it keep their place but cannot use the workspace until they turn it on. You need it on yourself first.", | |
| 1820 | + | }, | |
| 1767 | 1821 | }), | |
| 1768 | 1822 | &["workspace"], | |
| 1769 | 1823 | ), | |
| 1824 | + | Op::ListMembers | Op::LeaveWorkspace => object(json!({ "workspace": workspace_schema() }), &["workspace"]), | |
| 1825 | + | Op::UpdateMember => object( | |
| 1826 | + | json!({ | |
| 1827 | + | "workspace": workspace_schema(), | |
| 1828 | + | "username": { "type": "string", "description": "The member's username." }, | |
| 1829 | + | "role": { | |
| 1830 | + | "type": "string", | |
| 1831 | + | "enum": ["owner", "member"], | |
| 1832 | + | "description": "owner or member.", | |
| 1833 | + | }, | |
| 1834 | + | "org_roles": { | |
| 1835 | + | "type": "array", | |
| 1836 | + | "items": { "type": "string", "enum": g1t_contracts::OrgRole::ALL.map(|role| role.as_str()) }, | |
| 1837 | + | "description": "The roles they hold besides owner or member: billing_manager, security_manager. Replaces the list; [] takes them all away.", | |
| 1838 | + | }, | |
| 1839 | + | }), | |
| 1840 | + | &["workspace", "username"], | |
| 1841 | + | ), | |
| 1842 | + | Op::RemoveMember | Op::TransferOwnership => object( | |
| 1843 | + | json!({ | |
| 1844 | + | "workspace": workspace_schema(), | |
| 1845 | + | "username": { "type": "string", "description": "The member's username." }, | |
| 1846 | + | }), | |
| 1847 | + | &["workspace", "username"], | |
| 1848 | + | ), | |
| 1770 | 1849 | Op::TransferRepo => object( | |
| 1771 | 1850 | json!({ | |
| 1772 | 1851 | "repo": repo_schema(), | |
| 1804 | 1883 | "labels": { | |
| 1805 | 1884 | "type": "array", | |
| 1806 | 1885 | "items": { "type": "string" }, | |
| 1807 | − | "description": "Label names, e.g. [\"bug\", \"help wanted\"]. Names the repository does not have yet are created for someone with the Triage role.", | |
| 1886 | + | "description": "Label names, e.g. [\"bug\", \"help wanted\"]. Names the repository does not have yet are created for someone with the Write role.", | |
| 1808 | 1887 | }, | |
| 1809 | 1888 | })), | |
| 1810 | 1889 | &["repo", "number", "labels"], | |
| 2105 | 2184 | "labels": { | |
| 2106 | 2185 | "type": "array", | |
| 2107 | 2186 | "items": { "type": "string" }, | |
| 2108 | − | "description": "What kind of issue this is, e.g. \"bug\" or \"enhancement\": the repository's labels, as list_labels gives them. A name it does not have yet is created for someone with the Triage role.", | |
| 2187 | + | "description": "What kind of issue this is, e.g. \"bug\" or \"enhancement\": the repository's labels, as list_labels gives them. A name it does not have yet is created for someone with the Write role.", | |
| 2109 | 2188 | }, | |
| 2110 | 2189 | "checks": { | |
| 2111 | 2190 | "type": "array", | |
| 2124 | 2203 | "labels": { | |
| 2125 | 2204 | "type": "array", | |
| 2126 | 2205 | "items": { "type": "string" }, | |
| 2127 | − | "description": "Replaces the whole set. Names the repository does not have yet are created for someone with the Triage role.", | |
| 2206 | + | "description": "Replaces the whole set. Names the repository does not have yet are created for someone with the Write role.", | |
| 2128 | 2207 | }, | |
| 2129 | 2208 | "milestone": { | |
| 2130 | 2209 | "type": ["integer", "null"], | |
| 3090 | 3169 | | Op::CreateWorkspace | |
| 3091 | 3170 | | Op::DeleteWorkspace | |
| 3092 | 3171 | | Op::UpdateWorkspace | |
| 3172 | + | | Op::ListMembers | |
| 3173 | + | | Op::UpdateMember | |
| 3174 | + | | Op::RemoveMember | |
| 3175 | + | | Op::TransferOwnership | |
| 3176 | + | | Op::LeaveWorkspace | |
| 3093 | 3177 | | Op::ListEmails | |
| 3094 | 3178 | | Op::AddEmail | |
| 3095 | 3179 | | Op::RemoveEmail | |
| 3468 | 3552 | None => return failed(FailureCode::Invalid, "team_creation is members or owners."), | |
| 3469 | 3553 | }, | |
| 3470 | 3554 | }; | |
| 3555 | + | let privileges = match g1t_contracts::members::MemberPrivilegesPatch::from_json(input) { | |
| 3556 | + | Ok(patch) => patch, | |
| 3557 | + | Err(message) => return failed(FailureCode::Invalid, &message), | |
| 3558 | + | }; | |
| 3559 | + | let two_factor = match input.get("two_factor_requirement_enabled").filter(|value| !value.is_null()) { | |
| 3560 | + | None => None, | |
| 3561 | + | Some(Value::Bool(required)) => Some(*required), | |
| 3562 | + | Some(_) => return failed(FailureCode::Invalid, "two_factor_requirement_enabled is true or false."), | |
| 3563 | + | }; | |
| 3471 | 3564 | let (name, description) = (optional_text(input, "name"), optional_text(input, "description")); | |
| 3472 | − | if base.is_none() && creation.is_none() && name.is_none() && description.is_none() { | |
| 3473 | − | return failed(FailureCode::Invalid, "Give name, description, base_permission or team_creation to change."); | |
| 3565 | + | if base.is_none() | |
| 3566 | + | && creation.is_none() | |
| 3567 | + | && name.is_none() | |
| 3568 | + | && description.is_none() | |
| 3569 | + | && privileges.is_empty() | |
| 3570 | + | && two_factor.is_none() | |
| 3571 | + | { | |
| 3572 | + | return failed( | |
| 3573 | + | FailureCode::Invalid, | |
| 3574 | + | "Give name, description, base_permission, team_creation, a member privilege or two_factor_requirement_enabled to change.", | |
| 3575 | + | ); | |
| 3474 | 3576 | } | |
| 3475 | 3577 | let found = || async { | |
| 3476 | 3578 | g1t_kit::call::<_, Option<Workspace>>(identity, "get_workspace", &json!({ "slug": workspace() })).await | |
| 3527 | 3629 | return Ok(Outcome::Fail(failure)); | |
| 3528 | 3630 | } | |
| 3529 | 3631 | } | |
| 3632 | + | if !privileges.is_empty() { | |
| 3633 | + | let set: Outcome<g1t_contracts::MemberPrivileges> = call( | |
| 3634 | + | identity, | |
| 3635 | + | "set_member_privileges", | |
| 3636 | + | &g1t_contracts::members::SetMemberPrivilegesArgs { | |
| 3637 | + | actor: actor(), | |
| 3638 | + | slug: workspace(), | |
| 3639 | + | privileges, | |
| 3640 | + | surface: Some(services.audit.surface), | |
| 3641 | + | }, | |
| 3642 | + | ) | |
| 3643 | + | .await?; | |
| 3644 | + | if let Outcome::Fail(failure) = set { | |
| 3645 | + | return Ok(Outcome::Fail(failure)); | |
| 3646 | + | } | |
| 3647 | + | } | |
| 3648 | + | if let Some(required) = two_factor { | |
| 3649 | + | let set: Outcome<bool> = call( | |
| 3650 | + | identity, | |
| 3651 | + | "set_two_factor_requirement", | |
| 3652 | + | &g1t_contracts::members::SetTwoFactorRequirementArgs { | |
| 3653 | + | actor: actor(), | |
| 3654 | + | slug: workspace(), | |
| 3655 | + | required, | |
| 3656 | + | surface: Some(services.audit.surface), | |
| 3657 | + | }, | |
| 3658 | + | ) | |
| 3659 | + | .await?; | |
| 3660 | + | if let Outcome::Fail(failure) = set { | |
| 3661 | + | return Ok(Outcome::Fail(failure)); | |
| 3662 | + | } | |
| 3663 | + | } | |
| 3530 | 3664 | match found().await? { | |
| 3531 | 3665 | Some(workspace) => ok(&workspace), | |
| 3532 | 3666 | None => failed(FailureCode::NotFound, "Workspace not found."), | |
| 3533 | 3667 | } | |
| 3534 | 3668 | } | |
| 3669 | + | Op::ListMembers => pass(identity, "list_members", &json!({ "slug": workspace(), "viewer": viewer })).await, | |
| 3670 | + | Op::UpdateMember => { | |
| 3671 | + | let role = match input.get("role").filter(|value| !value.is_null()) { | |
| 3672 | + | None => None, | |
| 3673 | + | Some(value) => match value.as_str().map(|text| text.trim().to_ascii_lowercase()).as_deref() { | |
| 3674 | + | Some("owner") | Some("admin") => Some(g1t_contracts::Role::Owner), | |
| 3675 | + | Some("member") => Some(g1t_contracts::Role::Member), | |
| 3676 | + | _ => return failed(FailureCode::Invalid, "role is owner or member."), | |
| 3677 | + | }, | |
| 3678 | + | }; | |
| 3679 | + | let org_roles = match input.get("org_roles").filter(|value| !value.is_null()) { | |
| 3680 | + | None => None, | |
| 3681 | + | Some(Value::Array(items)) => { | |
| 3682 | + | let mut roles = Vec::new(); | |
| 3683 | + | for item in items { | |
| 3684 | + | match item.as_str().and_then(g1t_contracts::OrgRole::parse) { | |
| 3685 | + | Some(role) => roles.push(role), | |
| 3686 | + | None => return failed(FailureCode::Invalid, "org_roles lists billing_manager and security_manager."), | |
| 3687 | + | } | |
| 3688 | + | } | |
| 3689 | + | Some(roles) | |
| 3690 | + | } | |
| 3691 | + | Some(_) => return failed(FailureCode::Invalid, "org_roles is a list: billing_manager, security_manager."), | |
| 3692 | + | }; | |
| 3693 | + | pass( | |
| 3694 | + | identity, | |
| 3695 | + | "update_member", | |
| 3696 | + | &g1t_contracts::members::UpdateMemberArgs { | |
| 3697 | + | actor: actor(), | |
| 3698 | + | slug: workspace(), | |
| 3699 | + | username: text(input, "username"), | |
| 3700 | + | role, | |
| 3701 | + | org_roles, | |
| 3702 | + | surface: Some(services.audit.surface), | |
| 3703 | + | }, | |
| 3704 | + | ) | |
| 3705 | + | .await | |
| 3706 | + | } | |
| 3707 | + | Op::RemoveMember => { | |
| 3708 | + | pass( | |
| 3709 | + | identity, | |
| 3710 | + | "remove_member", | |
| 3711 | + | &json!({ | |
| 3712 | + | "actor": actor(), | |
| 3713 | + | "slug": workspace(), | |
| 3714 | + | "username": text(input, "username"), | |
| 3715 | + | "surface": services.audit.surface, | |
| 3716 | + | }), | |
| 3717 | + | ) | |
| 3718 | + | .await | |
| 3719 | + | } | |
| 3720 | + | Op::TransferOwnership => { | |
| 3721 | + | pass( | |
| 3722 | + | identity, | |
| 3723 | + | "transfer_ownership", | |
| 3724 | + | &g1t_contracts::members::TransferOwnershipArgs { | |
| 3725 | + | actor: actor(), | |
| 3726 | + | slug: workspace(), | |
| 3727 | + | username: text(input, "username"), | |
| 3728 | + | surface: Some(services.audit.surface), | |
| 3729 | + | }, | |
| 3730 | + | ) | |
| 3731 | + | .await | |
| 3732 | + | } | |
| 3733 | + | Op::LeaveWorkspace => { | |
| 3734 | + | pass( | |
| 3735 | + | identity, | |
| 3736 | + | "leave_workspace", | |
| 3737 | + | &g1t_contracts::members::LeaveWorkspaceArgs { | |
| 3738 | + | user: actor(), | |
| 3739 | + | slug: workspace(), | |
| 3740 | + | surface: Some(services.audit.surface), | |
| 3741 | + | }, | |
| 3742 | + | ) | |
| 3743 | + | .await | |
| 3744 | + | } | |
| 3535 | 3745 | Op::TransferRepo => { | |
| 3536 | 3746 | pass( | |
| 3537 | 3747 | repos, | |
| 5383 | 5593 | Op::ListOutsideCollaborators, | |
| 5384 | 5594 | ]; | |
| 5385 | 5595 | ||
| 5596 | + | const MEMBERS: [Op; 5] = [Op::ListMembers, Op::UpdateMember, Op::RemoveMember, Op::TransferOwnership, Op::LeaveWorkspace]; | |
| 5597 | + | ||
| 5598 | + | /// Who belongs to a workspace, and who owns it, is people's business: | |
| 5599 | + | /// no run lists these, and agents are refused them whatever a scope says. | |
| 5600 | + | #[test] | |
| 5601 | + | fn agents_never_manage_members() { | |
| 5602 | + | use g1t_contracts::credentials::{CredentialUse, NEVER, RunCredentialKind, operations_for}; | |
| 5603 | + | for op in MEMBERS { | |
| 5604 | + | assert!(NEVER.contains(&op.name()), "{} is not in NEVER", op.name()); | |
| 5605 | + | assert!(!op.needs_repo(), "{}", op.name()); | |
| 5606 | + | assert!(op.needs_user(), "{}", op.name()); | |
| 5607 | + | for kind in RunCredentialKind::ALL { | |
| 5608 | + | for usage in [CredentialUse::Runner, CredentialUse::Tools] { | |
| 5609 | + | assert!(!operations_for(kind, usage).contains(&op.name())); | |
| 5610 | + | } | |
| 5611 | + | } | |
| 5612 | + | } | |
| 5613 | + | assert_eq!(Op::UpdateMember.input()["properties"]["org_roles"]["items"]["enum"], json!(["billing_manager", "security_manager"])); | |
| 5614 | + | } | |
| 5615 | + | ||
| 5386 | 5616 | /// Who has access is for people: no run's scope lists these, and the | |
| 5387 | 5617 | /// ones that change or reveal access are refused whatever a scope says. | |
| 5388 | 5618 | #[test] |
| 66 | 66 | "description": null, | |
| 67 | 67 | "created_at": "2026-10-04T16:02:51.337Z", | |
| 68 | 68 | "member_count": 1, | |
| 69 | − | "base_permission": "write", | |
| 70 | − | "team_creation": "members" | |
| 69 | + | "base_permission": "read", | |
| 70 | + | "team_creation": "members", | |
| 71 | + | "members_can_create_public_repositories": true, | |
| 72 | + | "members_can_create_private_repositories": true, | |
| 73 | + | "members_can_change_repo_visibility": true, | |
| 74 | + | "members_can_delete_repositories": false, | |
| 75 | + | "members_can_invite_outside_collaborators": true, | |
| 76 | + | "two_factor_requirement_enabled": false | |
| 71 | 77 | }, | |
| 72 | − | "notes": "`base_permission` is what every member gets on each of its repositories: `write` until an owner changes it with `PATCH /workspaces/{workspace}` or `PUT /workspaces/{workspace}/base_permission`. See [Access and roles](/guides/access-and-roles/). `team_creation` is who may create its teams: `members` (any member) until an owner sets `owners` with `PATCH /workspaces/{workspace}`. A new workspace is free, and each person owns at most one free workspace: while you own one, this answers `402` with `payment_required` and says which, until it is on the plan or deleted. See [One free workspace per person](/guides/usage-and-billing/#one-free-workspace-per-person)." | |
| 78 | + | "notes": "`base_permission` is what every member gets on each of its repositories: `read` for a new workspace until an owner changes it with `PATCH /workspaces/{workspace}` or `PUT /workspaces/{workspace}/base_permission`. See [Access and roles](/guides/access-and-roles/). `team_creation` is who may create its teams: `members` (any member) until an owner sets `owners` with `PATCH /workspaces/{workspace}`. A new workspace is free, and each person owns at most one free workspace: while you own one, this answers `402` with `payment_required` and says which, until it is on the plan or deleted. See [One free workspace per person](/guides/usage-and-billing/#one-free-workspace-per-person)." | |
| 73 | 79 | }, | |
| 74 | 80 | "get_workspace": { | |
| 75 | 81 | "params": { | |
| 83 | 89 | "created_at": "2026-10-04T16:02:51.337Z", | |
| 84 | 90 | "member_count": 3, | |
| 85 | 91 | "base_permission": "write", | |
| 86 | − | "team_creation": "members" | |
| 92 | + | "team_creation": "members", | |
| 93 | + | "members_can_create_public_repositories": true, | |
| 94 | + | "members_can_create_private_repositories": true, | |
| 95 | + | "members_can_change_repo_visibility": true, | |
| 96 | + | "members_can_delete_repositories": false, | |
| 97 | + | "members_can_invite_outside_collaborators": true, | |
| 98 | + | "two_factor_requirement_enabled": false | |
| 87 | 99 | }, | |
| 88 | − | "notes": "`team_creation` is who may create its teams: `members` (any member, the default) or `owners`. Change it, and the rest, with [`PATCH /workspaces/{workspace}`](/reference/api/workspaces/update-workspace/). `404` for anyone who is not a member. See [Workspaces](/guides/workspaces/)." | |
| 100 | + | "notes": "`team_creation` is who may create its teams: `members` (any member, the default) or `owners`. The `members_can_…` fields are its [member privileges](/guides/workspaces/#member-privileges), and `two_factor_requirement_enabled` whether it [requires two-factor authentication](/guides/authentication/#require-two-factor-authentication). Change it, and the rest, with [`PATCH /workspaces/{workspace}`](/reference/api/workspaces/update-workspace/). `404` for anyone who is not a member. See [Workspaces](/guides/workspaces/)." | |
| 89 | 101 | }, | |
| 90 | 102 | "update_workspace": { | |
| 91 | 103 | "params": { | |
| 94 | 106 | "request": { | |
| 95 | 107 | "name": "Acme Labs", | |
| 96 | 108 | "description": "Rockets, and the software that flies them.", | |
| 97 | − | "team_creation": "owners" | |
| 109 | + | "team_creation": "owners", | |
| 110 | + | "members_can_delete_repositories": true | |
| 98 | 111 | }, | |
| 99 | 112 | "response": { | |
| 100 | 113 | "id": "wsp_01m43teqa9em6bje0bhvdj4jkb", | |
| 104 | 117 | "created_at": "2026-10-04T16:02:51.337Z", | |
| 105 | 118 | "member_count": 3, | |
| 106 | 119 | "base_permission": "write", | |
| 107 | − | "team_creation": "owners" | |
| 120 | + | "team_creation": "owners", | |
| 121 | + | "members_can_create_public_repositories": true, | |
| 122 | + | "members_can_create_private_repositories": true, | |
| 123 | + | "members_can_change_repo_visibility": true, | |
| 124 | + | "members_can_delete_repositories": true, | |
| 125 | + | "members_can_invite_outside_collaborators": true, | |
| 126 | + | "two_factor_requirement_enabled": false | |
| 127 | + | }, | |
| 128 | + | "notes": "Only the fields given change. `name` is the display name; the slug, the first part of the workspace's addresses, stays as it is. `base_permission` needs the `access:admin` scope as well as `workspace:admin`; [`set_base_permission`](/reference/api/access/set-base-permission/) sets it alone. `team_creation` is `members` (any member may create a team, the default) or `owners`; teams already made stay. The `members_can_…` fields are the workspace's member privileges: who may create public and private repositories, and whether members with the Admin role on a repository may change its visibility, delete or transfer it, and invite outside collaborators. `two_factor_requirement_enabled` holds everyone without two-factor authentication out of the workspace until they turn it on, and needs it on your own account first. Refused with `403` for anyone but an owner signed in as a person. Recorded in the [audit log](/guides/audit-log/). See [Workspaces](/guides/workspaces/)." | |
| 129 | + | }, | |
| 130 | + | "list_members": { | |
| 131 | + | "params": { | |
| 132 | + | "workspace": "acme-labs" | |
| 133 | + | }, | |
| 134 | + | "response": [ | |
| 135 | + | { | |
| 136 | + | "username": "ada", | |
| 137 | + | "role": "owner", | |
| 138 | + | "org_roles": [], | |
| 139 | + | "two_factor": true, | |
| 140 | + | "name": "Ada Lovelace", | |
| 141 | + | "avatar": null | |
| 142 | + | }, | |
| 143 | + | { | |
| 144 | + | "username": "grace", | |
| 145 | + | "role": "member", | |
| 146 | + | "org_roles": [ | |
| 147 | + | "security_manager" | |
| 148 | + | ], | |
| 149 | + | "two_factor": false, | |
| 150 | + | "name": "Grace Hopper", | |
| 151 | + | "avatar": null | |
| 152 | + | } | |
| 153 | + | ], | |
| 154 | + | "notes": "Owners first, then by username. `two_factor` is shown to owners only, and is `null` for anyone else. `403` for anyone who is not a member. See [Workspaces](/guides/workspaces/#members-and-owners)." | |
| 155 | + | }, | |
| 156 | + | "update_member": { | |
| 157 | + | "params": { | |
| 158 | + | "workspace": "acme-labs", | |
| 159 | + | "username": "grace" | |
| 160 | + | }, | |
| 161 | + | "request": { | |
| 162 | + | "role": "member", | |
| 163 | + | "org_roles": [ | |
| 164 | + | "security_manager" | |
| 165 | + | ] | |
| 108 | 166 | }, | |
| 109 | − | "notes": "Only the fields given change. `name` is the display name; the slug, the first part of the workspace's addresses, stays as it is. `base_permission` needs the `access:admin` scope as well as `workspace:admin`; [`set_base_permission`](/reference/api/access/set-base-permission/) sets it alone. `team_creation` is `members` (any member may create a team, the default) or `owners`; teams already made stay. Refused with `403` for anyone but an owner signed in as a person. Recorded in the [audit log](/guides/audit-log/). See [Workspaces](/guides/workspaces/)." | |
| 167 | + | "response": { | |
| 168 | + | "username": "grace", | |
| 169 | + | "role": "member", | |
| 170 | + | "org_roles": [ | |
| 171 | + | "security_manager" | |
| 172 | + | ], | |
| 173 | + | "two_factor": null, | |
| 174 | + | "name": "Grace Hopper", | |
| 175 | + | "avatar": null | |
| 176 | + | }, | |
| 177 | + | "notes": "Only the fields given change; `org_roles` replaces the list. `409` when it would leave the workspace without an owner. Refused with `403` for anyone but an owner signed in as a person. Recorded in the [audit log](/guides/audit-log/) as `member.role_changed`, `member.org_role_added` or `member.org_role_removed`. See [Workspaces](/guides/workspaces/#members-and-owners)." | |
| 178 | + | }, | |
| 179 | + | "remove_member": { | |
| 180 | + | "params": { | |
| 181 | + | "workspace": "acme-labs", | |
| 182 | + | "username": "grace" | |
| 183 | + | }, | |
| 184 | + | "response": true, | |
| 185 | + | "notes": "Their roles on the workspace's repositories and their place in its teams go with them. Your own username is leaving, as [`leave_workspace`](/reference/api/members/leave-workspace/). `409` for the last owner. Recorded as `member.removed`." | |
| 110 | 186 | }, | |
| 187 | + | "transfer_ownership": { | |
| 188 | + | "params": { | |
| 189 | + | "workspace": "acme-labs" | |
| 190 | + | }, | |
| 191 | + | "request": { | |
| 192 | + | "username": "grace" | |
| 193 | + | }, | |
| 194 | + | "response": true, | |
| 195 | + | "notes": "`username` becomes an owner and you a member, in one step. Recorded as `workspace.ownership_transferred`." | |
| 196 | + | }, | |
| 197 | + | "leave_workspace": { | |
| 198 | + | "params": { | |
| 199 | + | "workspace": "acme-labs" | |
| 200 | + | }, | |
| 201 | + | "response": true, | |
| 202 | + | "notes": "`409` for the last owner: make another member an owner first, or delete the workspace. Recorded as `member.left`." | |
| 203 | + | }, | |
| 111 | 204 | "delete_workspace": { | |
| 112 | 205 | "request": { | |
| 113 | 206 | "confirm": "acme-labs" | |
| 3721 | 3814 | "request": { | |
| 3722 | 3815 | "config": { | |
| 3723 | 3816 | "base_url": "https://gpu.flagon.dev/v1", | |
| 3724 | − | "gateway_models": ["ollama/*"] | |
| 3817 | + | "gateway_models": [ | |
| 3818 | + | "ollama/*" | |
| 3819 | + | ] | |
| 3725 | 3820 | }, | |
| 3726 | 3821 | "secret": "sk-office-gpu-key" | |
| 3727 | 3822 | }, | |
| 3734 | 3829 | "config": { | |
| 3735 | 3830 | "write_back": true, | |
| 3736 | 3831 | "base_url": "https://gpu.flagon.dev/v1", | |
| 3737 | − | "gateway_models": ["ollama/*"] | |
| 3832 | + | "gateway_models": [ | |
| 3833 | + | "ollama/*" | |
| 3834 | + | ] | |
| 3738 | 3835 | }, | |
| 3739 | 3836 | "secret_hint": "…-key", | |
| 3740 | 3837 | "webhook_url": null, | |
| 3742 | 3839 | "created_at": "2026-10-07T12:10:44.512Z", | |
| 3743 | 3840 | "last_used_at": "2026-10-07T14:00:02.000Z", | |
| 3744 | 3841 | "last_error": null, | |
| 3745 | − | "models": ["llama3.3:70b", "qwen3-coder:30b"] | |
| 3842 | + | "models": [ | |
| 3843 | + | "llama3.3:70b", | |
| 3844 | + | "qwen3-coder:30b" | |
| 3845 | + | ] | |
| 3746 | 3846 | }, | |
| 3747 | 3847 | "notes": "The secret is write-only: it is kept encrypted and only its last four characters come back, as `secret_hint`. With `gateway_models` set to `ollama/*`, an AI Gateway request for `ollama/qwen3-coder:30b` reaches this endpoint as `qwen3-coder:30b`, on the workspace's own account. See [the AI Gateway guide](/guides/ai-gateway/#your-own-providers)." | |
| 3748 | 3848 | }, |
| 81 | 81 | return through::<access::RepoInvitation>(op, as_is); | |
| 82 | 82 | } | |
| 83 | 83 | Op::ListOutsideCollaborators => return through::<Vec<access::OutsideCollaborator>>(op, as_is), | |
| 84 | + | // Members, also `snake_case`. | |
| 85 | + | Op::ListMembers => return through::<Vec<g1t_contracts::identity::Member>>(op, as_is), | |
| 86 | + | Op::UpdateMember => return through::<g1t_contracts::identity::Member>(op, as_is), | |
| 87 | + | Op::RemoveMember | Op::TransferOwnership | Op::LeaveWorkspace => return through::<bool>(op, as_is), | |
| 84 | 88 | // Teams and code owners, also `snake_case`. | |
| 85 | 89 | Op::ListTeams | Op::ListChildTeams | Op::ListUserTeams => return through::<Vec<teams::Team>>(op, as_is), | |
| 86 | 90 | Op::GetTeam | Op::CreateTeam | Op::UpdateTeam | Op::SetTeamReviewAssignment => { |
| 113 | 113 | route("PATCH", "/user/repository_invitations/:id", Op::AcceptRepoInvitation, &[]), | |
| 114 | 114 | route("DELETE", "/user/repository_invitations/:id", Op::DeclineRepoInvitation, &[]), | |
| 115 | 115 | route("PATCH", "/workspaces/:workspace", Op::UpdateWorkspace, &[]), | |
| 116 | + | // Members and owners: GitHub's organization members, by username. | |
| 117 | + | route("GET", "/workspaces/:workspace/members", Op::ListMembers, &[]), | |
| 118 | + | route("PATCH", "/workspaces/:workspace/members/:username", Op::UpdateMember, &[]), | |
| 119 | + | route("DELETE", "/workspaces/:workspace/members/:username", Op::RemoveMember, &[]), | |
| 120 | + | route("POST", "/workspaces/:workspace/transfer_ownership", Op::TransferOwnership, &[]), | |
| 121 | + | route("DELETE", "/user/memberships/:workspace", Op::LeaveWorkspace, &[]), | |
| 116 | 122 | // A workspace's projects: what each is, where it runs, its links. | |
| 117 | 123 | route("GET", "/workspaces/:workspace/projects", Op::ListProjects, &[]), | |
| 118 | 124 | route("GET", "/workspaces/:workspace/projects/:project", Op::GetProject, &[]), |
| 99 | 99 | match self { | |
| 100 | 100 | RulesOp::ListRepoRulesets => "List a repository's rulesets: what may happen to its branches and tags, and what a pull request needs before it merges. With include_parents, also its workspace's rulesets that hold in it (level workspace). Each has its enforcement (active, evaluate: a dry run that records what it would have refused, or disabled), target (branch or tag), conditions (ref_name include and exclude patterns: fnmatch, ~DEFAULT_BRANCH, ~ALL), bypass_actors and rules. The one made from branch protection settings has source branch_protection.", | |
| 101 | 101 | RulesOp::GetRepoRuleset => "Get one of a repository's rulesets by id (rs_…), or one of its workspace's that holds in it.", | |
| 102 | − | RulesOp::CreateRepoRuleset => "Create a repository ruleset: name, enforcement (active, evaluate or disabled; active by default), target (branch or tag), conditions.ref_name (include and exclude patterns), bypass_actors (each a kind: role, team, user, token or g1t, a value, and a mode: always or pull_requests; nobody bypasses unless listed, g1t included) and rules (each a type, its parameters, and applies_to: everyone, agents or people). Rule types: creation, update, deletion, non_fast_forward, required_linear_history, required_signatures, pull_request, required_status_checks, merge_queue, required_deployments, commit_message_pattern, commit_author_email_pattern, committer_email_pattern, branch_name_pattern, tag_name_pattern, file_path_restriction, file_extension_restriction, max_file_size, max_file_path_length, max_files_changed, secret_scanning, confidence_threshold, cost_cap, path_review, merge_window and agent_auto_merge. Several rulesets stack: every rule of each holds. Takes the Maintain role. Returns the ruleset as saved, tidied.", | |
| 103 | − | RulesOp::UpdateRepoRuleset => "Change a repository ruleset. Fields left out stay as they are; rules and bypass_actors, when given, replace the whole list. Takes the Maintain role.", | |
| 104 | − | RulesOp::DeleteRepoRuleset => "Delete a repository ruleset. Its evaluations stay in the log. Takes the Maintain role.", | |
| 102 | + | RulesOp::CreateRepoRuleset => "Create a repository ruleset: name, enforcement (active, evaluate or disabled; active by default), target (branch or tag), conditions.ref_name (include and exclude patterns), bypass_actors (each a kind: role, team, user, token or g1t, a value, and a mode: always or pull_requests; nobody bypasses unless listed, g1t included) and rules (each a type, its parameters, and applies_to: everyone, agents or people). Rule types: creation, update, deletion, non_fast_forward, required_linear_history, required_signatures, pull_request, required_status_checks, merge_queue, required_deployments, commit_message_pattern, commit_author_email_pattern, committer_email_pattern, branch_name_pattern, tag_name_pattern, file_path_restriction, file_extension_restriction, max_file_size, max_file_path_length, max_files_changed, secret_scanning, confidence_threshold, cost_cap, path_review, merge_window and agent_auto_merge. Several rulesets stack: every rule of each holds. Takes the Admin role. Returns the ruleset as saved, tidied.", | |
| 103 | + | RulesOp::UpdateRepoRuleset => "Change a repository ruleset. Fields left out stay as they are; rules and bypass_actors, when given, replace the whole list. Takes the Admin role.", | |
| 104 | + | RulesOp::DeleteRepoRuleset => "Delete a repository ruleset. Its evaluations stay in the log. Takes the Admin role.", | |
| 105 | 105 | RulesOp::GetBranchRules => "Every rule that holds for a branch (or a tag, with target tag) of a repository, from every ruleset that targets it, the repository's and its workspace's: each with its type, parameters and applies_to, and the ruleset_id, ruleset_name, level and enforcement it comes from. Active rules come first, then those of rulesets in evaluate. rulesets lists the rulesets with who may bypass each. A branch name with slashes is URL-encoded in the path.", | |
| 106 | 106 | RulesOp::ListRuleEvaluations => "List how a repository's rulesets judged pushes, merges and other changes to its branches and tags, newest first: the ruleset, the action (push, merge, create_ref, delete_ref, rename_ref or commit), the ref, the actor and whether they are a person, an agent or g1t, the verdict (pass, fail or bypass) and each rule broken with why. A fail of a ruleset in evaluate is what it would have refused. Filter by ruleset_id or verdict, or problems_only; page with before. insights counts the last 30 days by ruleset and by rule. Takes the Write role.", | |
| 107 | 107 | RulesOp::ListWorkspaceRulesets => "List a workspace's own rulesets. Each holds in the repositories its conditions.repository selects: names matching include (fnmatch, or ~ALL) and not exclude, of a visibility (any, public or private), and carrying one of topics when given. Members only.", |
| 174 | 174 | match self { | |
| 175 | 175 | SecurityOp::ListSecretAlerts => "List secret scanning alerts: secrets found in pushes (blocked) and in history (open), newest first, in a repository or (with workspace) across a workspace. Filter by state (open, dismissed, fixed), secret_type, validity (active, inactive, unknown, unsupported) and bypassed. The secret itself is never returned: a preview and a fingerprint-based id only.", | |
| 176 | 176 | SecurityOp::GetSecretAlert => "Get one secret scanning alert by id (sec_…), with every place it was found, its activity, its bypass requests, and whether you may bypass it or only ask to.", | |
| 177 | − | SecurityOp::UpdateSecretAlert => "Dismiss a secret scanning alert (state dismissed, with a reason: false_positive, used_in_tests, revoked or wont_fix, and an optional comment) or reopen it (state open). Revoked marks it fixed; the others let pushes carrying it through. Takes the Admin role.", | |
| 177 | + | SecurityOp::UpdateSecretAlert => "Dismiss a secret scanning alert (state dismissed, with a reason: false_positive, used_in_tests, revoked or wont_fix, and an optional comment) or reopen it (state open). Revoked marks it fixed; the others let pushes carrying it through. Takes the Write role, or a security manager of the workspace.", | |
| 178 | 178 | SecurityOp::ListSecretLocations => "List every place a secret was found: file, line, commit and whether a push or the history scan found it.", | |
| 179 | 179 | SecurityOp::BypassPushProtection => "Push past push protection for a blocked secret, with a reason: false_positive or used_in_tests (the alert is closed with that reason) or will_fix_later (it stays open, to be rotated). Recorded on the alert and in the audit log. With delegated bypass on, someone who does not review bypasses makes a request instead, which owners and the repository's admins approve or deny; the answer says which happened. Push again once it is bypassed or approved.", | |
| 180 | 180 | SecurityOp::CheckSecretValidity => "Ask a landed secret's issuer whether it still works, and mark the alert active or inactive. The check is the issuer's own read-only identity call over HTTPS; the secret goes nowhere else. Needs validity checks on for the workspace (and the Security and quality activation on a private repository). Formats with no safe check answer unsupported.", | |
| 181 | 181 | SecurityOp::ListBypassRequests => "List a workspace's push protection bypass requests, pending first. Owners and repository admins see every request; anyone else their own. Filter by state (pending, approved, denied, cancelled) or repo.", | |
| 182 | − | SecurityOp::ReviewBypassRequest => "Approve or deny a bypass request (owners and the repository's admins, never your own), or cancel your own. An approved request bypasses push protection for that secret, as its requester asked.", | |
| 182 | + | SecurityOp::ReviewBypassRequest => "Approve or deny a bypass request (owners, security managers and the repository's admins, never your own), or cancel your own. An approved request bypasses push protection for that secret, as its requester asked.", | |
| 183 | 183 | SecurityOp::ListCustomPatterns => "List custom secret patterns: a repository's own and the ones it inherits from its workspace (with repo), or a workspace's (with workspace).", | |
| 184 | 184 | SecurityOp::CreateCustomPattern => "Create a custom secret pattern: a name, a regular expression for the secret, optional regular expressions for what comes right before and after it, and test strings. Patterns run in linear time (no look-around or back-references) and within size limits. With publish true, push protection and scans use it at once and the history is scanned again for it; otherwise it is a draft. A repository's takes Admin; a workspace's, an owner. On a private repository it needs the Security and quality activation.", | |
| 185 | 185 | SecurityOp::UpdateCustomPattern => "Change a custom pattern, publish it, or turn it back into a draft (publish false). Returns where it matched each test string.", | |
| 199 | 199 | SecurityOp::GetSbom => "Export the dependency graph as an SPDX 2.3 JSON document, in sbom. Every package is named by its package URL.", | |
| 200 | 200 | SecurityOp::CompareDependencies => "Compare the dependencies at two commits, branches or tags (basehead, as base...head): what was added and removed per lockfile, with the known vulnerabilities of what was added and whether it passes the repository's dependency review policy. Needs the Security and quality activation on a private repository.", | |
| 201 | 201 | SecurityOp::GetSettings => "Get a repository's security settings: when the Code scanning check fails, dependency review and its policy, its workspace's settings, and whether the paid features are on for it.", | |
| 202 | − | SecurityOp::UpdateSettings => "Change a repository's security settings: code_scanning_gate (none, errors, critical, high, medium or any), dependency_review, review_fail_on (critical, high, medium, low or none), review_deny_licenses (SPDX ids) and review_comment. Takes the Maintain role. Require the Code scanning and Dependency review checks in branch protection to gate merges on them.", | |
| 202 | + | SecurityOp::UpdateSettings => "Change a repository's security settings: code_scanning_gate (none, errors, critical, high, medium or any), dependency_review, review_fail_on (critical, high, medium, low or none), review_deny_licenses (SPDX ids) and review_comment. Takes the Admin role, or a security manager of the workspace. Require the Code scanning and Dependency review checks in branch protection to gate merges on them.", | |
| 203 | 203 | SecurityOp::GetWorkspaceSettings => "Get a workspace's security settings (delegated bypass, validity checks) and whether it has the Security and quality activation.", | |
| 204 | 204 | SecurityOp::UpdateWorkspaceSettings => "Turn delegated bypass and validity checks on or off for a workspace. Owners only.", | |
| 205 | 205 | SecurityOp::GetOverview => "Get a workspace's security overview: open alerts by type and severity, how many opened and closed in the last days (7 to 90, 30 by default), a daily trend, and for each repository which features are on and what is open, most in need first. Private repositories count with the Security and quality activation only.", |
| 339 | 339 | a("get", Op::GetWorkspace, "A workspace's details and settings"), | |
| 340 | 340 | a("create", Op::CreateWorkspace, "Create a workspace"), | |
| 341 | 341 | a("delete", Op::DeleteWorkspace, "Delete a workspace and everything in it (support can restore it for 30 days)"), | |
| 342 | − | a("update", Op::UpdateWorkspace, "Change its name, description, base permission or who may create teams"), | |
| 342 | + | a("update", Op::UpdateWorkspace, "Change its name, description, base permission, who may create teams, member privileges or the two-factor requirement"), | |
| 343 | + | a("list_members", Op::ListMembers, "Its members, owners first, with their roles"), | |
| 344 | + | a("update_member", Op::UpdateMember, "Make someone an owner or a member, billing manager or security manager"), | |
| 345 | + | a("remove_member", Op::RemoveMember, "Remove someone from it"), | |
| 346 | + | a("transfer_ownership", Op::TransferOwnership, "Hand it to another member: they become an owner, you a member"), | |
| 347 | + | a("leave", Op::LeaveWorkspace, "Leave it yourself"), | |
| 343 | 348 | a("list_invites", Op::ListWorkspaceInvites, "Its invites"), | |
| 344 | 349 | a("invite_member", Op::InviteMember, "Invite an email address"), | |
| 345 | 350 | a("revoke_invite", Op::RevokeWorkspaceInvite, "Revoke a pending invite"), | |
| 472 | 477 | | Op::Rules(RulesOp::DeleteRepoRuleset | RulesOp::DeleteWorkspaceRuleset) | |
| 473 | 478 | | Op::DeleteWorkspace | |
| 474 | 479 | | Op::UpdateWorkspace | |
| 480 | + | | Op::RemoveMember | |
| 481 | + | | Op::TransferOwnership | |
| 482 | + | | Op::LeaveWorkspace | |
| 475 | 483 | | Op::DeleteRepo | |
| 476 | 484 | | Op::PurgeRepo | |
| 477 | 485 | | Op::TransferRepo |
| 1 | 1 | --- | |
| 2 | 2 | title: Access and roles | |
| 3 | − | description: The five repository roles and what each can do, the base permission members get, roles through teams, outside collaborators and invitations, and what agents may do on a person's behalf. | |
| 3 | + | description: The five repository roles and what each can do, the base permission members get, the Admin role a repository's creator gets, security managers, roles through teams, outside collaborators and invitations, and what agents may do on a person's behalf. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 6 | 6 | Everyone who can work in a repository has a role on it. The role says what | |
| 15 | 15 | | Role | For | | |
| 16 | 16 | | --- | --- | | |
| 17 | 17 | | **Read** | Read and clone; open issues and pull requests, and comment. | | |
| 18 | − | | **Triage** | Read, and manage issues and pull requests: label, assign, close. | | |
| 19 | − | | **Write** | Triage, and push, merge, and put agents to work. | | |
| 20 | − | | **Maintain** | Write, and manage the repository's settings and branch protection. | | |
| 21 | − | | **Admin** | Everything: webhooks, secrets, deployments, who has access, and the repository's name, visibility and archiving. | | |
| 18 | + | | **Triage** | Read, and manage issues and pull requests: apply labels and milestones, assign, close. | | |
| 19 | + | | **Write** | Triage, and push, merge, manage labels and milestones, see security alerts, and put agents to work. | | |
| 20 | + | | **Maintain** | Write, and manage the repository's settings and topics. | | |
| 21 | + | | **Admin** | Everything: branch protection and rulesets, webhooks, secrets, deployments, security settings, who has access, and the repository's name, visibility and archiving. | | |
| 22 | 22 | ||
| 23 | 23 | Each role has everything the one above it has. | |
| 24 | 24 | ||
| 28 | 28 | | --- | --- | --- | --- | --- | --- | | |
| 29 | 29 | | See code, issues and pull requests; clone and fetch | Yes | Yes | Yes | Yes | Yes | | |
| 30 | 30 | | Open issues and pull requests, and comment | Yes | Yes | Yes | Yes | Yes | | |
| 31 | − | | Label, assign, close and reopen issues and pull requests | | Yes | Yes | Yes | Yes | | |
| 31 | + | | Apply labels and milestones; assign, close and reopen issues and pull requests | | Yes | Yes | Yes | Yes | | |
| 32 | 32 | | Push to branches that are not protected | | | Yes | Yes | Yes | | |
| 33 | 33 | | Merge pull requests and use the merge queue | | | Yes | Yes | Yes | | |
| 34 | + | | Create, edit and delete labels and milestones | | | Yes | Yes | Yes | | |
| 35 | + | | See and dismiss security alerts | | | Yes | Yes | Yes | | |
| 34 | 36 | | Assign agents and start runs, plans and workflows | | | Yes | Yes | Yes | | |
| 35 | 37 | | Change the description, topics, and pull request and agent settings | | | | Yes | Yes | | |
| 36 | − | | Change branch protection and guardrails | | | | Yes | Yes | | |
| 38 | + | | Change branch protection, rulesets and guardrails | | | | | Yes | | |
| 39 | + | | Change security settings, custom patterns and bypass reviews | | | | | Yes | | |
| 37 | 40 | | Manage webhooks, secrets, variables, deployments and domains | | | | | Yes | | |
| 38 | 41 | | Manage who has access, and invitations | | | | | Yes | | |
| 39 | − | | Rename, archive, change visibility and the default branch | | | | | Yes | | |
| 40 | − | | Transfer or delete the repository | | | | | Owners only | | |
| 42 | + | | Rename, archive and change the default branch | | | | | Yes | | |
| 43 | + | | Change visibility | | | | | Yes, if the [member privileges](/guides/workspaces/#member-privileges) allow | | |
| 44 | + | | Transfer or delete the repository | | | | | Owners, or Admins if the member privileges allow | | |
| 41 | 45 | ||
| 42 | − | Transferring and deleting a repository also need an owner of its | |
| 43 | − | workspace: someone given Admin on one repository cannot do either. Whoever | |
| 44 | − | opened an issue or pull request can still edit and close their own, | |
| 45 | − | whatever their role. | |
| 46 | + | Changing a repository's visibility, transferring it and deleting it also | |
| 47 | + | depend on its workspace's [member privileges](/guides/workspaces/#member-privileges). | |
| 48 | + | By default, a member with Admin can change visibility, and only an owner | |
| 49 | + | can transfer or delete. Someone given Admin on one repository without being | |
| 50 | + | a member, an outside collaborator, can do none of the three. | |
| 51 | + | ||
| 52 | + | Some things work a little differently on g1t: | |
| 46 | 53 | ||
| 54 | + | - Whoever opened an issue or pull request can still edit, label and close | |
| 55 | + | their own, whatever their role. | |
| 56 | + | - A [protected branch](/guides/git/#protected-branches) takes no pushes | |
| 57 | + | from anyone, Maintain and Admin included. To let a role push, list it as | |
| 58 | + | a bypass actor of a [ruleset](/guides/rules/) instead. | |
| 59 | + | - Applying a label the repository does not have yet creates it, for | |
| 60 | + | someone with Write. | |
| 61 | + | ||
| 47 | 62 | Read and Triage cannot put agents to work, or start anything else that | |
| 48 | 63 | spends compute: runs, plans, workflows and deployments need Write. | |
| 49 | 64 | ||
| 57 | 72 | workspace's [base permission](#the-base-permission) on every repository | |
| 58 | 73 | in it. | |
| 59 | 74 | 3. **A role given to you on that repository.** See | |
| 60 | − | [add someone to a repository](#add-someone-to-a-repository). | |
| 61 | − | 4. **Your teams.** The role each [team](/guides/teams/) you are in has on | |
| 75 | + | [add someone to a repository](#add-someone-to-a-repository). Whoever | |
| 76 | + | creates a repository is given Admin on it this way, so it stays theirs | |
| 77 | + | to run whatever the base permission is. | |
| 78 | + | 4. **Security manager.** A member who is one of the workspace's | |
| 79 | + | [security managers](/guides/workspaces/#roles-that-add-to-a-member) | |
| 80 | + | has Read on every repository, and can see and manage its security | |
| 81 | + | alerts and security settings whatever their role. | |
| 82 | + | 5. **Your teams.** The role each [team](/guides/teams/) you are in has on | |
| 62 | 83 | that repository, and the roles of that team's parent teams, which child | |
| 63 | 84 | teams inherit. See [repository access](/guides/teams/#repository-access). | |
| 64 | − | 5. **Public.** Anyone, signed in or not, can read a public repository. | |
| 85 | + | 6. **Public.** Anyone, signed in or not, can read a public repository. | |
| 65 | 86 | ||
| 66 | 87 | The highest wins. A member whose base permission is Read and who is given | |
| 67 | 88 | Maintain on one repository has Maintain there and Read everywhere else. A | |
| 83 | 104 | and a message naming the role they need: | |
| 84 | 105 | ||
| 85 | 106 | ```text | |
| 86 | − | You need the Maintain role or higher on acme/rocket to do that. | |
| 107 | + | You need the Admin role or higher on acme/rocket to do that. | |
| 87 | 108 | ``` | |
| 88 | 109 | ||
| 89 | 110 | ### Git | |
| 105 | 126 | | Base permission | Members get | | |
| 106 | 127 | | --- | --- | | |
| 107 | 128 | | **None** | Nothing beyond what is public. Members see only the private repositories they are given a role on. | | |
| 108 | − | | **Read** | Read on every repository. | | |
| 109 | − | | **Write** | Write on every repository. The default. | | |
| 110 | − | | **Admin** | Admin on every repository. Transferring and deleting stay with owners. | | |
| 129 | + | | **Read** | Read on every repository. What a new workspace starts with. | | |
| 130 | + | | **Write** | Write on every repository. | | |
| 131 | + | | **Admin** | Admin on every repository. Transferring and deleting stay with owners unless the member privileges allow them. | | |
| 111 | 132 | ||
| 112 | 133 | Owners always have Admin, whatever it says. Only owners can change it: | |
| 113 | 134 | ||
| 116 | 137 | ||
| 117 | 138 | It takes effect on everyone's next request. To give one member more on | |
| 118 | 139 | one repository, give them a role there; to give them less, lower the base | |
| 119 | − | permission and give roles to the people who need them. | |
| 140 | + | permission and give roles to the people who need them. Whoever creates a | |
| 141 | + | repository keeps Admin on it however low the base permission is. | |
| 120 | 142 | ||
| 121 | 143 | ### What changed for existing members | |
| 122 | 144 | ||
| 145 | + | A workspace made from 2026-10-08 starts at **Read**. One made before keeps | |
| 146 | + | the base permission it had, **Write** unless an owner changed it. | |
| 147 | + | ||
| 123 | 148 | Before roles, every member of a workspace could also change a repository's | |
| 124 | 149 | settings, its branch protection and guardrails, webhooks, secrets and | |
| 125 | − | variables, deployments and domains. With the default base permission, | |
| 126 | − | Write, members keep pushing, merging and putting agents to work; changing | |
| 127 | − | settings and protection now needs Maintain, and the rest Admin. Owners | |
| 150 | + | variables, deployments and domains. With Write, members keep pushing, | |
| 151 | + | merging and putting agents to work; changing settings now needs Maintain, | |
| 152 | + | and branch protection, rulesets, guardrails and the rest Admin. Owners | |
| 128 | 153 | have Admin, so they keep all of it. | |
| 129 | 154 | ||
| 155 | + | On 2026-10-08 the roles were brought in line with the table above: | |
| 156 | + | ||
| 157 | + | - Branch protection, rulesets and guardrails moved from Maintain to Admin. | |
| 158 | + | - Creating, editing and deleting labels and milestones moved from Triage | |
| 159 | + | to Write; Triage still applies them. | |
| 160 | + | - Seeing and dismissing security alerts, secrets included, takes Write; | |
| 161 | + | changing security settings and custom patterns takes Admin. | |
| 162 | + | - Whoever made each existing repository and is still a member of its | |
| 163 | + | workspace was given Admin on it, unless they had it already. | |
| 164 | + | ||
| 130 | 165 | To give members everything they had before, an owner sets the base | |
| 131 | 166 | permission to **Admin**. They then also get what only owners could do | |
| 132 | 167 | before: managing who has access, renaming and archiving repositories, and | |
| 147 | 182 | teams given a role on it, with how many people each has. People with Write | |
| 148 | 183 | or Maintain can see the lists; changing them needs Admin. | |
| 149 | 184 | ||
| 185 | + | Giving a role to someone outside the workspace takes an owner when the | |
| 186 | + | workspace's [member privileges](/guides/workspaces/#member-privileges) turn | |
| 187 | + | off **Repository admins can add outside collaborators**. | |
| 188 | + | ||
| 150 | 189 | Someone with Admin can give a team a role under **Teams with access**: | |
| 151 | 190 | pick the team and its role, and add it. Only the workspace's own teams can | |
| 152 | 191 | be added. See [teams](/guides/teams/#repository-access). | |
| 203 | 242 | (see [members and roles](/guides/workspaces/#members-and-roles)); the | |
| 204 | 243 | roles they have stay, and the base permission adds to them. | |
| 205 | 244 | ||
| 206 | − | Removing a member from a workspace also removes the roles they were given | |
| 207 | − | on its repositories, and takes them out of its teams. | |
| 245 | + | Removing a member from a workspace, or their leaving it, also removes the | |
| 246 | + | roles they were given on its repositories, and takes them out of its teams. | |
| 247 | + | ||
| 248 | + | A workspace that [requires two-factor authentication](/guides/authentication/#require-two-factor-authentication) | |
| 249 | + | holds its outside collaborators to it as it does its members: without it, | |
| 250 | + | they cannot reach its repositories until they turn it on. | |
| 208 | 251 | ||
| 209 | 252 | ## Invitations | |
| 210 | 253 | ||
| 274 | 317 | "username": "ada", | |
| 275 | 318 | "role": "write", | |
| 276 | 319 | "source": "base", | |
| 277 | − | "capabilities": ["read", "participate", "triage", "push", "merge", "run"] | |
| 320 | + | "capabilities": ["read", "participate", "triage", "push", "merge", "manage_labels", "security_alerts", "run"] | |
| 278 | 321 | } | |
| 279 | 322 | ``` | |
| 280 | 323 | ||
| 299 | 342 | ||
| 300 | 343 | The workspace's [audit log](/guides/audit-log/) records the same changes | |
| 301 | 344 | under those names, and also `repo.invitation_created`, | |
| 302 | − | `repo.invitation_revoked` and `workspace.base_permission_changed`. | |
| 345 | + | `repo.invitation_revoked` and `workspace.base_permission_changed`. A | |
| 346 | + | repository's creator being given Admin is not recorded: it comes with | |
| 347 | + | `repo.created`. | |
| 303 | 348 | ||
| 304 | 349 | A team's role on a repository changing is sent as `team.repo_added`, | |
| 305 | 350 | `team.repo_role_changed` or `team.repo_removed`; see |
| 31 | 31 | | `repo.collaborator_added`, `repo.collaborator_role_changed`, `repo.collaborator_removed` | Someone was given a role on it, had it changed, or lost it. See [access and roles](/guides/access-and-roles/). | | |
| 32 | 32 | | `repo.invitation_created`, `repo.invitation_revoked` | Someone was invited to it, or an invitation was withdrawn. | | |
| 33 | 33 | | `workspace.base_permission_changed` | An owner changed what members get on every repository. | | |
| 34 | + | | `member.added`, `member.removed`, `member.left` | Someone joined the workspace (added by an owner, or with an invite), was removed by an owner, or left. See [members and roles](/guides/workspaces/#members-and-roles). | | |
| 35 | + | | `member.role_changed` | An owner made someone an owner or a member. | | |
| 36 | + | | `member.org_role_added`, `member.org_role_removed` | An owner made someone a billing manager or a security manager, or took it away. | | |
| 37 | + | | `workspace.ownership_transferred` | An owner handed the workspace to another member. | | |
| 38 | + | | `workspace.member_privileges_changed` | An owner turned a [member privilege](/guides/workspaces/#member-privileges) on or off. | | |
| 39 | + | | `workspace.two_factor_required`, `workspace.two_factor_not_required` | An owner started or stopped [requiring two-factor authentication](/guides/workspaces/#require-two-factor-authentication). | | |
| 40 | + | | `workspace_token.created`, `workspace_token.deleted` | An owner made or deleted one of the workspace's [access tokens](/guides/workspaces/#workspace-access-tokens). | | |
| 41 | + | | `token.created`, `token.deleted`, `token.rescoped` | A member made, deleted or changed the scopes of one of their own [access tokens](/guides/authentication/#access-tokens). Recorded in each of their workspaces. | | |
| 42 | + | | `ssh_key.added`, `ssh_key.removed` | A member added or removed an SSH key. Recorded in each of their workspaces. | | |
| 43 | + | | `oauth_grant.created`, `oauth_grant.rescoped`, `oauth_grant.revoked` | A member [signed in to an application](/guides/authentication/#signing-in-with-oauth), changed what it may do, or signed it out. Recorded in each of their workspaces. | | |
| 44 | + | | `two_factor.enabled`, `two_factor.disabled` | A member turned [two-factor authentication](/guides/authentication/#two-factor-authentication) on or off. Recorded in each of their workspaces. | | |
| 34 | 45 | | `workspace.team_creation_changed` | An owner changed who can create teams. See [who can create teams](/guides/teams/#who-can-create-teams). | | |
| 35 | 46 | | `team.created`, `team.edited`, `team.deleted` | A [team](/guides/teams/) was created, changed or deleted. | | |
| 36 | 47 | | `team.member_added`, `team.member_role_changed`, `team.member_removed` | Someone was added to a team, made its maintainer or a member, or taken out of it. | | |
| 116 | 127 | ## What is not recorded | |
| 117 | 128 | ||
| 118 | 129 | - Reads by people and workspace tokens. | |
| 119 | − | - What people do on the website itself. The API, the MCP server and git | |
| 120 | − | are recorded. | |
| 130 | + | - Most of what people do on the website itself. The API, the MCP server | |
| 131 | + | and git are recorded, and so are the changes to members, roles, access, | |
| 132 | + | tokens, keys, applications, two-factor authentication and workspace | |
| 133 | + | settings in the table above, wherever they are made. | |
| 121 | 134 | - What g1t does on its own, such as closing a pull request whose agent | |
| 122 | 135 | failed. Those changes are in the pull request's timeline. |
| 1 | 1 | --- | |
| 2 | 2 | title: Accounts and authentication | |
| 3 | − | description: Accounts, invites, email addresses, confirming them, personal access tokens and their scopes, OAuth, signing in from a tool, password reset and your security log. | |
| 3 | + | description: Accounts, invites, email addresses, confirming them, two-factor authentication and recovery codes, personal access tokens and their scopes, OAuth, signing in from a tool, password reset and your security log. | |
| 4 | 4 | --- | |
| 5 | 5 | ||
| 6 | 6 | ## Creating an account | |
| 30 | 30 | | Access tokens | [`/settings/tokens`](https://g1t.sh/settings/tokens) | Your [personal access tokens](#access-tokens). | | |
| 31 | 31 | | GitHub | [`/settings/github`](https://g1t.sh/settings/github) | [Linking and unlinking GitHub](/guides/github/#link-and-unlink-github). | | |
| 32 | 32 | | Connected applications | [`/settings/applications`](https://g1t.sh/settings/applications) | Tools you [signed in to with OAuth](#signing-in-with-oauth), such as an agent using the MCP server. | | |
| 33 | + | | Two-factor authentication | [`/settings/two-factor`](https://g1t.sh/settings/two-factor) | [An authenticator app and recovery codes](#two-factor-authentication). | | |
| 33 | 34 | | Security log | [`/settings/security-log`](https://g1t.sh/settings/security-log) | [What happened to your account](#security-log). | | |
| 34 | 35 | ||
| 35 | 36 | `g1t.sh/settings` opens Profile. | |
| 44 | 45 | Making an account with GitHub needs an invite too: start from your invite | |
| 45 | 46 | link, or enter the code when g1t asks for it after GitHub. | |
| 46 | 47 | ||
| 48 | + | With [two-factor authentication](#two-factor-authentication) on, signing in | |
| 49 | + | with GitHub asks for a code from your app as well. | |
| 50 | + | ||
| 51 | + | ## Two-factor authentication | |
| 52 | + | ||
| 53 | + | Two-factor authentication asks for a code from an authenticator app on | |
| 54 | + | your phone each time you sign in with your password or with GitHub, so a | |
| 55 | + | stolen password is not enough. Any app that reads a time-based one-time | |
| 56 | + | password (TOTP) QR code works, such as 1Password, Google Authenticator or | |
| 57 | + | Authy. | |
| 58 | + | ||
| 59 | + | ### Turn it on | |
| 60 | + | ||
| 61 | + | 1. Open [Settings → Two-factor authentication](https://g1t.sh/settings/two-factor) | |
| 62 | + | and choose **Set up**. g1t asks for your password if you have not | |
| 63 | + | signed in in the last 10 minutes. | |
| 64 | + | 2. Scan the QR code with your app, or type the key shown under it. | |
| 65 | + | 3. Enter the six-digit code the app shows, and choose **Turn on**. | |
| 66 | + | 4. Save the ten recovery codes g1t shows. They are shown only then. | |
| 67 | + | ||
| 68 | + | ### Signing in with it on | |
| 69 | + | ||
| 70 | + | After your password (or GitHub), g1t asks for the code from your app. A | |
| 71 | + | code works for 30 seconds, and the one before and after it are accepted | |
| 72 | + | too, for a phone clock a little off. Each code works once. After five wrong | |
| 73 | + | codes, or ten minutes, start the sign-in again. | |
| 74 | + | ||
| 75 | + | Lost your phone? Enter a recovery code instead of the app's code. Each | |
| 76 | + | works once, and your security log records its use. | |
| 77 | + | ||
| 78 | + | Git over HTTPS never takes your password while two-factor authentication | |
| 79 | + | is on: use a [personal access token](#access-tokens) as the password, or | |
| 80 | + | [SSH](/guides/git/). Access tokens, SSH keys and OAuth applications are | |
| 81 | + | not affected. | |
| 82 | + | ||
| 83 | + | ### Recovery codes, and turning it off | |
| 84 | + | ||
| 85 | + | On the same page: | |
| 86 | + | ||
| 87 | + | - **Make new recovery codes** replaces all ten; the old ones stop working. | |
| 88 | + | - **Turn off** needs a code from your app or a recovery code, and your | |
| 89 | + | password if you have not signed in in the last 10 minutes. | |
| 90 | + | ||
| 91 | + | You cannot turn it off while you own a workspace that | |
| 92 | + | [requires it](/guides/workspaces/#require-two-factor-authentication): stop | |
| 93 | + | requiring it there first, or hand the workspace to another owner. In a | |
| 94 | + | workspace that requires it, turning it off holds you out of that workspace | |
| 95 | + | until you turn it on again. | |
| 96 | + | ||
| 97 | + | Turning it on or off, and making new recovery codes, are emailed to your | |
| 98 | + | primary and backup addresses, written to your [security log](#security-log), | |
| 99 | + | and recorded in the [audit log](/guides/audit-log/) of each of your | |
| 100 | + | workspaces as `two_factor.enabled` and `two_factor.disabled`. | |
| 101 | + | ||
| 102 | + | ### Require two-factor authentication | |
| 103 | + | ||
| 104 | + | An owner can require it of everyone with access to a workspace. See | |
| 105 | + | [Workspaces](/guides/workspaces/#require-two-factor-authentication). | |
| 106 | + | ||
| 107 | + | Passkeys are not supported yet; they are next. | |
| 108 | + | ||
| 47 | 109 | ## Invites | |
| 48 | 110 | ||
| 49 | 111 | While g1t is invite-only, every new account needs an invite code, such as | |
| 183 | 245 | ||
| 184 | 246 | ### Confirming it is you | |
| 185 | 247 | ||
| 186 | − | Adding or removing an address, and changing your primary or backup, need | |
| 187 | − | proof that it is you: a sign-in in the last 10 minutes, or your password, | |
| 248 | + | Adding or removing an address, changing your primary or backup, and | |
| 249 | + | turning two-factor authentication on or off, need proof that it is you: a | |
| 250 | + | sign-in in the last 10 minutes, or your password, | |
| 188 | 251 | which g1t asks for on the page. After you enter it, g1t does not ask again | |
| 189 | 252 | for 10 minutes. An account that signs in only with GitHub signs out and in | |
| 190 | 253 | with GitHub again, or sets a password with | |
| 596 | 659 | ||
| 597 | 660 | [Settings → Security log](https://g1t.sh/settings/security-log) lists what | |
| 598 | 661 | happened to your account: addresses added, confirmed, removed or made | |
| 599 | − | primary, your backup and privacy settings, password changes, and pauses | |
| 600 | − | after too many wrong passwords. Changes g1t staff made, such as removing an | |
| 601 | − | address someone else needed, say so and why. | |
| 662 | + | primary, your backup and privacy settings, password changes, pauses after | |
| 663 | + | too many wrong passwords, two-factor authentication turned on or off and | |
| 664 | + | recovery codes made or used, personal access tokens created, deleted or | |
| 665 | + | given new scopes, SSH keys added or removed, and applications authorized, | |
| 666 | + | changed or revoked. Changes g1t staff made, such as removing an address | |
| 667 | + | someone else needed, say so and why. | |
| 668 | + | ||
| 669 | + | Token, SSH key, application and two-factor changes are also recorded in the | |
| 670 | + | [audit log](/guides/audit-log/) of each workspace you belong to, where its | |
| 671 | + | owners see them. | |
| 602 | 672 | ||
| 603 | 673 | ## What g1t stores | |
| 604 | 674 | ||
| 605 | 675 | Passwords are stored as salted PBKDF2-SHA256 hashes. Sessions and tokens are | |
| 606 | − | stored as SHA-256 hashes. Neither can be read back. | |
| 676 | + | stored as SHA-256 hashes. Neither can be read back. A two-factor secret is | |
| 677 | + | encrypted (AES-256-GCM) and bound to your account, and recovery codes are | |
| 678 | + | kept as SHA-256 hashes. |
| 240 | 240 | ||
| 241 | 241 | ## Require review from code owners | |
| 242 | 242 | ||
| 243 | − | Someone with the Maintain role or higher turns it on under the | |
| 243 | + | Someone with the Admin role turns it on under the | |
| 244 | 244 | repository's **Settings → Rules**, in a ruleset's **Require a pull request before merging** rule: | |
| 245 | 245 | **Require review from code owners**. It is off by default. From the API it | |
| 246 | 246 | is the `pull_request` rule's `require_code_owner_review` (see [rules](/guides/rules/)). | |
| 333 | 333 | | --- | --- | --- | | |
| 334 | 334 | | `GET /repos/{owner}/{name}/codeowners/errors` | `repository` `codeowners` | The file at `ref` (the default branch when left out): where it is, its size, its rules and sections, and every error. Needs `repo:read`. | | |
| 335 | 335 | | `GET /repos/{owner}/{name}/pulls/{number}` | `pull_request` `get` | `code_owners`: the file's path, `required`, a review per rule with `section`, `pattern`, `owners`, `files`, `required`, `approved_by`, `changes_requested_by` and `satisfied`, what is `missing`, and how many `errors` the file has. Absent when the target has no file. | | |
| 336 | − | | `PATCH /repos/{owner}/{name}/settings` | `repository` `update_settings` | `require_code_owner_review`: `true` or `false`. Needs Maintain. | | |
| 336 | + | | `PATCH /repos/{owner}/{name}/settings` | `repository` `update_settings` | `require_code_owner_review`: `true` or `false`. Needs Admin. | | |
| 337 | 337 | ||
| 338 | 338 | ```sh | |
| 339 | 339 | curl "https://api.g1t.sh/repos/acme/api/codeowners/errors?ref=main" \ |
| 23 | 23 | - **Workspace defaults**: the workspace's **Settings**, **Guardrails**. | |
| 24 | 24 | Owners can change them; members can read them. | |
| 25 | 25 | - **A project's overrides**: the project's **Settings**, **Guardrails**. | |
| 26 | − | People with the Maintain [role](/guides/access-and-roles/) or higher on | |
| 26 | + | People with the Admin [role](/guides/access-and-roles/) on | |
| 27 | 27 | its repository can see and change them; the page is not shown to anyone | |
| 28 | 28 | else. | |
| 29 | 29 | ||
| 149 | 149 | no environments. | |
| 150 | 150 | ||
| 151 | 151 | Workflow-only domains are set by the same people as the rest of the page: | |
| 152 | − | owners for the workspace's, Maintain or higher for a project's. Each change | |
| 152 | + | owners for the workspace's, Admin for a project's. Each change | |
| 153 | 153 | is recorded in the workspace's [audit log](/guides/audit-log/) as | |
| 154 | 154 | `update_guardrails`, saying which domains were added or removed and what | |
| 155 | 155 | they were limited to. |
| 56 | 56 | An issue or a pull request carries at most 20 labels. | |
| 57 | 57 | ||
| 58 | 58 | You can label an issue as you open it, too: tick labels on **New issue**, | |
| 59 | − | or with the Triage role, type new ones beside them. | |
| 59 | + | or with the Write role, type new ones beside them. | |
| 60 | 60 | ||
| 61 | 61 | ## Filter by a label | |
| 62 | 62 | ||
| 73 | 73 | requests carry it; choose a count to see them. Search finds a label by its | |
| 74 | 74 | name or description. | |
| 75 | 75 | ||
| 76 | − | With the Triage role or higher you can: | |
| 76 | + | With the Write role or higher you can (applying labels needs only Triage): | |
| 77 | 77 | ||
| 78 | 78 | | To | Do this | | |
| 79 | 79 | | --- | --- | |
| 110 | 110 | | Default branch | Admin | | |
| 111 | 111 | | Rename a branch | Write; Admin for the default branch | | |
| 112 | 112 | | Rename the repository | Admin | | |
| 113 | − | | Make it public or private | Admin | | |
| 113 | + | | Make it public or private | Admin, and the [member privileges](/guides/workspaces/#member-privileges) to allow it, or an owner | | |
| 114 | 114 | | Archive or unarchive | Admin | | |
| 115 | − | | [Transfer](/guides/transferring-repositories/) | An owner of both workspaces | | |
| 116 | − | | Delete, restore and purge | An owner of its workspace | | |
| 115 | + | | [Transfer](/guides/transferring-repositories/) | An owner of its workspace, or a member with Admin when the member privileges allow it; and in the other workspace, being able to create a repository | | |
| 116 | + | | Delete | An owner of its workspace, or a member with Admin when the member privileges allow it | | |
| 117 | + | | Restore and purge | An owner of its workspace | | |
| 117 | 118 | | See the Recently deleted list | An owner of its workspace | | |
| 118 | 119 | ||
| 119 | 120 | Renaming the repository or its default branch, changing its visibility |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.